diff --git a/src/resources/extensions/gsd/auto-recovery.ts b/src/resources/extensions/gsd/auto-recovery.ts index c8b9b0637..1557432df 100644 --- a/src/resources/extensions/gsd/auto-recovery.ts +++ b/src/resources/extensions/gsd/auto-recovery.ts @@ -436,11 +436,11 @@ export function writeBlockerPlaceholder( if (unitType === "execute-task" && mid && sid && tid) { try { updateTaskStatus(mid, sid, tid, "complete", ts); } catch (e) { logWarning("recovery", `updateTaskStatus failed during context exhaustion: ${e instanceof Error ? e.message : String(e)}`); } // Append event so worktree reconciliation can replay this recovery completion - try { appendEvent(base, { cmd: "complete-task", params: { milestoneId: mid, sliceId: sid, taskId: tid }, ts, actor: "system", trigger_reason: "blocker-placeholder-recovery" }); } catch { /* non-fatal */ } + try { appendEvent(base, { cmd: "complete-task", params: { milestoneId: mid, sliceId: sid, taskId: tid }, ts, actor: "system", trigger_reason: "blocker-placeholder-recovery" }); } catch (e) { logWarning("recovery", `appendEvent failed for task recovery: ${e instanceof Error ? e.message : String(e)}`); } } if (unitType === "complete-slice" && mid && sid) { try { updateSliceStatus(mid, sid, "complete", ts); } catch (e) { logWarning("recovery", `updateSliceStatus failed during context exhaustion: ${e instanceof Error ? e.message : String(e)}`); } - try { appendEvent(base, { cmd: "complete-slice", params: { milestoneId: mid, sliceId: sid }, ts, actor: "system", trigger_reason: "blocker-placeholder-recovery" }); } catch { /* non-fatal */ } + try { appendEvent(base, { cmd: "complete-slice", params: { milestoneId: mid, sliceId: sid }, ts, actor: "system", trigger_reason: "blocker-placeholder-recovery" }); } catch (e) { logWarning("recovery", `appendEvent failed for slice recovery: ${e instanceof Error ? e.message : String(e)}`); } } } diff --git a/src/resources/extensions/gsd/tests/wave2-events-regressions.test.ts b/src/resources/extensions/gsd/tests/wave2-events-regressions.test.ts new file mode 100644 index 000000000..d7673a9ab --- /dev/null +++ b/src/resources/extensions/gsd/tests/wave2-events-regressions.test.ts @@ -0,0 +1,48 @@ +// GSD State Machine — Wave 2 Event Log Regression Tests +// Validates fixes for appendEvent isolation, entity replay handlers, +// and post-reconcile cache invalidation. + +import { describe, test } from "node:test"; +import assert from "node:assert/strict"; +import { extractEntityKey } from "../workflow-reconcile.js"; +import type { WorkflowEvent } from "../workflow-events.js"; + +const base = { params: {}, ts: "", hash: "", actor: "agent" as const, session_id: "" }; + +// ── Fix 8: New entity event types handled by extractEntityKey ── + +describe("extractEntityKey handles plan events", () => { + test("plan-milestone → milestone type", () => { + const event: WorkflowEvent = { ...base, cmd: "plan-milestone", params: { milestoneId: "M001" } }; + const key = extractEntityKey(event); + assert.deepStrictEqual(key, { type: "milestone", id: "M001" }); + }); + + test("plan-task → task type", () => { + const event: WorkflowEvent = { ...base, cmd: "plan-task", params: { taskId: "T01" } }; + const key = extractEntityKey(event); + assert.deepStrictEqual(key, { type: "task", id: "T01" }); + }); + + test("plan-slice preserves slice_plan type (conflict isolation)", () => { + const event: WorkflowEvent = { ...base, cmd: "plan-slice", params: { sliceId: "S01" } }; + const key = extractEntityKey(event); + assert.deepStrictEqual(key, { type: "slice_plan", id: "S01" }); + }); + + test("replan-slice → slice type", () => { + const event: WorkflowEvent = { ...base, cmd: "replan-slice", params: { sliceId: "S01" } }; + const key = extractEntityKey(event); + assert.deepStrictEqual(key, { type: "slice", id: "S01" }); + }); +}); + +// ── Fix 8b: Unknown commands return null (don't crash) ── + +describe("extractEntityKey handles unknown commands gracefully", () => { + test("unknown-command returns null", () => { + const event: WorkflowEvent = { ...base, cmd: "unknown-future-cmd", params: { foo: "bar" } }; + const key = extractEntityKey(event); + assert.strictEqual(key, null); + }); +});