mirror of https://github.com/garrytan/gstack.git
161 lines
5.7 KiB
TypeScript
161 lines
5.7 KiB
TypeScript
import { describe, test, expect, beforeEach, afterEach } from "bun:test";
|
|
import fs from "fs";
|
|
import os from "os";
|
|
import path from "path";
|
|
import {
|
|
planRoundArtifacts,
|
|
recordRoundAttempt,
|
|
resolveImagePaths,
|
|
} from "../src/cli";
|
|
|
|
const PNG_BYTES = Buffer.from(
|
|
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkAAIAAAoAAv/lxKUAAAAASUVORK5CYII=",
|
|
"base64",
|
|
);
|
|
|
|
function writePng(filePath: string): void {
|
|
fs.writeFileSync(filePath, PNG_BYTES);
|
|
}
|
|
|
|
function writeManifest(tmpDir: string, entries: Record<string, any[]>): void {
|
|
fs.writeFileSync(
|
|
path.join(tmpDir, ".gstack-design-rounds.json"),
|
|
JSON.stringify(entries, null, 2),
|
|
);
|
|
}
|
|
|
|
describe("plan-design-review round variant preservation", () => {
|
|
let tmpDir: string;
|
|
|
|
beforeEach(() => {
|
|
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "variant-preservation-"));
|
|
});
|
|
|
|
afterEach(() => {
|
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
});
|
|
|
|
test("recommended round allocates sequence-stable suffixed paths", () => {
|
|
const alias = path.join(tmpDir, "variant-recommended.png");
|
|
const first = planRoundArtifacts(alias);
|
|
|
|
expect(first?.primaryOutput).toBe(path.join(tmpDir, "variant-recommended-A.png"));
|
|
expect(first?.aliasOutput).toBe(alias);
|
|
|
|
writeManifest(tmpDir, {
|
|
[path.join(tmpDir, "variant-recommended")]: [
|
|
{ label: "A", path: first!.primaryOutput, success: true },
|
|
],
|
|
});
|
|
|
|
const second = planRoundArtifacts(alias);
|
|
expect(second?.primaryOutput).toBe(path.join(tmpDir, "variant-recommended-B.png"));
|
|
});
|
|
|
|
test("concurrent round reservations claim distinct paths and preserve every variant", async () => {
|
|
const alias = path.join(tmpDir, "variant-recommended.png");
|
|
const plans = await Promise.all(
|
|
Array.from({ length: 3 }, async () => {
|
|
const plan = planRoundArtifacts(alias)!;
|
|
await Promise.resolve();
|
|
writePng(plan.primaryOutput);
|
|
recordRoundAttempt(plan, true);
|
|
return plan;
|
|
}),
|
|
);
|
|
|
|
expect(plans.map(plan => plan.label).sort()).toEqual(["A", "B", "C"]);
|
|
expect(new Set(plans.map(plan => plan.primaryOutput)).size).toBe(3);
|
|
expect(plans.every(plan => fs.existsSync(plan.primaryOutput))).toBe(true);
|
|
await expect(resolveImagePaths(alias)).resolves.toEqual(
|
|
plans.map(plan => plan.primaryOutput),
|
|
);
|
|
});
|
|
|
|
test("failed reservations are recorded without blocking a later variant", () => {
|
|
const alias = path.join(tmpDir, "variant-recommended.png");
|
|
const failed = planRoundArtifacts(alias)!;
|
|
recordRoundAttempt(failed, false, "API error");
|
|
|
|
const retry = planRoundArtifacts(alias)!;
|
|
writePng(retry.primaryOutput);
|
|
recordRoundAttempt(retry, true);
|
|
|
|
expect(retry.label).toBe("B");
|
|
expect(fs.existsSync(failed.primaryOutput)).toBe(false);
|
|
expect(fs.existsSync(retry.primaryOutput)).toBe(true);
|
|
});
|
|
|
|
test("recommended round alias expands to every successful generated candidate", async () => {
|
|
const alias = path.join(tmpDir, "variant-recommended.png");
|
|
const variantA = path.join(tmpDir, "variant-recommended-A.png");
|
|
const variantB = path.join(tmpDir, "variant-recommended-B.png");
|
|
writePng(variantA);
|
|
writePng(variantB);
|
|
writePng(alias);
|
|
writeManifest(tmpDir, {
|
|
[path.join(tmpDir, "variant-recommended")]: [
|
|
{ label: "A", path: variantA, success: true },
|
|
{ label: "B", path: variantB, success: true },
|
|
],
|
|
});
|
|
|
|
await expect(resolveImagePaths(alias)).resolves.toEqual([variantA, variantB]);
|
|
});
|
|
|
|
test("iteration round discovers A/B/C candidates without collapsing onto the alias", async () => {
|
|
const alias = path.join(tmpDir, "variant-iteration-01.png");
|
|
const variantA = path.join(tmpDir, "variant-iteration-01-A.png");
|
|
const variantB = path.join(tmpDir, "variant-iteration-01-B.png");
|
|
const variantC = path.join(tmpDir, "variant-iteration-01-C.png");
|
|
writePng(variantA);
|
|
writePng(variantB);
|
|
writePng(variantC);
|
|
writePng(alias);
|
|
|
|
await expect(resolveImagePaths(alias)).resolves.toEqual([variantA, variantB, variantC]);
|
|
});
|
|
|
|
test("single recommended generation preserves the suffixed candidate and ignores alias copy", async () => {
|
|
const alias = path.join(tmpDir, "variant-recommended.png");
|
|
const variantA = path.join(tmpDir, "variant-recommended-A.png");
|
|
writePng(variantA);
|
|
writePng(alias);
|
|
writeManifest(tmpDir, {
|
|
[path.join(tmpDir, "variant-recommended")]: [
|
|
{ label: "A", path: variantA, success: true },
|
|
],
|
|
});
|
|
|
|
await expect(resolveImagePaths(alias)).resolves.toEqual([variantA]);
|
|
});
|
|
|
|
test("failed sibling leaves successful candidate in place and reports missing index", async () => {
|
|
const alias = path.join(tmpDir, "variant-recommended.png");
|
|
const variantA = path.join(tmpDir, "variant-recommended-A.png");
|
|
const variantB = path.join(tmpDir, "variant-recommended-B.png");
|
|
writePng(variantA);
|
|
writeManifest(tmpDir, {
|
|
[path.join(tmpDir, "variant-recommended")]: [
|
|
{ label: "A", path: variantA, success: true },
|
|
{ label: "B", path: variantB, success: false, error: "API error" },
|
|
],
|
|
});
|
|
|
|
await expect(resolveImagePaths(alias)).rejects.toThrow("variant-recommended-B.png");
|
|
expect(fs.existsSync(variantA)).toBe(true);
|
|
});
|
|
|
|
test("initial 3-option board paths are unchanged", async () => {
|
|
const variantA = path.join(tmpDir, "variant-A.png");
|
|
const variantB = path.join(tmpDir, "variant-B.png");
|
|
const variantC = path.join(tmpDir, "variant-C.png");
|
|
writePng(variantA);
|
|
writePng(variantB);
|
|
writePng(variantC);
|
|
|
|
const input = `${variantA},${variantB},${variantC}`;
|
|
await expect(resolveImagePaths(input)).resolves.toEqual([variantA, variantB, variantC]);
|
|
});
|
|
});
|