refactor(test): replace try/finally with t.after() in gsd/tests (e-i) (#2396)

This commit is contained in:
Tom Boucher 2026-03-24 23:31:42 -04:00 committed by GitHub
parent 2223298f76
commit c237c56016
8 changed files with 195 additions and 225 deletions

View file

@ -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<string, { description?: string; handler: (args: string, ctx: any) => Promise<void> }>();
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");

View file

@ -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);
});

View file

@ -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

View file

@ -56,7 +56,7 @@ function makeStep(overrides: Partial<GraphStep> & { 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",

View file

@ -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([{

View file

@ -39,61 +39,55 @@ function activeData(overrides: Partial<HealthWidgetData> = {}): 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");
});

View file

@ -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 });

View file

@ -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 });
}
});