diff --git a/src/resources/extensions/gsd/tests/exit-command.test.ts b/src/resources/extensions/gsd/tests/exit-command.test.ts index 4f1eaed12..25a934250 100644 --- a/src/resources/extensions/gsd/tests/exit-command.test.ts +++ b/src/resources/extensions/gsd/tests/exit-command.test.ts @@ -3,7 +3,7 @@ import assert from "node:assert/strict"; import { registerExitCommand } from "../exit-command.ts"; -test("/exit requests graceful shutdown instead of process.exit", async () => { +test("/exit requests graceful shutdown instead of process.exit", async (t) => { const commands = new Map< string, { @@ -35,15 +35,13 @@ test("/exit requests graceful shutdown instead of process.exit", async () => { throw new Error(`process.exit should not be called: ${code ?? "undefined"}`); }) as typeof process.exit; - try { - await exit.handler("", { - async shutdown() { - shutdownCalls += 1; - }, - }); - } finally { - process.exit = originalExit; - } + t.after(() => { process.exit = originalExit; }); + + await exit.handler("", { + async shutdown() { + shutdownCalls += 1; + }, + }); assert.equal(stopAutoCalls, 1, "handler should stop auto-mode exactly once before shutdown"); assert.equal(shutdownCalls, 1, "handler should request graceful shutdown exactly once"); @@ -51,7 +49,7 @@ test("/exit requests graceful shutdown instead of process.exit", async () => { // ─── #1839 regression: ESM cache mismatch must not crash exit ──────────────── -test("/exit still shuts down gracefully when stopAuto throws (ESM module cache mismatch)", async () => { +test("/exit still shuts down gracefully when stopAuto throws (ESM module cache mismatch)", async (t) => { const commands = new Map Promise }>(); const pi = { @@ -80,20 +78,18 @@ test("/exit still shuts down gracefully when stopAuto throws (ESM module cache m throw new Error(`process.exit should not be called: ${code ?? "undefined"}`); }) as typeof process.exit; - try { - await exit.handler("", { - async shutdown() { - shutdownCalls += 1; + t.after(() => { process.exit = originalExit; }); + + await exit.handler("", { + async shutdown() { + shutdownCalls += 1; + }, + ui: { + notify(msg: string, level: string) { + notifications.push({ msg, level }); }, - ui: { - notify(msg: string, level: string) { - notifications.push({ msg, level }); - }, - }, - }); - } finally { - process.exit = originalExit; - } + }, + }); assert.equal(shutdownCalls, 1, "shutdown must still be called even when stopAuto throws"); assert.equal(notifications.length, 1, "should emit exactly one warning notification"); diff --git a/src/resources/extensions/gsd/tests/files-loadfile-eisdir.test.ts b/src/resources/extensions/gsd/tests/files-loadfile-eisdir.test.ts index cff1d4876..c0bc25d19 100644 --- a/src/resources/extensions/gsd/tests/files-loadfile-eisdir.test.ts +++ b/src/resources/extensions/gsd/tests/files-loadfile-eisdir.test.ts @@ -6,15 +6,13 @@ import fs from "node:fs"; import { loadFile } from "../files.ts"; -test("loadFile returns null for directory paths instead of throwing EISDIR", async () => { +test("loadFile returns null for directory paths instead of throwing EISDIR", async (t) => { const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "gsd-loadfile-eisdir-")); const dirPath = path.join(tmp, "tasks"); fs.mkdirSync(dirPath); - try { - const result = await loadFile(dirPath); - assert.equal(result, null); - } finally { - fs.rmSync(tmp, { recursive: true, force: true }); - } + t.after(() => { fs.rmSync(tmp, { recursive: true, force: true }); }); + + const result = await loadFile(dirPath); + assert.equal(result, null); }); diff --git a/src/resources/extensions/gsd/tests/gitignore-tracked-gsd.test.ts b/src/resources/extensions/gsd/tests/gitignore-tracked-gsd.test.ts index b9bda919a..b73512e3d 100644 --- a/src/resources/extensions/gsd/tests/gitignore-tracked-gsd.test.ts +++ b/src/resources/extensions/gsd/tests/gitignore-tracked-gsd.test.ts @@ -53,43 +53,37 @@ function cleanup(dir: string): void { // ─── hasGitTrackedGsdFiles ─────────────────────────────────────────── -test("hasGitTrackedGsdFiles returns false when .gsd/ does not exist", () => { +test("hasGitTrackedGsdFiles returns false when .gsd/ does not exist", (t) => { const dir = makeTempRepo(); - try { - assert.equal(hasGitTrackedGsdFiles(dir), false); - } finally { - cleanup(dir); - } + t.after(() => { cleanup(dir); }); + + assert.equal(hasGitTrackedGsdFiles(dir), false); }); -test("hasGitTrackedGsdFiles returns true when .gsd/ has tracked files", () => { +test("hasGitTrackedGsdFiles returns true when .gsd/ has tracked files", (t) => { const dir = makeTempRepo(); - try { - mkdirSync(join(dir, ".gsd", "milestones"), { recursive: true }); - writeFileSync(join(dir, ".gsd", "PROJECT.md"), "# Test Project\n"); - git(dir, "add", ".gsd/PROJECT.md"); - git(dir, "commit", "-m", "add gsd"); - assert.equal(hasGitTrackedGsdFiles(dir), true); - } finally { - cleanup(dir); - } + t.after(() => { cleanup(dir); }); + + mkdirSync(join(dir, ".gsd", "milestones"), { recursive: true }); + writeFileSync(join(dir, ".gsd", "PROJECT.md"), "# Test Project\n"); + git(dir, "add", ".gsd/PROJECT.md"); + git(dir, "commit", "-m", "add gsd"); + assert.equal(hasGitTrackedGsdFiles(dir), true); }); -test("hasGitTrackedGsdFiles returns false when .gsd/ exists but is untracked", () => { +test("hasGitTrackedGsdFiles returns false when .gsd/ exists but is untracked", (t) => { const dir = makeTempRepo(); - try { - mkdirSync(join(dir, ".gsd"), { recursive: true }); - writeFileSync(join(dir, ".gsd", "STATE.md"), "state\n"); - // Not git-added — should return false - assert.equal(hasGitTrackedGsdFiles(dir), false); - } finally { - cleanup(dir); - } + t.after(() => { cleanup(dir); }); + + mkdirSync(join(dir, ".gsd"), { recursive: true }); + writeFileSync(join(dir, ".gsd", "STATE.md"), "state\n"); + // Not git-added — should return false + assert.equal(hasGitTrackedGsdFiles(dir), false); }); // ─── ensureGitignore — tracked .gsd/ protection ───────────────────── -test("ensureGitignore does NOT add .gsd when .gsd/ has tracked files (#1364)", () => { +test("ensureGitignore does NOT add .gsd when .gsd/ has tracked files (#1364)", (t) => { const dir = makeTempRepo(); try { // Set up .gsd/ with tracked files @@ -118,7 +112,7 @@ test("ensureGitignore does NOT add .gsd when .gsd/ has tracked files (#1364)", ( } }); -test("ensureGitignore adds .gsd when .gsd/ has NO tracked files", () => { +test("ensureGitignore adds .gsd when .gsd/ has NO tracked files", (t) => { const dir = makeTempRepo(); try { // Run ensureGitignore (no .gsd/ at all) @@ -136,20 +130,18 @@ test("ensureGitignore adds .gsd when .gsd/ has NO tracked files", () => { } }); -test("ensureGitignore respects manageGitignore: false", () => { +test("ensureGitignore respects manageGitignore: false", (t) => { const dir = makeTempRepo(); - try { - const result = ensureGitignore(dir, { manageGitignore: false }); - assert.equal(result, false); - assert.ok(!existsSync(join(dir, ".gitignore")), "Should not create .gitignore"); - } finally { - cleanup(dir); - } + t.after(() => { cleanup(dir); }); + + const result = ensureGitignore(dir, { manageGitignore: false }); + assert.equal(result, false); + assert.ok(!existsSync(join(dir, ".gitignore")), "Should not create .gitignore"); }); // ─── ensureGitignore — verify no tracked files become invisible ───── -test("ensureGitignore with tracked .gsd/ does not cause git to see files as deleted", () => { +test("ensureGitignore with tracked .gsd/ does not cause git to see files as deleted", (t) => { const dir = makeTempRepo(); try { // Create tracked .gsd/ files @@ -183,7 +175,7 @@ test("ensureGitignore with tracked .gsd/ does not cause git to see files as dele } }); -test("hasGitTrackedGsdFiles returns true (fail-safe) when git is not available", () => { +test("hasGitTrackedGsdFiles returns true (fail-safe) when git is not available", (t) => { const dir = makeTempRepo(); try { // Create and track .gsd/ files @@ -207,7 +199,7 @@ test("hasGitTrackedGsdFiles returns true (fail-safe) when git is not available", // ─── migrateToExternalState — tracked .gsd/ protection ────────────── -test("migrateToExternalState aborts when .gsd/ has tracked files (#1364)", () => { +test("migrateToExternalState aborts when .gsd/ has tracked files (#1364)", (t) => { const dir = makeTempRepo(); try { // Create tracked .gsd/ files @@ -235,7 +227,7 @@ test("migrateToExternalState aborts when .gsd/ has tracked files (#1364)", () => } }); -test("migrateToExternalState cleans git index so tracked files don't show as deleted (#1364 path 2)", () => { +test("migrateToExternalState cleans git index so tracked files don't show as deleted (#1364 path 2)", (t) => { const dir = makeTempRepo(); try { // Track .gsd/ files, then untrack them so migration proceeds diff --git a/src/resources/extensions/gsd/tests/graph-operations.test.ts b/src/resources/extensions/gsd/tests/graph-operations.test.ts index 229557c0d..c73696604 100644 --- a/src/resources/extensions/gsd/tests/graph-operations.test.ts +++ b/src/resources/extensions/gsd/tests/graph-operations.test.ts @@ -56,7 +56,7 @@ function makeStep(overrides: Partial & { id: string }): GraphStep { // ─── writeGraph + readGraph round-trip ─────────────────────────────────── describe("writeGraph + readGraph round-trip", () => { - it("preserves all fields including parentStepId and dependsOn", () => { + it("preserves all fields including parentStepId and dependsOn", (t) => { const dir = makeTmpDir(); try { const graph = makeGraph([ @@ -89,7 +89,7 @@ describe("writeGraph + readGraph round-trip", () => { } }); - it("preserves startedAt and finishedAt fields", () => { + it("preserves startedAt and finishedAt fields", (t) => { const dir = makeTmpDir(); try { const graph = makeGraph([ @@ -110,7 +110,7 @@ describe("writeGraph + readGraph round-trip", () => { } }); - it("creates directory if it does not exist", () => { + it("creates directory if it does not exist", (t) => { const base = makeTmpDir(); const nested = join(base, "sub", "dir"); try { @@ -129,59 +129,53 @@ describe("writeGraph + readGraph round-trip", () => { // ─── readGraph error paths ─────────────────────────────────────────────── describe("readGraph error paths", () => { - it("throws with descriptive error when file is missing", () => { + it("throws with descriptive error when file is missing", (t) => { const dir = makeTmpDir(); - try { - assert.throws( - () => readGraph(dir), - (err: Error) => { - assert.ok(err.message.includes("GRAPH.yaml not found")); - assert.ok(err.message.includes(dir)); - return true; - }, - ); - } finally { - cleanupDir(dir); - } + t.after(() => { cleanupDir(dir); }); + + assert.throws( + () => readGraph(dir), + (err: Error) => { + assert.ok(err.message.includes("GRAPH.yaml not found")); + assert.ok(err.message.includes(dir)); + return true; + }, + ); }); - it("throws with descriptive error when YAML is malformed (missing steps)", () => { + it("throws with descriptive error when YAML is malformed (missing steps)", (t) => { const dir = makeTmpDir(); - try { - writeFileSync(join(dir, "GRAPH.yaml"), "metadata:\n name: bad\n", "utf-8"); - assert.throws( - () => readGraph(dir), - (err: Error) => { - assert.ok(err.message.includes("missing or invalid 'steps' array")); - return true; - }, - ); - } finally { - cleanupDir(dir); - } + t.after(() => { cleanupDir(dir); }); + + writeFileSync(join(dir, "GRAPH.yaml"), "metadata:\n name: bad\n", "utf-8"); + assert.throws( + () => readGraph(dir), + (err: Error) => { + assert.ok(err.message.includes("missing or invalid 'steps' array")); + return true; + }, + ); }); - it("throws when steps is not an array", () => { + it("throws when steps is not an array", (t) => { const dir = makeTmpDir(); - try { - writeFileSync(join(dir, "GRAPH.yaml"), "steps: not-an-array\nmetadata:\n name: bad\n", "utf-8"); - assert.throws( - () => readGraph(dir), - (err: Error) => { - assert.ok(err.message.includes("missing or invalid 'steps' array")); - return true; - }, - ); - } finally { - cleanupDir(dir); - } + t.after(() => { cleanupDir(dir); }); + + writeFileSync(join(dir, "GRAPH.yaml"), "steps: not-an-array\nmetadata:\n name: bad\n", "utf-8"); + assert.throws( + () => readGraph(dir), + (err: Error) => { + assert.ok(err.message.includes("missing or invalid 'steps' array")); + return true; + }, + ); }); }); // ─── getNextPendingStep ────────────────────────────────────────────────── describe("getNextPendingStep", () => { - it("returns first step with all deps complete", () => { + it("returns first step with all deps complete", (t) => { const graph = makeGraph([ makeStep({ id: "a", status: "complete" }), makeStep({ id: "b", dependsOn: ["a"] }), @@ -192,7 +186,7 @@ describe("getNextPendingStep", () => { assert.equal(next?.id, "b"); }); - it("skips steps with incomplete deps", () => { + it("skips steps with incomplete deps", (t) => { const graph = makeGraph([ makeStep({ id: "a" }), makeStep({ id: "b", dependsOn: ["a"] }), @@ -203,7 +197,7 @@ describe("getNextPendingStep", () => { assert.equal(next?.id, "a"); }); - it("returns null when all steps are complete", () => { + it("returns null when all steps are complete", (t) => { const graph = makeGraph([ makeStep({ id: "a", status: "complete" }), makeStep({ id: "b", status: "complete" }), @@ -212,7 +206,7 @@ describe("getNextPendingStep", () => { assert.equal(getNextPendingStep(graph), null); }); - it("returns null when all pending steps are blocked", () => { + it("returns null when all pending steps are blocked", (t) => { const graph = makeGraph([ makeStep({ id: "a", status: "active" }), // not complete makeStep({ id: "b", dependsOn: ["a"] }), // blocked @@ -221,7 +215,7 @@ describe("getNextPendingStep", () => { assert.equal(getNextPendingStep(graph), null); }); - it("returns first pending step with no deps when root steps exist", () => { + it("returns first pending step with no deps when root steps exist", (t) => { const graph = makeGraph([ makeStep({ id: "a" }), makeStep({ id: "b" }), @@ -231,7 +225,7 @@ describe("getNextPendingStep", () => { assert.equal(next?.id, "a"); }); - it("skips expanded steps", () => { + it("skips expanded steps", (t) => { const graph = makeGraph([ makeStep({ id: "a", status: "expanded" }), makeStep({ id: "b" }), @@ -245,7 +239,7 @@ describe("getNextPendingStep", () => { // ─── markStepComplete ──────────────────────────────────────────────────── describe("markStepComplete", () => { - it("returns new graph with step status 'complete' (original unchanged)", () => { + it("returns new graph with step status 'complete' (original unchanged)", (t) => { const original = makeGraph([ makeStep({ id: "a" }), makeStep({ id: "b" }), @@ -264,7 +258,7 @@ describe("markStepComplete", () => { assert.equal(updated.steps[1].status, "pending"); }); - it("sets finishedAt timestamp", () => { + it("sets finishedAt timestamp", (t) => { const graph = makeGraph([makeStep({ id: "a" })]); const updated = markStepComplete(graph, "a"); assert.ok(updated.steps[0].finishedAt); @@ -272,7 +266,7 @@ describe("markStepComplete", () => { assert.ok(!isNaN(Date.parse(updated.steps[0].finishedAt!))); }); - it("throws for unknown step ID", () => { + it("throws for unknown step ID", (t) => { const graph = makeGraph([makeStep({ id: "a" })]); assert.throws( () => markStepComplete(graph, "nonexistent"), @@ -284,7 +278,7 @@ describe("markStepComplete", () => { ); }); - it("preserves metadata in returned graph", () => { + it("preserves metadata in returned graph", (t) => { const graph = makeGraph([makeStep({ id: "a" })], "my-workflow"); const updated = markStepComplete(graph, "a"); assert.equal(updated.metadata.name, "my-workflow"); @@ -295,7 +289,7 @@ describe("markStepComplete", () => { // ─── expandIteration ───────────────────────────────────────────────────── describe("expandIteration", () => { - it("creates instance steps with correct IDs (stepId--001, stepId--002)", () => { + it("creates instance steps with correct IDs (stepId--001, stepId--002)", (t) => { const graph = makeGraph([ makeStep({ id: "iter-step", title: "Process items" }), makeStep({ id: "final", dependsOn: ["iter-step"] }), @@ -317,7 +311,7 @@ describe("expandIteration", () => { assert.equal(expanded.steps[3].id, "iter-step--003"); }); - it("marks parent step as 'expanded'", () => { + it("marks parent step as 'expanded'", (t) => { const graph = makeGraph([ makeStep({ id: "iter", title: "Iterate" }), ]); @@ -326,7 +320,7 @@ describe("expandIteration", () => { assert.equal(expanded.steps[0].status, "expanded"); }); - it("instance steps have correct titles, prompts, parentStepId, and deps", () => { + it("instance steps have correct titles, prompts, parentStepId, and deps", (t) => { const graph = makeGraph([ makeStep({ id: "pre", status: "complete" }), makeStep({ id: "iter", title: "Process", dependsOn: ["pre"] }), @@ -352,7 +346,7 @@ describe("expandIteration", () => { assert.equal(inst2.parentStepId, "iter"); }); - it("rewrites downstream deps from parent ID to all instance IDs", () => { + it("rewrites downstream deps from parent ID to all instance IDs", (t) => { const graph = makeGraph([ makeStep({ id: "iter", title: "Iterate" }), makeStep({ id: "after", dependsOn: ["iter"] }), @@ -370,7 +364,7 @@ describe("expandIteration", () => { assert.deepStrictEqual(afterStep.dependsOn, ["iter--001", "iter--002"]); }); - it("preserves steps that don't depend on the parent", () => { + it("preserves steps that don't depend on the parent", (t) => { const graph = makeGraph([ makeStep({ id: "unrelated" }), makeStep({ id: "iter", title: "Iterate" }), @@ -382,7 +376,7 @@ describe("expandIteration", () => { assert.deepStrictEqual(unrelated.dependsOn, []); }); - it("throws for non-pending parent step", () => { + it("throws for non-pending parent step", (t) => { const graph = makeGraph([ makeStep({ id: "iter", status: "complete" }), ]); @@ -397,7 +391,7 @@ describe("expandIteration", () => { ); }); - it("throws for unknown step ID", () => { + it("throws for unknown step ID", (t) => { const graph = makeGraph([makeStep({ id: "a" })]); assert.throws( () => expandIteration(graph, "nonexistent", ["a"], "{{item}}"), @@ -409,7 +403,7 @@ describe("expandIteration", () => { ); }); - it("does not mutate the input graph", () => { + it("does not mutate the input graph", (t) => { const graph = makeGraph([ makeStep({ id: "iter", title: "Iterate" }), makeStep({ id: "after", dependsOn: ["iter"] }), @@ -430,7 +424,7 @@ describe("expandIteration", () => { // ─── initializeGraph ───────────────────────────────────────────────────── describe("initializeGraph", () => { - it("converts a valid 3-step definition to graph with all pending steps", () => { + it("converts a valid 3-step definition to graph with all pending steps", (t) => { const def: WorkflowDefinition = { version: 1, name: "test-workflow", @@ -465,7 +459,7 @@ describe("initializeGraph", () => { assert.deepStrictEqual(graph.steps[2].dependsOn, ["s1", "s2"]); }); - it("is also exported as graphFromDefinition (backward compat)", () => { + it("is also exported as graphFromDefinition (backward compat)", (t) => { assert.equal(graphFromDefinition, initializeGraph); }); }); @@ -473,7 +467,7 @@ describe("initializeGraph", () => { // ─── Atomic write safety ───────────────────────────────────────────────── describe("atomic write safety", () => { - it("final file exists and .tmp file does not exist after write", () => { + it("final file exists and .tmp file does not exist after write", (t) => { const dir = makeTmpDir(); try { const graph = makeGraph([makeStep({ id: "s1" })]); @@ -486,7 +480,7 @@ describe("atomic write safety", () => { } }); - it("YAML content is valid and parseable", () => { + it("YAML content is valid and parseable", (t) => { const dir = makeTmpDir(); try { const graph = makeGraph([makeStep({ id: "s1" })]); @@ -507,7 +501,7 @@ describe("atomic write safety", () => { // ─── YAML snake_case / camelCase boundary ──────────────────────────────── describe("YAML snake_case / camelCase boundary", () => { - it("writes snake_case to disk and reads back as camelCase", () => { + it("writes snake_case to disk and reads back as camelCase", (t) => { const dir = makeTmpDir(); try { const graph = makeGraph([ @@ -541,7 +535,7 @@ describe("YAML snake_case / camelCase boundary", () => { } }); - it("omits optional fields from YAML when undefined", () => { + it("omits optional fields from YAML when undefined", (t) => { const dir = makeTmpDir(); try { const graph = makeGraph([ @@ -565,7 +559,7 @@ describe("YAML snake_case / camelCase boundary", () => { // ─── Edge cases ────────────────────────────────────────────────────────── describe("edge cases", () => { - it("handles empty items array in expandIteration", () => { + it("handles empty items array in expandIteration", (t) => { const graph = makeGraph([ makeStep({ id: "iter" }), ]); @@ -576,7 +570,7 @@ describe("edge cases", () => { assert.equal(expanded.steps[0].status, "expanded"); }); - it("handles graph with single step", () => { + it("handles graph with single step", (t) => { const graph = makeGraph([makeStep({ id: "only" })]); const next = getNextPendingStep(graph); assert.equal(next?.id, "only"); @@ -585,7 +579,7 @@ describe("edge cases", () => { assert.equal(getNextPendingStep(completed), null); }); - it("initializeGraph handles steps with empty requires", () => { + it("initializeGraph handles steps with empty requires", (t) => { const def: WorkflowDefinition = { version: 1, name: "empty-requires", diff --git a/src/resources/extensions/gsd/tests/headless-answers.test.ts b/src/resources/extensions/gsd/tests/headless-answers.test.ts index e59cc8f83..a6796fc81 100644 --- a/src/resources/extensions/gsd/tests/headless-answers.test.ts +++ b/src/resources/extensions/gsd/tests/headless-answers.test.ts @@ -23,7 +23,7 @@ function makeTempDir(prefix: string): string { // loadAndValidateAnswerFile // --------------------------------------------------------------------------- -test('loadAndValidateAnswerFile — valid file', () => { +test('loadAndValidateAnswerFile — valid file', (t) => { const tmp = makeTempDir('answers-valid'); try { const data = { @@ -43,7 +43,7 @@ test('loadAndValidateAnswerFile — valid file', () => { } }); -test('loadAndValidateAnswerFile — invalid JSON', () => { +test('loadAndValidateAnswerFile — invalid JSON', (t) => { const tmp = makeTempDir('answers-bad-json'); try { const filePath = join(tmp, 'answers.json'); @@ -58,7 +58,7 @@ test('loadAndValidateAnswerFile — invalid JSON', () => { } }); -test('loadAndValidateAnswerFile — wrong types (non-string question value)', () => { +test('loadAndValidateAnswerFile — wrong types (non-string question value)', (t) => { const tmp = makeTempDir('answers-bad-q'); try { const filePath = join(tmp, 'answers.json'); @@ -73,7 +73,7 @@ test('loadAndValidateAnswerFile — wrong types (non-string question value)', () } }); -test('loadAndValidateAnswerFile — wrong types (non-string secret value)', () => { +test('loadAndValidateAnswerFile — wrong types (non-string secret value)', (t) => { const tmp = makeTempDir('answers-bad-secret'); try { const filePath = join(tmp, 'answers.json'); @@ -116,7 +116,7 @@ function makeSelectEvent( }; } -test('observeEvent stores metadata', () => { +test('observeEvent stores metadata', (t) => { const injector = new AnswerInjector({}); injector.observeEvent(makeToolExecutionStart([{ @@ -140,7 +140,7 @@ test('observeEvent stores metadata', () => { assert.strictEqual(injector.getStats().questionsDefaulted, 1); }); -test('tryHandle matches by question ID — single select', () => { +test('tryHandle matches by question ID — single select', (t) => { const injector = new AnswerInjector({ questions: { deploy_target: 'GCP' } }); injector.observeEvent(makeToolExecutionStart([{ @@ -164,7 +164,7 @@ test('tryHandle matches by question ID — single select', () => { assert.strictEqual(injector.getStats().questionsAnswered, 1); }); -test('tryHandle unknown question deferred — first_option timeout', async () => { +test('tryHandle unknown question deferred — first_option timeout', async (t) => { const injector = new AnswerInjector({ defaults: { strategy: 'first_option' } }); const captured: string[] = []; @@ -188,7 +188,7 @@ test('tryHandle unknown question deferred — first_option timeout', async () => assert.strictEqual(injector.getStats().questionsDefaulted, 1); }); -test('tryHandle multi-select', () => { +test('tryHandle multi-select', (t) => { const injector = new AnswerInjector({ questions: { features: ['auth', 'payments'] } }); injector.observeEvent(makeToolExecutionStart([{ @@ -218,7 +218,7 @@ test('tryHandle multi-select', () => { assert.strictEqual(injector.getStats().questionsAnswered, 1); }); -test('tryHandle answer not in options — first_option strategy returns false', () => { +test('tryHandle answer not in options — first_option strategy returns false', (t) => { const injector = new AnswerInjector({ questions: { deploy_target: 'Azure' } }); injector.observeEvent(makeToolExecutionStart([{ @@ -240,7 +240,7 @@ test('tryHandle answer not in options — first_option strategy returns false', assert.strictEqual(injector.getStats().questionsAnswered, 0); }); -test('tryHandle deferred resolution — observeEvent after tryHandle', async () => { +test('tryHandle deferred resolution — observeEvent after tryHandle', async (t) => { const injector = new AnswerInjector({ questions: { deploy_target: 'GCP' } }); const captured: string[] = []; @@ -272,7 +272,7 @@ test('tryHandle deferred resolution — observeEvent after tryHandle', async () // AnswerInjector — getSecretEnvVars // --------------------------------------------------------------------------- -test('getSecretEnvVars returns secrets map', () => { +test('getSecretEnvVars returns secrets map', (t) => { const secrets = { API_KEY: 'sk-123', DB_URL: 'postgres://localhost/db' }; const injector = new AnswerInjector({ secrets }); @@ -283,7 +283,7 @@ test('getSecretEnvVars returns secrets map', () => { // AnswerInjector — getUnusedWarnings // --------------------------------------------------------------------------- -test('getUnusedWarnings reports unused question IDs and secret keys', () => { +test('getUnusedWarnings reports unused question IDs and secret keys', (t) => { const injector = new AnswerInjector({ questions: { q1: 'val1', q2: 'val2' }, secrets: { KEY1: 'v1' }, @@ -314,7 +314,7 @@ test('getUnusedWarnings reports unused question IDs and secret keys', () => { // AnswerInjector — defaults.strategy cancel // --------------------------------------------------------------------------- -test('defaults.strategy cancel — sends cancelled response', () => { +test('defaults.strategy cancel — sends cancelled response', (t) => { const injector = new AnswerInjector({ defaults: { strategy: 'cancel' } }); injector.observeEvent(makeToolExecutionStart([{ diff --git a/src/resources/extensions/gsd/tests/health-widget.test.ts b/src/resources/extensions/gsd/tests/health-widget.test.ts index fc4898af7..b918e8b54 100644 --- a/src/resources/extensions/gsd/tests/health-widget.test.ts +++ b/src/resources/extensions/gsd/tests/health-widget.test.ts @@ -39,61 +39,55 @@ function activeData(overrides: Partial = {}): HealthWidgetData }; } -test("detectHealthWidgetProjectState: no .gsd returns none", () => { +test("detectHealthWidgetProjectState: no .gsd returns none", (t) => { const dir = makeTempDir("none"); - try { - assert.equal(detectHealthWidgetProjectState(dir), "none"); - } finally { - cleanup(dir); - } + t.after(() => { cleanup(dir); }); + + assert.equal(detectHealthWidgetProjectState(dir), "none"); }); -test("detectHealthWidgetProjectState: bootstrapped .gsd without milestones returns initialized", () => { +test("detectHealthWidgetProjectState: bootstrapped .gsd without milestones returns initialized", (t) => { const dir = makeTempDir("initialized"); - try { - mkdirSync(join(dir, ".gsd"), { recursive: true }); - assert.equal(detectHealthWidgetProjectState(dir), "initialized"); - } finally { - cleanup(dir); - } + t.after(() => { cleanup(dir); }); + + mkdirSync(join(dir, ".gsd"), { recursive: true }); + assert.equal(detectHealthWidgetProjectState(dir), "initialized"); }); -test("detectHealthWidgetProjectState: milestone without metrics returns active", () => { +test("detectHealthWidgetProjectState: milestone without metrics returns active", (t) => { const dir = makeTempDir("active"); - try { - mkdirSync(join(dir, ".gsd", "milestones", "M001"), { recursive: true }); - assert.equal(detectHealthWidgetProjectState(dir), "active"); - } finally { - cleanup(dir); - } + t.after(() => { cleanup(dir); }); + + mkdirSync(join(dir, ".gsd", "milestones", "M001"), { recursive: true }); + assert.equal(detectHealthWidgetProjectState(dir), "active"); }); -test("buildHealthLines: none state shows onboarding copy", () => { +test("buildHealthLines: none state shows onboarding copy", (t) => { assert.deepEqual(buildHealthLines(activeData({ projectState: "none" })), [ " GSD No project loaded — run /gsd to start", ]); }); -test("buildHealthLines: initialized state shows continue setup copy", () => { +test("buildHealthLines: initialized state shows continue setup copy", (t) => { assert.deepEqual(buildHealthLines(activeData({ projectState: "initialized" })), [ " GSD Project initialized — run /gsd to continue setup", ]); }); -test("buildHealthLines: active state with ledger-driven spend shows spent summary", () => { +test("buildHealthLines: active state with ledger-driven spend shows spent summary", (t) => { const lines = buildHealthLines(activeData({ budgetSpent: 0.42 })); assert.equal(lines.length, 1); assert.match(lines[0]!, /● System OK/); assert.match(lines[0]!, /Spent: 42\.0¢/); }); -test("buildHealthLines: active state with budget ceiling shows percent summary", () => { +test("buildHealthLines: active state with budget ceiling shows percent summary", (t) => { const lines = buildHealthLines(activeData({ budgetSpent: 2.5, budgetCeiling: 10 })); assert.equal(lines.length, 1); assert.match(lines[0]!, /Budget: \$2\.50\/\$10\.00 \(25%\)/); }); -test("buildHealthLines: active state with issues reports issue summary", () => { +test("buildHealthLines: active state with issues reports issue summary", (t) => { const lines = buildHealthLines(activeData({ providerIssue: "✗ OpenAI key missing", environmentErrorCount: 1, @@ -104,17 +98,15 @@ test("buildHealthLines: active state with issues reports issue summary", () => { assert.match(lines[0]!, /Env: 1 error/); }); -test("detectHealthWidgetProjectState: metrics file alone does not imply project", () => { +test("detectHealthWidgetProjectState: metrics file alone does not imply project", (t) => { const dir = makeTempDir("metrics-only"); - try { - mkdirSync(join(dir, ".gsd"), { recursive: true }); - writeFileSync( - join(dir, ".gsd", "metrics.json"), - JSON.stringify({ version: 1, projectStartedAt: Date.now(), units: [] }), - "utf-8", - ); - assert.equal(detectHealthWidgetProjectState(dir), "initialized"); - } finally { - cleanup(dir); - } + t.after(() => { cleanup(dir); }); + + mkdirSync(join(dir, ".gsd"), { recursive: true }); + writeFileSync( + join(dir, ".gsd", "metrics.json"), + JSON.stringify({ version: 1, projectStartedAt: Date.now(), units: [] }), + "utf-8", + ); + assert.equal(detectHealthWidgetProjectState(dir), "initialized"); }); diff --git a/src/resources/extensions/gsd/tests/init-wizard.test.ts b/src/resources/extensions/gsd/tests/init-wizard.test.ts index cf10d2754..c3350a5a4 100644 --- a/src/resources/extensions/gsd/tests/init-wizard.test.ts +++ b/src/resources/extensions/gsd/tests/init-wizard.test.ts @@ -36,19 +36,17 @@ function cleanup(dir: string): void { // ─── Detection Integration Tests ──────────────────────────────────────────────── -test("init-wizard: clean folder detected as state=none", () => { +test("init-wizard: clean folder detected as state=none", (t) => { const dir = makeTempDir("clean"); - try { - const detection = detectProjectState(dir); - assert.equal(detection.state, "none"); - assert.equal(detection.v1, undefined); - assert.equal(detection.v2, undefined); - } finally { - cleanup(dir); - } + t.after(() => { cleanup(dir); }); + + const detection = detectProjectState(dir); + assert.equal(detection.state, "none"); + assert.equal(detection.v1, undefined); + assert.equal(detection.v2, undefined); }); -test("init-wizard: v1 .planning/ triggers v1-planning state", () => { +test("init-wizard: v1 .planning/ triggers v1-planning state", (t) => { const dir = makeTempDir("v1"); try { mkdirSync(join(dir, ".planning", "phases", "01"), { recursive: true }); @@ -65,7 +63,7 @@ test("init-wizard: v1 .planning/ triggers v1-planning state", () => { } }); -test("init-wizard: existing .gsd/ with milestones skips init", () => { +test("init-wizard: existing .gsd/ with milestones skips init", (t) => { const dir = makeTempDir("existing"); try { mkdirSync(join(dir, ".gsd", "milestones", "M001"), { recursive: true }); @@ -80,7 +78,7 @@ test("init-wizard: existing .gsd/ with milestones skips init", () => { } }); -test("init-wizard: empty .gsd/ (no milestones) returns v2-gsd-empty", () => { +test("init-wizard: empty .gsd/ (no milestones) returns v2-gsd-empty", (t) => { const dir = makeTempDir("empty-gsd"); try { mkdirSync(join(dir, ".gsd", "milestones"), { recursive: true }); @@ -94,7 +92,7 @@ test("init-wizard: empty .gsd/ (no milestones) returns v2-gsd-empty", () => { } }); -test("init-wizard: project signals populate from Node.js project", () => { +test("init-wizard: project signals populate from Node.js project", (t) => { const dir = makeTempDir("node-project"); try { writeFileSync( @@ -121,7 +119,7 @@ test("init-wizard: project signals populate from Node.js project", () => { } }); -test("init-wizard: v2 .gsd/ preferences detected", () => { +test("init-wizard: v2 .gsd/ preferences detected", (t) => { const dir = makeTempDir("prefs-detect"); try { mkdirSync(join(dir, ".gsd", "milestones"), { recursive: true }); @@ -135,7 +133,7 @@ test("init-wizard: v2 .gsd/ preferences detected", () => { } }); -test("init-wizard: v2 uppercase PREFERENCES.md also detected", () => { +test("init-wizard: v2 uppercase PREFERENCES.md also detected", (t) => { const dir = makeTempDir("prefs-upper"); try { mkdirSync(join(dir, ".gsd", "milestones"), { recursive: true }); @@ -149,7 +147,7 @@ test("init-wizard: v2 uppercase PREFERENCES.md also detected", () => { } }); -test("init-wizard: CONTEXT.md detected in v2", () => { +test("init-wizard: CONTEXT.md detected in v2", (t) => { const dir = makeTempDir("context"); try { mkdirSync(join(dir, ".gsd", "milestones"), { recursive: true }); @@ -163,7 +161,7 @@ test("init-wizard: CONTEXT.md detected in v2", () => { } }); -test("init-wizard: multiple project files detected together", () => { +test("init-wizard: multiple project files detected together", (t) => { const dir = makeTempDir("multi-files"); try { writeFileSync(join(dir, "package.json"), JSON.stringify({ name: "test" }), "utf-8"); @@ -180,7 +178,7 @@ test("init-wizard: multiple project files detected together", () => { } }); -test("init-wizard: v1 with both .planning/ and .gsd/ prioritizes v2", () => { +test("init-wizard: v1 with both .planning/ and .gsd/ prioritizes v2", (t) => { const dir = makeTempDir("both-v1-v2"); try { mkdirSync(join(dir, ".planning", "phases"), { recursive: true }); diff --git a/src/resources/extensions/gsd/tests/integration-proof.test.ts b/src/resources/extensions/gsd/tests/integration-proof.test.ts index 4350156e5..0255abc0b 100644 --- a/src/resources/extensions/gsd/tests/integration-proof.test.ts +++ b/src/resources/extensions/gsd/tests/integration-proof.test.ts @@ -278,8 +278,12 @@ test("full lifecycle: migration through completion through doctor", async (t) => const base = createRealisticFixture(); const dbPath = join(base, ".gsd", "gsd.db"); - try { - // ── (a) Open file-backed DB ────────────────────────────────────── + t.after(() => { + closeDatabase(); + rmSync(base, { recursive: true, force: true }); + }); + + // ── (a) Open file-backed DB ────────────────────────────────────── const opened = openDatabase(dbPath); assert.equal(opened, true, "DB should open successfully"); assert.equal(isDbAvailable(), true, "DB should be available"); @@ -414,10 +418,6 @@ test("full lifecycle: migration through completion through doctor", async (t) => const rogues = detectRogueFileWrites("execute-task", "M001/S01/T99", base); assert.ok(rogues.length > 0, "Should detect rogue file write for T99"); assert.equal(rogues[0].unitId, "M001/S01/T99", "Rogue detection should identify the correct unit"); - } finally { - closeDatabase(); - rmSync(base, { recursive: true, force: true }); - } }); // ═══════════════════════════════════════════════════════════════════════════ @@ -429,8 +429,12 @@ test("recovery: DB loss → migrateFromMarkdown restores state, stale render det const base = createRealisticFixture(); const dbPath = join(base, ".gsd", "gsd.db"); - try { - // Set up a completed state first + t.after(() => { + closeDatabase(); + rmSync(base, { recursive: true, force: true }); + }); + + // Set up a completed state first openDatabase(dbPath); migrateHierarchyToDb(base); await handleCompleteTask(makeCompleteTaskParams("T01"), base); @@ -503,10 +507,6 @@ test("recovery: DB loss → migrateFromMarkdown restores state, stale render det const t2Recovered = getTask("M001", "S01", "T02"); assert.ok(t2Recovered, "T02 should exist after recovery"); assert.equal(t2Recovered!.status, "complete", "T02 should be complete after recovery"); - } finally { - closeDatabase(); - rmSync(base, { recursive: true, force: true }); - } }); // ═══════════════════════════════════════════════════════════════════════════ @@ -517,8 +517,12 @@ test("undo/reset: undo task and reset slice revert DB + markdown", async (t) => const base = createRealisticFixture(); const dbPath = join(base, ".gsd", "gsd.db"); - try { - // Build up completed state + t.after(() => { + closeDatabase(); + rmSync(base, { recursive: true, force: true }); + }); + + // Build up completed state openDatabase(dbPath); migrateHierarchyToDb(base); await handleCompleteTask(makeCompleteTaskParams("T01"), base); @@ -636,8 +640,4 @@ test("undo/reset: undo task and reset slice revert DB + markdown", async (t) => resetNotifs.some(n => n.level === "success"), "Reset should produce success notification", ); - } finally { - closeDatabase(); - rmSync(base, { recursive: true, force: true }); - } });