mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-12 07:59:02 +02:00
fix(decision-log): --supersede silently discarded the replacement decision
The supersede/redact branch appended the retirement event and exited before the JSON argument was ever read — a user recording a reversal WITH its replacement lost the replacement, and the payload finder's first-non-flag-arg predicate would have mistaken the target id for JSON anyway. Payloads are now identified by their leading brace, validated BEFORE any write, and appended FIRST (retirement second), so the only visible interleaving under a crash is both-active — recoverable, never lost. The replacement carries supersedes:<old-id> provenance. Bare --supersede <id> (the documented reversal-without-replacement) stays legal; --redact with a payload now refuses instead of dropping it. Ported from time-attack/gstack (GStack 2), tests included. Co-authored-by: Sina Matian <sina@time-attack.dev> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Sina Matian
Claude Fable 5
parent
d2e6925638
commit
225e6e4ccd
@@ -63,6 +63,39 @@ describe("gstack-decision-log", () => {
|
||||
const r = log("not json", true);
|
||||
expect(r.code).toBe(1);
|
||||
});
|
||||
test("--supersede with a replacement body records the replacement, linked to the old id", () => {
|
||||
const id = log('{"decision":"old-call","scope":"repo","source":"user"}').out;
|
||||
const out = logFlag(
|
||||
`--supersede ${id} '{"decision":"new-call","rationale":"better","scope":"repo","source":"user"}'`,
|
||||
);
|
||||
expect(out).toContain(id);
|
||||
expect(search()).toContain("new-call"); // the replacement is NOT silently dropped
|
||||
expect(search()).not.toContain("old-call");
|
||||
const arr = JSON.parse(search("--json"));
|
||||
expect(arr.find((d: any) => d.decision === "new-call")?.supersedes).toBe(id);
|
||||
});
|
||||
test("--supersede with an INVALID replacement persists nothing (old stays active)", () => {
|
||||
const id = log('{"decision":"keep-me","scope":"repo","source":"user"}').out;
|
||||
let code = 0;
|
||||
try {
|
||||
logFlag(`--supersede ${id} '{"decision":""}'`);
|
||||
} catch (e: any) {
|
||||
code = e.status || 1;
|
||||
}
|
||||
expect(code).toBe(1);
|
||||
expect(search()).toContain("keep-me"); // not retired by a failed replacement
|
||||
});
|
||||
test("--redact refuses a replacement body instead of dropping it", () => {
|
||||
const id = log('{"decision":"redact-target","scope":"repo","source":"user"}').out;
|
||||
let code = 0;
|
||||
try {
|
||||
logFlag(`--redact ${id} '{"decision":"would-be-lost","scope":"repo","source":"user"}'`);
|
||||
} catch (e: any) {
|
||||
code = e.status || 1;
|
||||
}
|
||||
expect(code).toBe(1);
|
||||
expect(search()).toContain("redact-target"); // nothing happened at all
|
||||
});
|
||||
});
|
||||
|
||||
describe("gstack-decision-search", () => {
|
||||
|
||||
Reference in New Issue
Block a user