Files
Garry TanandMatt Van Horn fe6d1ae62a v1.91.17.0 fix: mvanhorn wave (checkpoint provenance, never-overwrite design images) (#3022)
* fix: mark Remaining Work provenance so restore verifies guessed steps first (#3004)

/context-save now starts every Remaining Work item with `Open.` and ends it
with how this session knows it: `(path run)` with the observed outcome,
`(path read)` or `(path assumed)` for concrete paths, and
`(target state checked)`, `(code read)` or `(path assumed)` for writing
steps. /context-restore keeps saved order and original text but splits the
list into Next steps and Verify first (assumed, code-read, failed runs and
unmarked concrete or writing items), prints a banner for pre-marker
checkpoints, and option A walks saved order so an unverified first item is
verified before any later step runs.

Adds a prompt-byte contract test for the marker wire format and a periodic
behavior E2E (context-restore-provenance-order) that seeds an assumed item
ahead of a run item plus a code-read write.

Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>

* fix(design): never overwrite a paid design image (#1529)

Every image the API returns now goes through one persistence path
(design/src/persist.ts): mkdir, exclusive `wx` claim that bumps
x.png -> x-2.png ... x-999.png, write, fsync, close. A failure after the
bytes arrived removes the claimed file and keeps a 0600 recovery copy in
the temp dir; empty image data is a failure, never a 0-byte PNG. The API
call is the only thing inside each retry loop, so a local save failure
never buys a second image (variants, iterate fallback, generate --retry).

generate, iterate, evolve and variants (both branches) print the same run
fields (requested, saved, selected, failures, recovered) on every exit,
with exit 0 / 2 (nothing saved) / 3 (stopped after saving). Each
generate --retry attempt is its own saved image listed in `attempts`.
compare accepts --images-file and JSON-array --images. The gallery lists
bumped names, orders history by mtime then board-images.json, and marks
approvals by approved_path. New bin/gstack-design-claim publishes staged
images without clobbering; bin/gstack-design-approved resolves an
approval record (legacy letter-only records still resolve; a missing
approved_path is an error, never a substitute).

Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>

* fix(design skills): read the paths $D printed, never fixed variant names (#1529)

Every design flow now captures each generating $D command's JSON and exit
code without set -e, does round accounting before the first board, check
or inline view (stopping when nothing was saved), and carries the printed
paths forward. Boards are built from board-images.json via
compare --images-file after archiving any earlier feedback.json, and
approvals map the confirmed letter through that file and record
approved_path. design-shotgun subagents generate into a per-run mktemp
staging directory and publish with bin/gstack-design-claim instead of a
shared /tmp/variant-X.png plus cp. design-html and taste memory resolve
approvals with gstack-design-approved; design-review verifies against the
printed target path.

test/design-printed-paths.test.ts bans fixed variant names, variant globs,
variant-{letter} reads, cp/mv of $D output and literal check paths in the
generated skills and resolvers (one content-anchored allowlist entry: the
design-html discovery probe), and pins the accounting/board marker order.
test/design-approval-flow.test.ts runs the generated board and approval
blocks through a stubbed two-round flow.

* chore: release v1.91.17.0 (mvanhorn fix wave: checkpoint provenance, never-overwrite design images)

* fix(design): keep iterate sessions in the OS temp dir and normalize tripwire paths on Windows

* fix(setup-deploy): report failing status commands, specify merge-method, health and workflow checks

---------

Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
2026-10-03 16:13:45 -07:00

62 lines
3.3 KiB
TypeScript

/**
* approved.json readers: approved_path (relative to approved.json) wins, legacy
* letter-only records still resolve, and a missing approved image is an error,
* never a substitute.
*/
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
import { spawnSync } from "node:child_process";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { resolveApprovedImage } from "../src/approval";
const BIN = path.join(import.meta.dir, "..", "..", "bin", "gstack-design-approved");
let dir: string;
beforeEach(() => { dir = fs.mkdtempSync(path.join(os.tmpdir(), "design-approval-")); });
afterEach(() => { fs.rmSync(dir, { recursive: true, force: true }); });
function record(sessionDir: string, data: object): string {
fs.mkdirSync(sessionDir, { recursive: true });
const file = path.join(sessionDir, "approved.json");
fs.writeFileSync(file, JSON.stringify(data));
return file;
}
describe("resolveApprovedImage", () => {
// Value: protects=pre-upgrade letter-only records keep resolving to variant-<L>.png; fails_when=the legacy fallback is removed;
// why_new=approved_path is new and legacy records exist on disk; seam=none
test("legacy letter-only record resolves by letter", () => {
const file = record(dir, { approved_variant: "B" });
fs.writeFileSync(path.join(dir, "variant-B.png"), "b");
expect(resolveApprovedImage(file)).toEqual({ ok: true, path: path.join(dir, "variant-B.png"), legacy: true });
});
// Value: protects=a present-but-missing approved_path errors instead of resolving variant-A.png; fails_when=readers fall back to the letter;
// why_new=eng approval-fallback precedence; seam=none
test("missing approved_path errors and never substitutes the letter's image", () => {
const file = record(dir, { approved_variant: "A", approved_path: "variant-A-2.png" });
fs.writeFileSync(path.join(dir, "variant-A.png"), "round one");
const r = resolveApprovedImage(file);
expect(r).toEqual({ ok: false, error: `approved image ${path.join(dir, "variant-A-2.png")} is missing; reselect from the board` });
const cli = spawnSync(process.execPath, [BIN, file], { encoding: "utf8", timeout: 30_000 });
expect(cli.status).toBe(1);
expect(cli.stdout).toBe("");
expect(cli.stderr).toContain("reselect from the board");
});
// Value: protects=approved_path is relative, so a copied or relocated designs directory still resolves; fails_when=an absolute path is stored or resolved against cwd;
// why_new=DX relocation requirement; seam=none
test("relocated designs directory still resolves the approved image", () => {
const original = path.join(dir, "state", "designs", "home-20261003");
record(original, { approved_variant: "A", approved_path: "variant-A-2.png" });
fs.writeFileSync(path.join(original, "variant-A-2.png"), "round two");
const moved = path.join(dir, "elsewhere", "home-20261003");
fs.cpSync(original, moved, { recursive: true });
fs.rmSync(path.join(dir, "state"), { recursive: true });
const cli = spawnSync(process.execPath, [BIN, path.join(moved, "approved.json")], { encoding: "utf8", timeout: 30_000, cwd: os.tmpdir() });
expect(cli.status, cli.stderr).toBe(0);
expect(cli.stdout.trim()).toBe(path.join(moved, "variant-A-2.png"));
});
});