diff --git a/src/resources/extensions/gsd/tests/wave5-consistency-regressions.test.ts b/src/resources/extensions/gsd/tests/wave5-consistency-regressions.test.ts new file mode 100644 index 000000000..15c64b6cf --- /dev/null +++ b/src/resources/extensions/gsd/tests/wave5-consistency-regressions.test.ts @@ -0,0 +1,107 @@ +// GSD State Machine — Wave 5 Consistency Regression Tests +// Validates isClosedStatus usage in projections, upsertDecision seq preservation, +// and event schema versioning. + +import { describe, test } from "node:test"; +import assert from "node:assert/strict"; +import { mkdtempSync, rmSync } from "node:fs"; +import { join } from "node:path"; +import { tmpdir } from "node:os"; +import { isClosedStatus } from "../status-guards.js"; +import { openDatabase, closeDatabase, upsertDecision, _getAdapter } from "../gsd-db.js"; + +// ── Fix 19: isClosedStatus covers all closed statuses ── + +describe("isClosedStatus used by projections", () => { + test("skipped is closed (projections now show checked)", () => { + assert.ok(isClosedStatus("skipped")); + }); + test("complete is closed", () => { + assert.ok(isClosedStatus("complete")); + }); + test("done is closed", () => { + assert.ok(isClosedStatus("done")); + }); + test("in-progress is not closed", () => { + assert.ok(!isClosedStatus("in-progress")); + }); +}); + +// ── Fix 20: upsertDecision preserves seq on update ── + +describe("upsertDecision preserves seq column", () => { + test("seq is preserved when decision is re-upserted", () => { + const tmp = mkdtempSync(join(tmpdir(), "gsd-upsert-test-")); + const dbPath = join(tmp, "gsd.db"); + try { + openDatabase(dbPath); + const adapter = _getAdapter(); + assert.ok(adapter, "adapter must be available"); + + // Insert two decisions + upsertDecision({ + id: "D001", when_context: "ctx1", scope: "s1", + decision: "d1", choice: "c1", rationale: "r1", + revisable: "yes", made_by: "agent", superseded_by: null, + }); + upsertDecision({ + id: "D002", when_context: "ctx2", scope: "s2", + decision: "d2", choice: "c2", rationale: "r2", + revisable: "yes", made_by: "agent", superseded_by: null, + }); + + // Get original seq values + const rows1 = adapter.prepare("SELECT id, seq FROM decisions ORDER BY seq").all() as Array<{ id: string; seq: number }>; + assert.strictEqual(rows1[0].id, "D001"); + assert.strictEqual(rows1[1].id, "D002"); + const d001OriginalSeq = rows1[0].seq; + + // Re-upsert D001 with updated content + upsertDecision({ + id: "D001", when_context: "updated", scope: "s1", + decision: "d1-updated", choice: "c1", rationale: "r1", + revisable: "yes", made_by: "agent", superseded_by: null, + }); + + // Verify seq is preserved (not moved to end) + const rows2 = adapter.prepare("SELECT id, seq FROM decisions ORDER BY seq").all() as Array<{ id: string; seq: number }>; + assert.strictEqual(rows2[0].id, "D001", "D001 should still be first by seq"); + assert.strictEqual(rows2[0].seq, d001OriginalSeq, "D001 seq should be preserved"); + assert.strictEqual(rows2[1].id, "D002", "D002 should still be second"); + + // Verify content was updated + const updated = adapter.prepare("SELECT decision FROM decisions WHERE id = 'D001'").get() as { decision: string }; + assert.strictEqual(updated.decision, "d1-updated"); + + closeDatabase(); + } finally { + rmSync(tmp, { recursive: true, force: true }); + } + }); +}); + +// ── Fix 23: Event schema versioning ── + +describe("WorkflowEvent v field", () => { + test("appendEvent includes v:2 in output", async () => { + const tmp = mkdtempSync(join(tmpdir(), "gsd-event-v-test-")); + try { + const { appendEvent } = await import("../workflow-events.js"); + appendEvent(tmp, { + cmd: "test-event", + params: { foo: "bar" }, + ts: new Date().toISOString(), + actor: "system", + }); + + const { readFileSync } = await import("node:fs"); + const logPath = join(tmp, ".gsd", "event-log.jsonl"); + const line = readFileSync(logPath, "utf-8").trim(); + const event = JSON.parse(line); + assert.strictEqual(event.v, 2, "New events should have v:2"); + assert.strictEqual(event.cmd, "test-event"); + } finally { + rmSync(tmp, { recursive: true, force: true }); + } + }); +});