test: replace try/finally cleanup with beforeEach/afterEach hooks in 6 test files (#2234)
Move temp directory creation and cleanup from try/finally blocks inside test bodies into beforeEach/afterEach hooks on describe blocks. For tests that also save/restore env vars (manifest-status), those are handled in the hooks as well. Tests that don't need cleanup (pure assertions, no temp dirs) remain as standalone test() calls. Closes #2064 Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
d63d11b86a
commit
6c876db69a
6 changed files with 473 additions and 708 deletions
|
|
@ -4,7 +4,7 @@
|
|||
* - activity-log-save.test.ts (caching, dedup, collision recovery)
|
||||
*/
|
||||
|
||||
import test from "node:test";
|
||||
import { describe, test, beforeEach, afterEach } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { existsSync, mkdtempSync, mkdirSync, readdirSync, realpathSync, rmSync, utimesSync, writeFileSync, readFileSync } from "node:fs";
|
||||
import { join, dirname } from "node:path";
|
||||
|
|
@ -48,9 +48,12 @@ function createCtx(entries: unknown[]) {
|
|||
|
||||
// ── Pruning ──────────────────────────────────────────────────────────────────
|
||||
|
||||
test("pruneActivityLogs deletes old files, keeps recent and highest-seq", () => {
|
||||
const dir = createTmpDir();
|
||||
try {
|
||||
describe("pruneActivityLogs", () => {
|
||||
let dir: string;
|
||||
beforeEach(() => { dir = createTmpDir(); });
|
||||
afterEach(() => { rmSync(dir, { recursive: true, force: true }); });
|
||||
|
||||
test("deletes old files, keeps recent and highest-seq", () => {
|
||||
const f001 = writeActivityFile(dir, "001", "execute-task-M001-S01-T01");
|
||||
writeActivityFile(dir, "002", "execute-task-M001-S01-T02");
|
||||
writeActivityFile(dir, "003", "execute-task-M001-S01-T03");
|
||||
|
|
@ -61,14 +64,9 @@ test("pruneActivityLogs deletes old files, keeps recent and highest-seq", () =>
|
|||
assert.ok(!remaining.includes("001-execute-task-M001-S01-T01.jsonl"));
|
||||
assert.ok(remaining.includes("002-execute-task-M001-S01-T02.jsonl"));
|
||||
assert.ok(remaining.includes("003-execute-task-M001-S01-T03.jsonl"));
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test("pruneActivityLogs preserves highest-seq even when all files are old", () => {
|
||||
const dir = createTmpDir();
|
||||
try {
|
||||
test("preserves highest-seq even when all files are old", () => {
|
||||
const f001 = writeActivityFile(dir, "001", "t1");
|
||||
const f002 = writeActivityFile(dir, "002", "t2");
|
||||
const f003 = writeActivityFile(dir, "003", "t3");
|
||||
|
|
@ -78,14 +76,9 @@ test("pruneActivityLogs preserves highest-seq even when all files are old", () =
|
|||
const remaining = listFiles(dir);
|
||||
assert.equal(remaining.length, 1);
|
||||
assert.ok(remaining[0].startsWith("003-"));
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test("pruneActivityLogs with retentionDays=0 keeps only highest-seq", () => {
|
||||
const dir = createTmpDir();
|
||||
try {
|
||||
test("with retentionDays=0 keeps only highest-seq", () => {
|
||||
writeActivityFile(dir, "001", "t1");
|
||||
writeActivityFile(dir, "002", "t2");
|
||||
writeActivityFile(dir, "003", "t3");
|
||||
|
|
@ -94,51 +87,31 @@ test("pruneActivityLogs with retentionDays=0 keeps only highest-seq", () => {
|
|||
const remaining = listFiles(dir);
|
||||
assert.equal(remaining.length, 1);
|
||||
assert.ok(remaining[0].startsWith("003-"));
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test("pruneActivityLogs no-op when all files are recent", () => {
|
||||
const dir = createTmpDir();
|
||||
try {
|
||||
test("no-op when all files are recent", () => {
|
||||
writeActivityFile(dir, "001", "t1");
|
||||
writeActivityFile(dir, "002", "t2");
|
||||
writeActivityFile(dir, "003", "t3");
|
||||
|
||||
pruneActivityLogs(dir, 30);
|
||||
assert.equal(listFiles(dir).length, 3);
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test("pruneActivityLogs handles empty directory", () => {
|
||||
const dir = createTmpDir();
|
||||
try {
|
||||
test("handles empty directory", () => {
|
||||
assert.doesNotThrow(() => pruneActivityLogs(dir, 30));
|
||||
assert.equal(readdirSync(dir).length, 0);
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test("pruneActivityLogs preserves single old file (it is highest-seq)", () => {
|
||||
const dir = createTmpDir();
|
||||
try {
|
||||
test("preserves single old file (it is highest-seq)", () => {
|
||||
const f = writeActivityFile(dir, "001", "t1");
|
||||
backdateFile(f, 100);
|
||||
|
||||
pruneActivityLogs(dir, 30);
|
||||
assert.equal(listFiles(dir).length, 1);
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test("pruneActivityLogs ignores non-matching filenames", () => {
|
||||
const dir = createTmpDir();
|
||||
try {
|
||||
test("ignores non-matching filenames", () => {
|
||||
const f001 = writeActivityFile(dir, "001", "t1");
|
||||
writeFileSync(join(dir, "notes.txt"), "some notes\n", "utf-8");
|
||||
backdateFile(f001, 40);
|
||||
|
|
@ -148,16 +121,17 @@ test("pruneActivityLogs ignores non-matching filenames", () => {
|
|||
assert.ok(remaining.includes("notes.txt"));
|
||||
// 001 is the only seq file, so it's highest-seq and survives
|
||||
assert.ok(remaining.includes("001-t1.jsonl"));
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ── Save: caching, dedup, collision recovery ─────────────────────────────────
|
||||
|
||||
test("saveActivityLog caches sequence instead of rescanning", () => {
|
||||
const baseDir = createTmpDir();
|
||||
try {
|
||||
describe("saveActivityLog", () => {
|
||||
let baseDir: string;
|
||||
beforeEach(() => { baseDir = createTmpDir(); });
|
||||
afterEach(() => { rmSync(baseDir, { recursive: true, force: true }); });
|
||||
|
||||
test("caches sequence instead of rescanning", () => {
|
||||
saveActivityLog(createCtx([{ kind: "first", n: 1 }]) as any, baseDir, "execute-task", "M001/S01/T01");
|
||||
writeFileSync(join(activityDir(baseDir), "999-external.jsonl"), '{"x":1}\n', "utf-8");
|
||||
saveActivityLog(createCtx([{ kind: "second", n: 2 }]) as any, baseDir, "execute-task", "M001/S01/T02");
|
||||
|
|
@ -166,14 +140,9 @@ test("saveActivityLog caches sequence instead of rescanning", () => {
|
|||
assert.ok(files.includes("001-execute-task-M001-S01-T01.jsonl"));
|
||||
assert.ok(files.includes("002-execute-task-M001-S01-T02.jsonl"));
|
||||
assert.ok(!files.some(f => f.startsWith("1000-")));
|
||||
} finally {
|
||||
rmSync(baseDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test("saveActivityLog deduplicates identical snapshots for same unit", () => {
|
||||
const baseDir = createTmpDir();
|
||||
try {
|
||||
test("deduplicates identical snapshots for same unit", () => {
|
||||
const ctx = createCtx([{ role: "assistant", content: "same" }]);
|
||||
saveActivityLog(ctx as any, baseDir, "plan-slice", "M002/S01");
|
||||
saveActivityLog(ctx as any, baseDir, "plan-slice", "M002/S01");
|
||||
|
|
@ -184,14 +153,9 @@ test("saveActivityLog deduplicates identical snapshots for same unit", () => {
|
|||
saveActivityLog(createCtx([{ role: "assistant", content: "changed" }]) as any, baseDir, "plan-slice", "M002/S01");
|
||||
files = listFiles(activityDir(baseDir));
|
||||
assert.equal(files.length, 2);
|
||||
} finally {
|
||||
rmSync(baseDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test("saveActivityLog recovers on sequence collision", () => {
|
||||
const baseDir = createTmpDir();
|
||||
try {
|
||||
test("recovers on sequence collision", () => {
|
||||
saveActivityLog(createCtx([{ turn: 1 }]) as any, baseDir, "execute-task", "M003/S02/T01");
|
||||
writeFileSync(join(activityDir(baseDir), "002-execute-task-M003-S02-T02.jsonl"), '{"collision":true}\n', "utf-8");
|
||||
saveActivityLog(createCtx([{ turn: 2 }]) as any, baseDir, "execute-task", "M003/S02/T02");
|
||||
|
|
@ -199,9 +163,7 @@ test("saveActivityLog recovers on sequence collision", () => {
|
|||
const files = listFiles(activityDir(baseDir));
|
||||
assert.ok(files.includes("002-execute-task-M003-S02-T02.jsonl"));
|
||||
assert.ok(files.includes("003-execute-task-M003-S02-T02.jsonl"));
|
||||
} finally {
|
||||
rmSync(baseDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ── Prompt text assertion ────────────────────────────────────────────────────
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import test from "node:test";
|
||||
import { describe, test, beforeEach, afterEach } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import {
|
||||
mkdirSync,
|
||||
|
|
@ -46,9 +46,12 @@ function makeEntry(overrides: Partial<JournalEntry> = {}): JournalEntry {
|
|||
|
||||
// ─── emitJournalEvent ─────────────────────────────────────────────────────────
|
||||
|
||||
test("emitJournalEvent creates journal directory and JSONL file", () => {
|
||||
const base = makeTmpBase();
|
||||
try {
|
||||
describe("emitJournalEvent", () => {
|
||||
let base: string;
|
||||
beforeEach(() => { base = makeTmpBase(); });
|
||||
afterEach(() => { cleanup(base); });
|
||||
|
||||
test("creates journal directory and JSONL file", () => {
|
||||
const entry = makeEntry();
|
||||
emitJournalEvent(base, entry);
|
||||
|
||||
|
|
@ -61,14 +64,9 @@ test("emitJournalEvent creates journal directory and JSONL file", () => {
|
|||
assert.equal(parsed.flowId, entry.flowId);
|
||||
assert.equal(parsed.seq, entry.seq);
|
||||
assert.equal(parsed.eventType, entry.eventType);
|
||||
} finally {
|
||||
cleanup(base);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test("emitJournalEvent appends multiple lines to the same file", () => {
|
||||
const base = makeTmpBase();
|
||||
try {
|
||||
test("appends multiple lines to the same file", () => {
|
||||
emitJournalEvent(base, makeEntry({ seq: 0 }));
|
||||
emitJournalEvent(base, makeEntry({ seq: 1, eventType: "dispatch-match" }));
|
||||
emitJournalEvent(base, makeEntry({ seq: 2, eventType: "unit-start" }));
|
||||
|
|
@ -82,26 +80,9 @@ test("emitJournalEvent appends multiple lines to the same file", () => {
|
|||
assert.equal(parsed[1].seq, 1);
|
||||
assert.equal(parsed[2].seq, 2);
|
||||
assert.equal(parsed[1].eventType, "dispatch-match");
|
||||
} finally {
|
||||
cleanup(base);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test("emitJournalEvent auto-creates nonexistent parent directory", () => {
|
||||
const base = join(tmpdir(), `gsd-journal-test-${randomUUID()}`);
|
||||
// Don't create .gsd/ — emitJournalEvent should handle it via mkdirSync recursive
|
||||
try {
|
||||
emitJournalEvent(base, makeEntry());
|
||||
const filePath = join(base, ".gsd", "journal", "2025-03-21.jsonl");
|
||||
assert.ok(existsSync(filePath), "File should exist even when parent dirs did not");
|
||||
} finally {
|
||||
cleanup(base);
|
||||
}
|
||||
});
|
||||
|
||||
test("emitJournalEvent preserves optional fields (rule, causedBy, data)", () => {
|
||||
const base = makeTmpBase();
|
||||
try {
|
||||
test("preserves optional fields (rule, causedBy, data)", () => {
|
||||
const entry = makeEntry({
|
||||
rule: "my-dispatch-rule",
|
||||
causedBy: { flowId: "flow-prior", seq: 3 },
|
||||
|
|
@ -115,9 +96,42 @@ test("emitJournalEvent preserves optional fields (rule, causedBy, data)", () =>
|
|||
assert.deepEqual(parsed.causedBy, { flowId: "flow-prior", seq: 3 });
|
||||
assert.equal(parsed.data.unitId, "M001/S01/T01");
|
||||
assert.equal(parsed.data.status, "ok");
|
||||
} finally {
|
||||
cleanup(base);
|
||||
}
|
||||
});
|
||||
|
||||
test("silently catches read-only directory errors", () => {
|
||||
const journalDir = join(base, ".gsd", "journal");
|
||||
mkdirSync(journalDir, { recursive: true });
|
||||
|
||||
// Make the journal directory read-only
|
||||
chmodSync(journalDir, 0o444);
|
||||
|
||||
// Should not throw
|
||||
assert.doesNotThrow(() => {
|
||||
emitJournalEvent(base, makeEntry());
|
||||
});
|
||||
|
||||
// Restore permissions for cleanup
|
||||
try {
|
||||
chmodSync(journalDir, 0o755);
|
||||
} catch {
|
||||
/* */
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("emitJournalEvent — auto-creates parent directory", () => {
|
||||
let base: string;
|
||||
beforeEach(() => {
|
||||
base = join(tmpdir(), `gsd-journal-test-${randomUUID()}`);
|
||||
// Don't create .gsd/ — emitJournalEvent should handle it via mkdirSync recursive
|
||||
});
|
||||
afterEach(() => { cleanup(base); });
|
||||
|
||||
test("auto-creates nonexistent parent directory", () => {
|
||||
emitJournalEvent(base, makeEntry());
|
||||
const filePath = join(base, ".gsd", "journal", "2025-03-21.jsonl");
|
||||
assert.ok(existsSync(filePath), "File should exist even when parent dirs did not");
|
||||
});
|
||||
});
|
||||
|
||||
test("emitJournalEvent silently catches write errors (no throw)", () => {
|
||||
|
|
@ -127,35 +141,14 @@ test("emitJournalEvent silently catches write errors (no throw)", () => {
|
|||
});
|
||||
});
|
||||
|
||||
test("emitJournalEvent silently catches read-only directory errors", () => {
|
||||
const base = makeTmpBase();
|
||||
const journalDir = join(base, ".gsd", "journal");
|
||||
mkdirSync(journalDir, { recursive: true });
|
||||
|
||||
try {
|
||||
// Make the journal directory read-only
|
||||
chmodSync(journalDir, 0o444);
|
||||
|
||||
// Should not throw
|
||||
assert.doesNotThrow(() => {
|
||||
emitJournalEvent(base, makeEntry());
|
||||
});
|
||||
} finally {
|
||||
// Restore permissions for cleanup
|
||||
try {
|
||||
chmodSync(journalDir, 0o755);
|
||||
} catch {
|
||||
/* */
|
||||
}
|
||||
cleanup(base);
|
||||
}
|
||||
});
|
||||
|
||||
// ─── Daily Rotation ───────────────────────────────────────────────────────────
|
||||
|
||||
test("daily rotation: events with different dates go to different files", () => {
|
||||
const base = makeTmpBase();
|
||||
try {
|
||||
describe("daily rotation", () => {
|
||||
let base: string;
|
||||
beforeEach(() => { base = makeTmpBase(); });
|
||||
afterEach(() => { cleanup(base); });
|
||||
|
||||
test("events with different dates go to different files", () => {
|
||||
emitJournalEvent(base, makeEntry({ ts: "2025-03-20T23:59:59.000Z" }));
|
||||
emitJournalEvent(base, makeEntry({ ts: "2025-03-21T00:00:01.000Z" }));
|
||||
emitJournalEvent(base, makeEntry({ ts: "2025-03-22T12:00:00.000Z" }));
|
||||
|
|
@ -172,16 +165,17 @@ test("daily rotation: events with different dates go to different files", () =>
|
|||
.split("\n");
|
||||
assert.equal(lines.length, 1, `${date}.jsonl should have 1 line`);
|
||||
}
|
||||
} finally {
|
||||
cleanup(base);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ─── queryJournal ─────────────────────────────────────────────────────────────
|
||||
|
||||
test("queryJournal returns all entries when no filters provided", () => {
|
||||
const base = makeTmpBase();
|
||||
try {
|
||||
describe("queryJournal", () => {
|
||||
let base: string;
|
||||
beforeEach(() => { base = makeTmpBase(); });
|
||||
afterEach(() => { cleanup(base); });
|
||||
|
||||
test("returns all entries when no filters provided", () => {
|
||||
emitJournalEvent(base, makeEntry({ seq: 0 }));
|
||||
emitJournalEvent(base, makeEntry({ seq: 1, eventType: "dispatch-match" }));
|
||||
|
||||
|
|
@ -189,14 +183,9 @@ test("queryJournal returns all entries when no filters provided", () => {
|
|||
assert.equal(results.length, 2);
|
||||
assert.equal(results[0].seq, 0);
|
||||
assert.equal(results[1].seq, 1);
|
||||
} finally {
|
||||
cleanup(base);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test("queryJournal filters by flowId", () => {
|
||||
const base = makeTmpBase();
|
||||
try {
|
||||
test("filters by flowId", () => {
|
||||
emitJournalEvent(base, makeEntry({ flowId: "flow-aaa", seq: 0 }));
|
||||
emitJournalEvent(base, makeEntry({ flowId: "flow-bbb", seq: 1 }));
|
||||
emitJournalEvent(base, makeEntry({ flowId: "flow-aaa", seq: 2 }));
|
||||
|
|
@ -204,14 +193,9 @@ test("queryJournal filters by flowId", () => {
|
|||
const results = queryJournal(base, { flowId: "flow-aaa" });
|
||||
assert.equal(results.length, 2);
|
||||
assert.ok(results.every(e => e.flowId === "flow-aaa"));
|
||||
} finally {
|
||||
cleanup(base);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test("queryJournal filters by eventType", () => {
|
||||
const base = makeTmpBase();
|
||||
try {
|
||||
test("filters by eventType", () => {
|
||||
emitJournalEvent(base, makeEntry({ eventType: "iteration-start", seq: 0 }));
|
||||
emitJournalEvent(base, makeEntry({ eventType: "dispatch-match", seq: 1 }));
|
||||
emitJournalEvent(base, makeEntry({ eventType: "unit-start", seq: 2 }));
|
||||
|
|
@ -220,14 +204,9 @@ test("queryJournal filters by eventType", () => {
|
|||
const results = queryJournal(base, { eventType: "dispatch-match" });
|
||||
assert.equal(results.length, 2);
|
||||
assert.ok(results.every(e => e.eventType === "dispatch-match"));
|
||||
} finally {
|
||||
cleanup(base);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test("queryJournal filters by unitId (from data.unitId)", () => {
|
||||
const base = makeTmpBase();
|
||||
try {
|
||||
test("filters by unitId (from data.unitId)", () => {
|
||||
emitJournalEvent(
|
||||
base,
|
||||
makeEntry({ seq: 0, data: { unitId: "M001/S01/T01" } }),
|
||||
|
|
@ -249,14 +228,9 @@ test("queryJournal filters by unitId (from data.unitId)", () => {
|
|||
e => (e.data as Record<string, unknown>)?.unitId === "M001/S01/T01",
|
||||
),
|
||||
);
|
||||
} finally {
|
||||
cleanup(base);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test("queryJournal filters by time range (after/before)", () => {
|
||||
const base = makeTmpBase();
|
||||
try {
|
||||
test("filters by time range (after/before)", () => {
|
||||
emitJournalEvent(base, makeEntry({ ts: "2025-03-20T08:00:00.000Z", seq: 0 }));
|
||||
emitJournalEvent(base, makeEntry({ ts: "2025-03-21T10:00:00.000Z", seq: 1 }));
|
||||
emitJournalEvent(base, makeEntry({ ts: "2025-03-21T15:00:00.000Z", seq: 2 }));
|
||||
|
|
@ -276,14 +250,9 @@ test("queryJournal filters by time range (after/before)", () => {
|
|||
before: "2025-03-21T23:59:59.000Z",
|
||||
});
|
||||
assert.equal(rangeResults.length, 2, "2 entries within 2025-03-21");
|
||||
} finally {
|
||||
cleanup(base);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test("queryJournal combines multiple filters", () => {
|
||||
const base = makeTmpBase();
|
||||
try {
|
||||
test("combines multiple filters", () => {
|
||||
emitJournalEvent(
|
||||
base,
|
||||
makeEntry({ flowId: "flow-aaa", eventType: "unit-start", seq: 0 }),
|
||||
|
|
@ -304,25 +273,9 @@ test("queryJournal combines multiple filters", () => {
|
|||
assert.equal(results.length, 1);
|
||||
assert.equal(results[0].flowId, "flow-aaa");
|
||||
assert.equal(results[0].eventType, "unit-start");
|
||||
} finally {
|
||||
cleanup(base);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test("queryJournal on nonexistent directory returns empty array", () => {
|
||||
const base = join(tmpdir(), `gsd-journal-test-${randomUUID()}`);
|
||||
// Don't create anything
|
||||
try {
|
||||
const results = queryJournal(base);
|
||||
assert.deepEqual(results, []);
|
||||
} finally {
|
||||
cleanup(base);
|
||||
}
|
||||
});
|
||||
|
||||
test("queryJournal skips malformed JSON lines gracefully", () => {
|
||||
const base = makeTmpBase();
|
||||
try {
|
||||
test("skips malformed JSON lines gracefully", () => {
|
||||
const journalDir = join(base, ".gsd", "journal");
|
||||
mkdirSync(journalDir, { recursive: true });
|
||||
|
||||
|
|
@ -335,14 +288,9 @@ test("queryJournal skips malformed JSON lines gracefully", () => {
|
|||
assert.equal(results.length, 2, "Should skip the malformed line");
|
||||
assert.equal(results[0].seq, 0);
|
||||
assert.equal(results[1].seq, 1);
|
||||
} finally {
|
||||
cleanup(base);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test("queryJournal reads across multiple daily files", () => {
|
||||
const base = makeTmpBase();
|
||||
try {
|
||||
test("reads across multiple daily files", () => {
|
||||
emitJournalEvent(base, makeEntry({ ts: "2025-03-20T12:00:00.000Z", seq: 0 }));
|
||||
emitJournalEvent(base, makeEntry({ ts: "2025-03-21T12:00:00.000Z", seq: 1 }));
|
||||
emitJournalEvent(base, makeEntry({ ts: "2025-03-22T12:00:00.000Z", seq: 2 }));
|
||||
|
|
@ -353,14 +301,9 @@ test("queryJournal reads across multiple daily files", () => {
|
|||
assert.equal(results[0].ts, "2025-03-20T12:00:00.000Z");
|
||||
assert.equal(results[1].ts, "2025-03-21T12:00:00.000Z");
|
||||
assert.equal(results[2].ts, "2025-03-22T12:00:00.000Z");
|
||||
} finally {
|
||||
cleanup(base);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test("queryJournal filters by rule", () => {
|
||||
const base = makeTmpBase();
|
||||
try {
|
||||
test("filters by rule", () => {
|
||||
emitJournalEvent(
|
||||
base,
|
||||
makeEntry({ seq: 0, eventType: "dispatch-match", rule: "dispatch-task" }),
|
||||
|
|
@ -380,7 +323,19 @@ test("queryJournal filters by rule", () => {
|
|||
results.every(e => e.rule === "dispatch-task"),
|
||||
"All results should have rule === 'dispatch-task'",
|
||||
);
|
||||
} finally {
|
||||
cleanup(base);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("queryJournal — nonexistent directory", () => {
|
||||
let base: string;
|
||||
beforeEach(() => {
|
||||
base = join(tmpdir(), `gsd-journal-test-${randomUUID()}`);
|
||||
// Don't create anything
|
||||
});
|
||||
afterEach(() => { cleanup(base); });
|
||||
|
||||
test("on nonexistent directory returns empty array", () => {
|
||||
const results = queryJournal(base);
|
||||
assert.deepEqual(results, []);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
* Uses temp directories with real .gsd/milestones/M001/ structure.
|
||||
*/
|
||||
|
||||
import test from 'node:test';
|
||||
import { describe, test, beforeEach, afterEach } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { mkdirSync, writeFileSync, rmSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
|
|
@ -30,12 +30,21 @@ function writeManifest(base: string, content: string): void {
|
|||
|
||||
// ─── Mixed statuses ──────────────────────────────────────────────────────────
|
||||
|
||||
test('getManifestStatus: mixed statuses — categorizes entries correctly', async () => {
|
||||
const tmp = makeTempDir('manifest-mixed');
|
||||
const savedVal = process.env.GSD_TEST_EXISTING_KEY_001;
|
||||
try {
|
||||
describe('getManifestStatus: mixed statuses', () => {
|
||||
let tmp: string;
|
||||
let savedVal: string | undefined;
|
||||
beforeEach(() => {
|
||||
tmp = makeTempDir('manifest-mixed');
|
||||
savedVal = process.env.GSD_TEST_EXISTING_KEY_001;
|
||||
process.env.GSD_TEST_EXISTING_KEY_001 = 'some-value';
|
||||
});
|
||||
afterEach(() => {
|
||||
delete process.env.GSD_TEST_EXISTING_KEY_001;
|
||||
if (savedVal !== undefined) process.env.GSD_TEST_EXISTING_KEY_001 = savedVal;
|
||||
rmSync(tmp, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test('categorizes entries correctly', async () => {
|
||||
writeManifest(tmp, `# Secrets Manifest
|
||||
|
||||
**Milestone:** M001
|
||||
|
|
@ -80,18 +89,17 @@ test('getManifestStatus: mixed statuses — categorizes entries correctly', asyn
|
|||
assert.deepStrictEqual(result!.collected, ['COLLECTED_KEY']);
|
||||
assert.deepStrictEqual(result!.skipped, ['SKIPPED_KEY']);
|
||||
assert.deepStrictEqual(result!.existing, ['GSD_TEST_EXISTING_KEY_001']);
|
||||
} finally {
|
||||
delete process.env.GSD_TEST_EXISTING_KEY_001;
|
||||
if (savedVal !== undefined) process.env.GSD_TEST_EXISTING_KEY_001 = savedVal;
|
||||
rmSync(tmp, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ─── All pending ─────────────────────────────────────────────────────────────
|
||||
|
||||
test('getManifestStatus: all pending — 3 pending entries, none in env', async () => {
|
||||
const tmp = makeTempDir('manifest-pending');
|
||||
try {
|
||||
describe('getManifestStatus: simple temp dir tests', () => {
|
||||
let tmp: string;
|
||||
beforeEach(() => { tmp = makeTempDir('manifest-test'); });
|
||||
afterEach(() => { rmSync(tmp, { recursive: true, force: true }); });
|
||||
|
||||
test('all pending — 3 pending entries, none in env', async () => {
|
||||
// Ensure none of these are in process.env
|
||||
delete process.env.PEND_A;
|
||||
delete process.env.PEND_B;
|
||||
|
|
@ -133,16 +141,11 @@ test('getManifestStatus: all pending — 3 pending entries, none in env', async
|
|||
assert.deepStrictEqual(result!.collected, []);
|
||||
assert.deepStrictEqual(result!.skipped, []);
|
||||
assert.deepStrictEqual(result!.existing, []);
|
||||
} finally {
|
||||
rmSync(tmp, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ─── All collected ───────────────────────────────────────────────────────────
|
||||
// ─── All collected ───────────────────────────────────────────────────────────
|
||||
|
||||
test('getManifestStatus: all collected — 2 collected entries, none in env', async () => {
|
||||
const tmp = makeTempDir('manifest-collected');
|
||||
try {
|
||||
test('all collected — 2 collected entries, none in env', async () => {
|
||||
delete process.env.COLL_X;
|
||||
delete process.env.COLL_Y;
|
||||
|
||||
|
|
@ -174,64 +177,19 @@ test('getManifestStatus: all collected — 2 collected entries, none in env', as
|
|||
assert.deepStrictEqual(result!.collected, ['COLL_X', 'COLL_Y']);
|
||||
assert.deepStrictEqual(result!.skipped, []);
|
||||
assert.deepStrictEqual(result!.existing, []);
|
||||
} finally {
|
||||
rmSync(tmp, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Key in env overrides manifest status ────────────────────────────────────
|
||||
// ─── Missing manifest ────────────────────────────────────────────────────────
|
||||
|
||||
test('getManifestStatus: key in env overrides manifest status — collected key in env goes to existing', async () => {
|
||||
const tmp = makeTempDir('manifest-override');
|
||||
const savedVal = process.env.GSD_TEST_OVERRIDE_KEY;
|
||||
try {
|
||||
process.env.GSD_TEST_OVERRIDE_KEY = 'already-here';
|
||||
|
||||
writeManifest(tmp, `# Secrets Manifest
|
||||
|
||||
**Milestone:** M001
|
||||
**Generated:** 2025-06-20T10:00:00Z
|
||||
|
||||
### GSD_TEST_OVERRIDE_KEY
|
||||
|
||||
**Service:** Override
|
||||
**Status:** collected
|
||||
**Destination:** dotenv
|
||||
|
||||
1. Was collected but now in env
|
||||
`);
|
||||
|
||||
const result = await getManifestStatus(tmp, 'M001');
|
||||
assert.notStrictEqual(result, null);
|
||||
assert.deepStrictEqual(result!.pending, []);
|
||||
assert.deepStrictEqual(result!.collected, []);
|
||||
assert.deepStrictEqual(result!.skipped, []);
|
||||
assert.deepStrictEqual(result!.existing, ['GSD_TEST_OVERRIDE_KEY']);
|
||||
} finally {
|
||||
delete process.env.GSD_TEST_OVERRIDE_KEY;
|
||||
if (savedVal !== undefined) process.env.GSD_TEST_OVERRIDE_KEY = savedVal;
|
||||
rmSync(tmp, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
// ─── Missing manifest ────────────────────────────────────────────────────────
|
||||
|
||||
test('getManifestStatus: missing manifest — returns null', async () => {
|
||||
const tmp = makeTempDir('manifest-missing');
|
||||
try {
|
||||
test('missing manifest — returns null', async () => {
|
||||
// No .gsd directory at all
|
||||
const result = await getManifestStatus(tmp, 'M001');
|
||||
assert.strictEqual(result, null);
|
||||
} finally {
|
||||
rmSync(tmp, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Empty manifest (no entries) ─────────────────────────────────────────────
|
||||
// ─── Empty manifest (no entries) ─────────────────────────────────────────────
|
||||
|
||||
test('getManifestStatus: empty manifest — exists but no H3 sections', async () => {
|
||||
const tmp = makeTempDir('manifest-empty');
|
||||
try {
|
||||
test('empty manifest — exists but no H3 sections', async () => {
|
||||
writeManifest(tmp, `# Secrets Manifest
|
||||
|
||||
**Milestone:** M001
|
||||
|
|
@ -244,16 +202,11 @@ test('getManifestStatus: empty manifest — exists but no H3 sections', async ()
|
|||
assert.deepStrictEqual(result!.collected, []);
|
||||
assert.deepStrictEqual(result!.skipped, []);
|
||||
assert.deepStrictEqual(result!.existing, []);
|
||||
} finally {
|
||||
rmSync(tmp, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Env via .env file (not just process.env) ────────────────────────────────
|
||||
// ─── Env via .env file (not just process.env) ────────────────────────────────
|
||||
|
||||
test('getManifestStatus: key in .env file counts as existing', async () => {
|
||||
const tmp = makeTempDir('manifest-dotenv');
|
||||
try {
|
||||
test('key in .env file counts as existing', async () => {
|
||||
delete process.env.DOTENV_ONLY_KEY;
|
||||
|
||||
writeManifest(tmp, `# Secrets Manifest
|
||||
|
|
@ -277,7 +230,45 @@ test('getManifestStatus: key in .env file counts as existing', async () => {
|
|||
assert.notStrictEqual(result, null);
|
||||
assert.deepStrictEqual(result!.existing, ['DOTENV_ONLY_KEY']);
|
||||
assert.deepStrictEqual(result!.pending, []);
|
||||
} finally {
|
||||
rmSync(tmp, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Key in env overrides manifest status ────────────────────────────────────
|
||||
|
||||
describe('getManifestStatus: key in env overrides manifest status', () => {
|
||||
let tmp: string;
|
||||
let savedVal: string | undefined;
|
||||
beforeEach(() => {
|
||||
tmp = makeTempDir('manifest-override');
|
||||
savedVal = process.env.GSD_TEST_OVERRIDE_KEY;
|
||||
process.env.GSD_TEST_OVERRIDE_KEY = 'already-here';
|
||||
});
|
||||
afterEach(() => {
|
||||
delete process.env.GSD_TEST_OVERRIDE_KEY;
|
||||
if (savedVal !== undefined) process.env.GSD_TEST_OVERRIDE_KEY = savedVal;
|
||||
rmSync(tmp, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test('collected key in env goes to existing', async () => {
|
||||
writeManifest(tmp, `# Secrets Manifest
|
||||
|
||||
**Milestone:** M001
|
||||
**Generated:** 2025-06-20T10:00:00Z
|
||||
|
||||
### GSD_TEST_OVERRIDE_KEY
|
||||
|
||||
**Service:** Override
|
||||
**Status:** collected
|
||||
**Destination:** dotenv
|
||||
|
||||
1. Was collected but now in env
|
||||
`);
|
||||
|
||||
const result = await getManifestStatus(tmp, 'M001');
|
||||
assert.notStrictEqual(result, null);
|
||||
assert.deepStrictEqual(result!.pending, []);
|
||||
assert.deepStrictEqual(result!.collected, []);
|
||||
assert.deepStrictEqual(result!.skipped, []);
|
||||
assert.deepStrictEqual(result!.existing, ['GSD_TEST_OVERRIDE_KEY']);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
* 11. Dependency audit — git diff detection, npm audit parsing, graceful failures
|
||||
*/
|
||||
|
||||
import test from "node:test";
|
||||
import { describe, test, beforeEach, afterEach } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { mkdirSync, writeFileSync, rmSync } from "node:fs";
|
||||
import { join, dirname } from "node:path";
|
||||
|
|
@ -37,37 +37,30 @@ function makeTempDir(prefix: string): string {
|
|||
|
||||
// ─── Discovery Tests ─────────────────────────────────────────────────────────
|
||||
|
||||
test("verification-gate: discoverCommands from preference commands", () => {
|
||||
const tmp = makeTempDir("vg-pref");
|
||||
try {
|
||||
describe("verification-gate: discovery", () => {
|
||||
let tmp: string;
|
||||
beforeEach(() => { tmp = makeTempDir("vg-discovery"); });
|
||||
afterEach(() => { rmSync(tmp, { recursive: true, force: true }); });
|
||||
|
||||
test("discoverCommands from preference commands", () => {
|
||||
const result = discoverCommands({
|
||||
preferenceCommands: ["npm run lint", "npm run test"],
|
||||
cwd: tmp,
|
||||
});
|
||||
assert.deepStrictEqual(result.commands, ["npm run lint", "npm run test"]);
|
||||
assert.equal(result.source, "preference");
|
||||
} finally {
|
||||
rmSync(tmp, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test("verification-gate: discoverCommands from task plan verify field", () => {
|
||||
const tmp = makeTempDir("vg-taskplan");
|
||||
try {
|
||||
test("discoverCommands from task plan verify field", () => {
|
||||
const result = discoverCommands({
|
||||
taskPlanVerify: "npm run lint && npm run test",
|
||||
cwd: tmp,
|
||||
});
|
||||
assert.deepStrictEqual(result.commands, ["npm run lint", "npm run test"]);
|
||||
assert.equal(result.source, "task-plan");
|
||||
} finally {
|
||||
rmSync(tmp, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test("verification-gate: discoverCommands from package.json scripts", () => {
|
||||
const tmp = makeTempDir("vg-pkg");
|
||||
try {
|
||||
test("discoverCommands from package.json scripts", () => {
|
||||
writeFileSync(
|
||||
join(tmp, "package.json"),
|
||||
JSON.stringify({
|
||||
|
|
@ -86,14 +79,9 @@ test("verification-gate: discoverCommands from package.json scripts", () => {
|
|||
"npm run test",
|
||||
]);
|
||||
assert.equal(result.source, "package-json");
|
||||
} finally {
|
||||
rmSync(tmp, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test("verification-gate: first-non-empty-wins — preference beats task plan and package.json", () => {
|
||||
const tmp = makeTempDir("vg-precedence");
|
||||
try {
|
||||
test("first-non-empty-wins — preference beats task plan and package.json", () => {
|
||||
writeFileSync(
|
||||
join(tmp, "package.json"),
|
||||
JSON.stringify({ scripts: { lint: "eslint ." } }),
|
||||
|
|
@ -105,14 +93,9 @@ test("verification-gate: first-non-empty-wins — preference beats task plan and
|
|||
});
|
||||
assert.deepStrictEqual(result.commands, ["custom-check"]);
|
||||
assert.equal(result.source, "preference");
|
||||
} finally {
|
||||
rmSync(tmp, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test("verification-gate: task plan verify beats package.json", () => {
|
||||
const tmp = makeTempDir("vg-tp-beats-pkg");
|
||||
try {
|
||||
test("task plan verify beats package.json", () => {
|
||||
writeFileSync(
|
||||
join(tmp, "package.json"),
|
||||
JSON.stringify({ scripts: { lint: "eslint ." } }),
|
||||
|
|
@ -123,25 +106,15 @@ test("verification-gate: task plan verify beats package.json", () => {
|
|||
});
|
||||
assert.deepStrictEqual(result.commands, ["custom-verify"]);
|
||||
assert.equal(result.source, "task-plan");
|
||||
} finally {
|
||||
rmSync(tmp, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test("verification-gate: missing package.json → 0 checks, source none", () => {
|
||||
const tmp = makeTempDir("vg-no-pkg");
|
||||
try {
|
||||
test("missing package.json → 0 checks, source none", () => {
|
||||
const result = discoverCommands({ cwd: tmp });
|
||||
assert.deepStrictEqual(result.commands, []);
|
||||
assert.equal(result.source, "none");
|
||||
} finally {
|
||||
rmSync(tmp, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test("verification-gate: package.json with no matching scripts → 0 checks", () => {
|
||||
const tmp = makeTempDir("vg-no-scripts");
|
||||
try {
|
||||
test("package.json with no matching scripts → 0 checks", () => {
|
||||
writeFileSync(
|
||||
join(tmp, "package.json"),
|
||||
JSON.stringify({ scripts: { build: "tsc", start: "node index.js" } }),
|
||||
|
|
@ -149,14 +122,9 @@ test("verification-gate: package.json with no matching scripts → 0 checks", ()
|
|||
const result = discoverCommands({ cwd: tmp });
|
||||
assert.deepStrictEqual(result.commands, []);
|
||||
assert.equal(result.source, "none");
|
||||
} finally {
|
||||
rmSync(tmp, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test("verification-gate: empty preference array falls through to task plan", () => {
|
||||
const tmp = makeTempDir("vg-empty-pref");
|
||||
try {
|
||||
test("empty preference array falls through to task plan", () => {
|
||||
const result = discoverCommands({
|
||||
preferenceCommands: [],
|
||||
taskPlanVerify: "echo ok",
|
||||
|
|
@ -164,16 +132,99 @@ test("verification-gate: empty preference array falls through to task plan", ()
|
|||
});
|
||||
assert.deepStrictEqual(result.commands, ["echo ok"]);
|
||||
assert.equal(result.source, "task-plan");
|
||||
} finally {
|
||||
rmSync(tmp, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("package.json with only test script → returns only npm run test", () => {
|
||||
writeFileSync(
|
||||
join(tmp, "package.json"),
|
||||
JSON.stringify({
|
||||
scripts: {
|
||||
test: "vitest",
|
||||
build: "tsc",
|
||||
start: "node index.js",
|
||||
},
|
||||
}),
|
||||
);
|
||||
const result = discoverCommands({ cwd: tmp });
|
||||
assert.deepStrictEqual(result.commands, ["npm run test"]);
|
||||
assert.equal(result.source, "package-json");
|
||||
});
|
||||
|
||||
test("taskPlanVerify with single command (no &&)", () => {
|
||||
const result = discoverCommands({
|
||||
taskPlanVerify: "npm test",
|
||||
cwd: tmp,
|
||||
});
|
||||
assert.deepStrictEqual(result.commands, ["npm test"]);
|
||||
assert.equal(result.source, "task-plan");
|
||||
});
|
||||
|
||||
test("whitespace-only preference commands fall through", () => {
|
||||
writeFileSync(
|
||||
join(tmp, "package.json"),
|
||||
JSON.stringify({ scripts: { lint: "eslint ." } }),
|
||||
);
|
||||
const result = discoverCommands({
|
||||
preferenceCommands: [" ", ""],
|
||||
cwd: tmp,
|
||||
});
|
||||
// Whitespace-only strings are trimmed to empty and filtered out
|
||||
assert.equal(result.source, "package-json");
|
||||
assert.deepStrictEqual(result.commands, ["npm run lint"]);
|
||||
});
|
||||
|
||||
test("prose taskPlanVerify is rejected, falls through to package.json", () => {
|
||||
writeFileSync(
|
||||
join(tmp, "package.json"),
|
||||
JSON.stringify({ scripts: { test: "vitest" } }),
|
||||
);
|
||||
const result = discoverCommands({
|
||||
taskPlanVerify: "Document exists, contains all 5 scale names, all 14 semantic tokens",
|
||||
cwd: tmp,
|
||||
});
|
||||
// Prose should be rejected, so it falls through to package.json
|
||||
assert.equal(result.source, "package-json");
|
||||
assert.deepStrictEqual(result.commands, ["npm run test"]);
|
||||
});
|
||||
|
||||
test("prose taskPlanVerify with no package.json → source none", () => {
|
||||
const result = discoverCommands({
|
||||
taskPlanVerify: "Verify the output matches expected format and all fields are present",
|
||||
cwd: tmp,
|
||||
});
|
||||
assert.equal(result.source, "none");
|
||||
assert.deepStrictEqual(result.commands, []);
|
||||
});
|
||||
|
||||
test("valid command in taskPlanVerify still works", () => {
|
||||
const result = discoverCommands({
|
||||
taskPlanVerify: "npm run lint && npm run test",
|
||||
cwd: tmp,
|
||||
});
|
||||
assert.equal(result.source, "task-plan");
|
||||
assert.deepStrictEqual(result.commands, ["npm run lint", "npm run test"]);
|
||||
});
|
||||
|
||||
test("mixed prose and commands in taskPlanVerify — only commands kept", () => {
|
||||
const result = discoverCommands({
|
||||
taskPlanVerify: "Check that everything works && npm run test",
|
||||
cwd: tmp,
|
||||
});
|
||||
// "Check that everything works" is prose (starts with capital, 4+ words)
|
||||
// "npm run test" is a valid command
|
||||
assert.equal(result.source, "task-plan");
|
||||
assert.deepStrictEqual(result.commands, ["npm run test"]);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Execution Tests ─────────────────────────────────────────────────────────
|
||||
|
||||
test("verification-gate: all commands pass → gate passes", () => {
|
||||
const tmp = makeTempDir("vg-pass");
|
||||
try {
|
||||
describe("verification-gate: execution", () => {
|
||||
let tmp: string;
|
||||
beforeEach(() => { tmp = makeTempDir("vg-exec"); });
|
||||
afterEach(() => { rmSync(tmp, { recursive: true, force: true }); });
|
||||
|
||||
test("all commands pass → gate passes", () => {
|
||||
const result = runVerificationGate({
|
||||
basePath: tmp,
|
||||
unitId: "T01",
|
||||
|
|
@ -188,14 +239,9 @@ test("verification-gate: all commands pass → gate passes", () => {
|
|||
assert.ok(result.checks[0].stdout.includes("hello"));
|
||||
assert.ok(result.checks[1].stdout.includes("world"));
|
||||
assert.equal(typeof result.timestamp, "number");
|
||||
} finally {
|
||||
rmSync(tmp, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test("verification-gate: one command fails → gate fails with exit code + stderr", () => {
|
||||
const tmp = makeTempDir("vg-fail");
|
||||
try {
|
||||
test("one command fails → gate fails with exit code + stderr", () => {
|
||||
const result = runVerificationGate({
|
||||
basePath: tmp,
|
||||
unitId: "T01",
|
||||
|
|
@ -207,14 +253,9 @@ test("verification-gate: one command fails → gate fails with exit code + stder
|
|||
assert.equal(result.checks[0].exitCode, 0);
|
||||
assert.equal(result.checks[1].exitCode, 1);
|
||||
assert.ok(result.checks[1].stderr.includes("err"));
|
||||
} finally {
|
||||
rmSync(tmp, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test("verification-gate: no commands discovered → gate passes with 0 checks", () => {
|
||||
const tmp = makeTempDir("vg-empty");
|
||||
try {
|
||||
test("no commands discovered → gate passes with 0 checks", () => {
|
||||
const result = runVerificationGate({
|
||||
basePath: tmp,
|
||||
unitId: "T01",
|
||||
|
|
@ -223,14 +264,9 @@ test("verification-gate: no commands discovered → gate passes with 0 checks",
|
|||
assert.equal(result.passed, true);
|
||||
assert.equal(result.checks.length, 0);
|
||||
assert.equal(result.discoverySource, "none");
|
||||
} finally {
|
||||
rmSync(tmp, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test("verification-gate: command not found → exit code 127", () => {
|
||||
const tmp = makeTempDir("vg-notfound");
|
||||
try {
|
||||
test("command not found → exit code 127", () => {
|
||||
const result = runVerificationGate({
|
||||
basePath: tmp,
|
||||
unitId: "T01",
|
||||
|
|
@ -241,14 +277,9 @@ test("verification-gate: command not found → exit code 127", () => {
|
|||
assert.equal(result.checks.length, 1);
|
||||
assert.ok(result.checks[0].exitCode !== 0, "should have non-zero exit code");
|
||||
assert.ok(result.checks[0].durationMs >= 0);
|
||||
} finally {
|
||||
rmSync(tmp, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test("verification-gate: no DEP0190 deprecation warning when running commands", () => {
|
||||
const tmp = makeTempDir("vg-dep0190");
|
||||
try {
|
||||
test("no DEP0190 deprecation warning when running commands", () => {
|
||||
// Run a subprocess with --throw-deprecation so any DeprecationWarning
|
||||
// becomes a thrown error (non-zero exit). The fix passes the command
|
||||
// string to sh -c explicitly instead of using spawnSync(cmd, {shell:true}).
|
||||
|
|
@ -282,14 +313,9 @@ test("verification-gate: no DEP0190 deprecation warning when running commands",
|
|||
0,
|
||||
`Expected exit 0 (no deprecation) but got ${child.status}. stderr: ${child.stderr}`,
|
||||
);
|
||||
} finally {
|
||||
rmSync(tmp, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test("verification-gate: each check has durationMs", () => {
|
||||
const tmp = makeTempDir("vg-duration");
|
||||
try {
|
||||
test("each check has durationMs", () => {
|
||||
const result = runVerificationGate({
|
||||
basePath: tmp,
|
||||
unitId: "T01",
|
||||
|
|
@ -299,9 +325,42 @@ test("verification-gate: each check has durationMs", () => {
|
|||
assert.equal(result.checks.length, 1);
|
||||
assert.equal(typeof result.checks[0].durationMs, "number");
|
||||
assert.ok(result.checks[0].durationMs >= 0);
|
||||
} finally {
|
||||
rmSync(tmp, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("one command fails — remaining commands still run (non-short-circuit)", () => {
|
||||
// First fails, second and third should still execute
|
||||
const result = runVerificationGate({
|
||||
basePath: tmp,
|
||||
unitId: "T02",
|
||||
cwd: tmp,
|
||||
preferenceCommands: [
|
||||
"sh -c 'exit 1'",
|
||||
"echo second",
|
||||
"echo third",
|
||||
],
|
||||
});
|
||||
assert.equal(result.passed, false);
|
||||
assert.equal(result.checks.length, 3, "all 3 commands should run");
|
||||
assert.equal(result.checks[0].exitCode, 1, "first command fails");
|
||||
assert.equal(result.checks[1].exitCode, 0, "second command runs and passes");
|
||||
assert.ok(result.checks[1].stdout.includes("second"));
|
||||
assert.equal(result.checks[2].exitCode, 0, "third command runs and passes");
|
||||
assert.ok(result.checks[2].stdout.includes("third"));
|
||||
});
|
||||
|
||||
test("gate execution uses cwd for spawnSync", () => {
|
||||
// pwd should report the temp dir
|
||||
const result = runVerificationGate({
|
||||
basePath: tmp,
|
||||
unitId: "T02",
|
||||
cwd: tmp,
|
||||
preferenceCommands: ["pwd"],
|
||||
});
|
||||
assert.equal(result.passed, true);
|
||||
assert.equal(result.checks.length, 1);
|
||||
// The stdout should contain the tmp dir path (resolving symlinks)
|
||||
assert.ok(result.checks[0].stdout.trim().length > 0, "pwd should produce output");
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Preference Validation Tests ─────────────────────────────────────────────
|
||||
|
|
@ -361,62 +420,6 @@ test("verification-gate: validatePreferences floors verification_max_retries", (
|
|||
assert.equal(result.errors.length, 0);
|
||||
});
|
||||
|
||||
// ─── Additional Discovery Tests (T02) ───────────────────────────────────────
|
||||
|
||||
test("verification-gate: package.json with only test script → returns only npm run test", () => {
|
||||
const tmp = makeTempDir("vg-only-test");
|
||||
try {
|
||||
writeFileSync(
|
||||
join(tmp, "package.json"),
|
||||
JSON.stringify({
|
||||
scripts: {
|
||||
test: "vitest",
|
||||
build: "tsc",
|
||||
start: "node index.js",
|
||||
},
|
||||
}),
|
||||
);
|
||||
const result = discoverCommands({ cwd: tmp });
|
||||
assert.deepStrictEqual(result.commands, ["npm run test"]);
|
||||
assert.equal(result.source, "package-json");
|
||||
} finally {
|
||||
rmSync(tmp, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("verification-gate: taskPlanVerify with single command (no &&)", () => {
|
||||
const tmp = makeTempDir("vg-tp-single");
|
||||
try {
|
||||
const result = discoverCommands({
|
||||
taskPlanVerify: "npm test",
|
||||
cwd: tmp,
|
||||
});
|
||||
assert.deepStrictEqual(result.commands, ["npm test"]);
|
||||
assert.equal(result.source, "task-plan");
|
||||
} finally {
|
||||
rmSync(tmp, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("verification-gate: whitespace-only preference commands fall through", () => {
|
||||
const tmp = makeTempDir("vg-ws-pref");
|
||||
try {
|
||||
writeFileSync(
|
||||
join(tmp, "package.json"),
|
||||
JSON.stringify({ scripts: { lint: "eslint ." } }),
|
||||
);
|
||||
const result = discoverCommands({
|
||||
preferenceCommands: [" ", ""],
|
||||
cwd: tmp,
|
||||
});
|
||||
// Whitespace-only strings are trimmed to empty and filtered out
|
||||
assert.equal(result.source, "package-json");
|
||||
assert.deepStrictEqual(result.commands, ["npm run lint"]);
|
||||
} finally {
|
||||
rmSync(tmp, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
// ─── isLikelyCommand Tests (issue #1066) ────────────────────────────────────
|
||||
|
||||
test("isLikelyCommand: known command prefixes are accepted", () => {
|
||||
|
|
@ -468,116 +471,6 @@ test("isLikelyCommand: short lowercase tokens without flags are accepted (could
|
|||
assert.equal(isLikelyCommand("mycheck"), true);
|
||||
});
|
||||
|
||||
test("verification-gate: prose taskPlanVerify is rejected, falls through to package.json", () => {
|
||||
const tmp = makeTempDir("vg-prose-reject");
|
||||
try {
|
||||
writeFileSync(
|
||||
join(tmp, "package.json"),
|
||||
JSON.stringify({ scripts: { test: "vitest" } }),
|
||||
);
|
||||
const result = discoverCommands({
|
||||
taskPlanVerify: "Document exists, contains all 5 scale names, all 14 semantic tokens",
|
||||
cwd: tmp,
|
||||
});
|
||||
// Prose should be rejected, so it falls through to package.json
|
||||
assert.equal(result.source, "package-json");
|
||||
assert.deepStrictEqual(result.commands, ["npm run test"]);
|
||||
} finally {
|
||||
rmSync(tmp, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("verification-gate: prose taskPlanVerify with no package.json → source none", () => {
|
||||
const tmp = makeTempDir("vg-prose-none");
|
||||
try {
|
||||
const result = discoverCommands({
|
||||
taskPlanVerify: "Verify the output matches expected format and all fields are present",
|
||||
cwd: tmp,
|
||||
});
|
||||
assert.equal(result.source, "none");
|
||||
assert.deepStrictEqual(result.commands, []);
|
||||
} finally {
|
||||
rmSync(tmp, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("verification-gate: valid command in taskPlanVerify still works", () => {
|
||||
const tmp = makeTempDir("vg-valid-cmd");
|
||||
try {
|
||||
const result = discoverCommands({
|
||||
taskPlanVerify: "npm run lint && npm run test",
|
||||
cwd: tmp,
|
||||
});
|
||||
assert.equal(result.source, "task-plan");
|
||||
assert.deepStrictEqual(result.commands, ["npm run lint", "npm run test"]);
|
||||
} finally {
|
||||
rmSync(tmp, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("verification-gate: mixed prose and commands in taskPlanVerify — only commands kept", () => {
|
||||
const tmp = makeTempDir("vg-mixed");
|
||||
try {
|
||||
const result = discoverCommands({
|
||||
taskPlanVerify: "Check that everything works && npm run test",
|
||||
cwd: tmp,
|
||||
});
|
||||
// "Check that everything works" is prose (starts with capital, 4+ words)
|
||||
// "npm run test" is a valid command
|
||||
assert.equal(result.source, "task-plan");
|
||||
assert.deepStrictEqual(result.commands, ["npm run test"]);
|
||||
} finally {
|
||||
rmSync(tmp, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
// ─── Additional Execution Tests (T02) ───────────────────────────────────────
|
||||
|
||||
test("verification-gate: one command fails — remaining commands still run (non-short-circuit)", () => {
|
||||
const tmp = makeTempDir("vg-no-short-circuit");
|
||||
try {
|
||||
// First fails, second and third should still execute
|
||||
const result = runVerificationGate({
|
||||
basePath: tmp,
|
||||
unitId: "T02",
|
||||
cwd: tmp,
|
||||
preferenceCommands: [
|
||||
"sh -c 'exit 1'",
|
||||
"echo second",
|
||||
"echo third",
|
||||
],
|
||||
});
|
||||
assert.equal(result.passed, false);
|
||||
assert.equal(result.checks.length, 3, "all 3 commands should run");
|
||||
assert.equal(result.checks[0].exitCode, 1, "first command fails");
|
||||
assert.equal(result.checks[1].exitCode, 0, "second command runs and passes");
|
||||
assert.ok(result.checks[1].stdout.includes("second"));
|
||||
assert.equal(result.checks[2].exitCode, 0, "third command runs and passes");
|
||||
assert.ok(result.checks[2].stdout.includes("third"));
|
||||
} finally {
|
||||
rmSync(tmp, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("verification-gate: gate execution uses cwd for spawnSync", () => {
|
||||
const tmp = makeTempDir("vg-cwd");
|
||||
try {
|
||||
// pwd should report the temp dir
|
||||
const result = runVerificationGate({
|
||||
basePath: tmp,
|
||||
unitId: "T02",
|
||||
cwd: tmp,
|
||||
preferenceCommands: ["pwd"],
|
||||
});
|
||||
assert.equal(result.passed, true);
|
||||
assert.equal(result.checks.length, 1);
|
||||
// The stdout should contain the tmp dir path (resolving symlinks)
|
||||
assert.ok(result.checks[0].stdout.trim().length > 0, "pwd should produce output");
|
||||
} finally {
|
||||
rmSync(tmp, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
// ─── Additional Preference Validation Tests (T02) ──────────────────────────
|
||||
|
||||
test("verification-gate: verification_commands produces no unknown-key warnings", () => {
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
* rather than hard-coding package.json / src/ only.
|
||||
*/
|
||||
|
||||
import test from "node:test";
|
||||
import { describe, test, beforeEach, afterEach } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
|
@ -67,112 +67,69 @@ test("PROJECT_FILES is exported and contains expected multi-ecosystem entries",
|
|||
assert.ok(PROJECT_FILES.includes("Package.swift"), "includes Swift marker");
|
||||
});
|
||||
|
||||
test("health check passes for Rust project (Cargo.toml, no package.json)", () => {
|
||||
const dir = createGitRepo();
|
||||
try {
|
||||
describe("health check with git repo", () => {
|
||||
let dir: string;
|
||||
beforeEach(() => { dir = createGitRepo(); });
|
||||
afterEach(() => { rmSync(dir, { recursive: true, force: true }); });
|
||||
|
||||
test("health check passes for Rust project (Cargo.toml, no package.json)", () => {
|
||||
writeFileSync(join(dir, "Cargo.toml"), "[package]\nname = \"test\"\n");
|
||||
mkdirSync(join(dir, "crates"), { recursive: true });
|
||||
assert.ok(wouldPassHealthCheck(dir, existsSync), "Rust project should pass health check");
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test("health check passes for Go project (go.mod, no package.json)", () => {
|
||||
const dir = createGitRepo();
|
||||
try {
|
||||
test("health check passes for Go project (go.mod, no package.json)", () => {
|
||||
writeFileSync(join(dir, "go.mod"), "module example.com/test\n\ngo 1.21\n");
|
||||
assert.ok(wouldPassHealthCheck(dir, existsSync), "Go project should pass health check");
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test("health check passes for Python project (pyproject.toml, no package.json)", () => {
|
||||
const dir = createGitRepo();
|
||||
try {
|
||||
test("health check passes for Python project (pyproject.toml, no package.json)", () => {
|
||||
writeFileSync(join(dir, "pyproject.toml"), "[project]\nname = \"test\"\n");
|
||||
assert.ok(wouldPassHealthCheck(dir, existsSync), "Python project should pass health check");
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test("health check passes for Java project (pom.xml, no package.json)", () => {
|
||||
const dir = createGitRepo();
|
||||
try {
|
||||
test("health check passes for Java project (pom.xml, no package.json)", () => {
|
||||
writeFileSync(join(dir, "pom.xml"), "<project></project>\n");
|
||||
assert.ok(wouldPassHealthCheck(dir, existsSync), "Java project should pass health check");
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test("health check passes for Swift project (Package.swift, no package.json)", () => {
|
||||
const dir = createGitRepo();
|
||||
try {
|
||||
test("health check passes for Swift project (Package.swift, no package.json)", () => {
|
||||
writeFileSync(join(dir, "Package.swift"), "// swift-tools-version:5.7\n");
|
||||
assert.ok(wouldPassHealthCheck(dir, existsSync), "Swift project should pass health check");
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test("health check passes for C/C++ project (CMakeLists.txt, no package.json)", () => {
|
||||
const dir = createGitRepo();
|
||||
try {
|
||||
test("health check passes for C/C++ project (CMakeLists.txt, no package.json)", () => {
|
||||
writeFileSync(join(dir, "CMakeLists.txt"), "cmake_minimum_required(VERSION 3.20)\n");
|
||||
assert.ok(wouldPassHealthCheck(dir, existsSync), "C/C++ project should pass health check");
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test("health check passes for Elixir project (mix.exs, no package.json)", () => {
|
||||
const dir = createGitRepo();
|
||||
try {
|
||||
test("health check passes for Elixir project (mix.exs, no package.json)", () => {
|
||||
writeFileSync(join(dir, "mix.exs"), "defmodule Test.MixProject do\nend\n");
|
||||
assert.ok(wouldPassHealthCheck(dir, existsSync), "Elixir project should pass health check");
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test("health check passes for JS project (package.json, backward compat)", () => {
|
||||
const dir = createGitRepo();
|
||||
try {
|
||||
test("health check passes for JS project (package.json, backward compat)", () => {
|
||||
writeFileSync(join(dir, "package.json"), '{"name":"test"}\n');
|
||||
assert.ok(wouldPassHealthCheck(dir, existsSync), "JS project should pass health check");
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test("health check passes for src/-only project (backward compat)", () => {
|
||||
const dir = createGitRepo();
|
||||
try {
|
||||
test("health check passes for src/-only project (backward compat)", () => {
|
||||
mkdirSync(join(dir, "src"), { recursive: true });
|
||||
assert.ok(wouldPassHealthCheck(dir, existsSync), "src/-only project should pass health check");
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("health check fails for empty git repo with no project files", () => {
|
||||
assert.ok(!wouldPassHealthCheck(dir, existsSync), "empty git repo should fail health check");
|
||||
});
|
||||
});
|
||||
|
||||
test("health check fails for directory with no .git", () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), "wt-dispatch-test-nogit-"));
|
||||
try {
|
||||
describe("health check without git repo", () => {
|
||||
let dir: string;
|
||||
beforeEach(() => { dir = mkdtempSync(join(tmpdir(), "wt-dispatch-test-nogit-")); });
|
||||
afterEach(() => { rmSync(dir, { recursive: true, force: true }); });
|
||||
|
||||
test("health check fails for directory with no .git", () => {
|
||||
writeFileSync(join(dir, "Cargo.toml"), "[package]\nname = \"test\"\n");
|
||||
assert.ok(!wouldPassHealthCheck(dir, existsSync), "no-git directory should fail health check");
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("health check fails for empty git repo with no project files", () => {
|
||||
const dir = createGitRepo();
|
||||
try {
|
||||
assert.ok(!wouldPassHealthCheck(dir, existsSync), "empty git repo should fail health check");
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import test from "node:test";
|
||||
import { describe, test, beforeEach, afterEach } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { mkdtempSync, mkdirSync, rmSync, writeFileSync, existsSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
|
@ -73,9 +73,12 @@ test("worktreeBranchName formats branch name", () => {
|
|||
|
||||
// ─── createWorktree ───────────────────────────────────────────────────────────
|
||||
|
||||
test("createWorktree creates worktree with correct metadata", () => {
|
||||
const base = makeBaseRepo();
|
||||
try {
|
||||
describe("createWorktree", () => {
|
||||
let base: string;
|
||||
beforeEach(() => { base = makeBaseRepo(); });
|
||||
afterEach(() => { rmSync(base, { recursive: true, force: true }); });
|
||||
|
||||
test("creates worktree with correct metadata", () => {
|
||||
const info = createWorktree(base, "feature-x");
|
||||
assert.strictEqual(info.name, "feature-x", "name should match");
|
||||
assert.strictEqual(info.branch, "worktree/feature-x", "branch should be prefixed");
|
||||
|
|
@ -88,33 +91,9 @@ test("createWorktree creates worktree with correct metadata", () => {
|
|||
);
|
||||
const branches = run("git branch", base);
|
||||
assert.ok(branches.includes("worktree/feature-x"), "branch should be created in base repo");
|
||||
} finally {
|
||||
rmSync(base, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test("createWorktree rejects duplicate name", () => {
|
||||
const { base } = makeRepoWithWorktree("feature-x");
|
||||
try {
|
||||
assert.throws(
|
||||
() => createWorktree(base, "feature-x"),
|
||||
(err: Error) => {
|
||||
assert.ok(
|
||||
err.message.includes("already exists"),
|
||||
`expected "already exists" in error, got: ${err.message}`,
|
||||
);
|
||||
return true;
|
||||
},
|
||||
"should throw on duplicate worktree name",
|
||||
);
|
||||
} finally {
|
||||
rmSync(base, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("createWorktree rejects invalid name", () => {
|
||||
const base = makeBaseRepo();
|
||||
try {
|
||||
test("rejects invalid name", () => {
|
||||
assert.throws(
|
||||
() => createWorktree(base, "bad name!"),
|
||||
(err: Error) => {
|
||||
|
|
@ -126,42 +105,68 @@ test("createWorktree rejects invalid name", () => {
|
|||
},
|
||||
"should throw on invalid worktree name",
|
||||
);
|
||||
} finally {
|
||||
rmSync(base, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("createWorktree — duplicate rejection", () => {
|
||||
let base: string;
|
||||
beforeEach(() => {
|
||||
const repo = makeRepoWithWorktree("feature-x");
|
||||
base = repo.base;
|
||||
});
|
||||
afterEach(() => { rmSync(base, { recursive: true, force: true }); });
|
||||
|
||||
test("rejects duplicate name", () => {
|
||||
assert.throws(
|
||||
() => createWorktree(base, "feature-x"),
|
||||
(err: Error) => {
|
||||
assert.ok(
|
||||
err.message.includes("already exists"),
|
||||
`expected "already exists" in error, got: ${err.message}`,
|
||||
);
|
||||
return true;
|
||||
},
|
||||
"should throw on duplicate worktree name",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── listWorktrees ────────────────────────────────────────────────────────────
|
||||
|
||||
test("listWorktrees returns active worktrees", () => {
|
||||
const { base } = makeRepoWithWorktree("feature-x");
|
||||
try {
|
||||
describe("listWorktrees", () => {
|
||||
let base: string;
|
||||
beforeEach(() => {
|
||||
const repo = makeRepoWithWorktree("feature-x");
|
||||
base = repo.base;
|
||||
});
|
||||
afterEach(() => { rmSync(base, { recursive: true, force: true }); });
|
||||
|
||||
test("returns active worktrees", () => {
|
||||
const list = listWorktrees(base);
|
||||
assert.strictEqual(list.length, 1, "should list exactly one worktree");
|
||||
assert.strictEqual(list[0]!.name, "feature-x", "name should match");
|
||||
assert.strictEqual(list[0]!.branch, "worktree/feature-x", "branch should match");
|
||||
assert.ok(list[0]!.exists, "exists flag should be true");
|
||||
} finally {
|
||||
rmSync(base, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test("listWorktrees returns empty after removal", () => {
|
||||
const { base } = makeRepoWithWorktree("feature-x");
|
||||
try {
|
||||
test("returns empty after removal", () => {
|
||||
removeWorktree(base, "feature-x");
|
||||
const list = listWorktrees(base);
|
||||
assert.strictEqual(list.length, 0, "should have no worktrees after removal");
|
||||
} finally {
|
||||
rmSync(base, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ─── diffWorktreeGSD ─────────────────────────────────────────────────────────
|
||||
|
||||
test("diffWorktreeGSD detects added and modified GSD files", () => {
|
||||
const { base } = makeRepoWithChanges("feature-x");
|
||||
try {
|
||||
describe("diffWorktreeGSD and getWorktreeGSDDiff", () => {
|
||||
let base: string;
|
||||
beforeEach(() => {
|
||||
const repo = makeRepoWithChanges("feature-x");
|
||||
base = repo.base;
|
||||
});
|
||||
afterEach(() => { rmSync(base, { recursive: true, force: true }); });
|
||||
|
||||
test("detects added and modified GSD files", () => {
|
||||
const diff = diffWorktreeGSD(base, "feature-x");
|
||||
assert.ok(diff.added.length > 0, "should have added files");
|
||||
assert.ok(
|
||||
|
|
@ -174,58 +179,60 @@ test("diffWorktreeGSD detects added and modified GSD files", () => {
|
|||
"M001 roadmap should be in modified files",
|
||||
);
|
||||
assert.strictEqual(diff.removed.length, 0, "should have no removed files");
|
||||
} finally {
|
||||
rmSync(base, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ─── getWorktreeGSDDiff ───────────────────────────────────────────────────────
|
||||
|
||||
test("getWorktreeGSDDiff returns patch content", () => {
|
||||
const { base } = makeRepoWithChanges("feature-x");
|
||||
try {
|
||||
test("returns patch content", () => {
|
||||
const fullDiff = getWorktreeGSDDiff(base, "feature-x");
|
||||
assert.ok(fullDiff.includes("M002"), "diff should mention M002");
|
||||
assert.ok(fullDiff.includes("updated"), "diff should mention the update");
|
||||
} finally {
|
||||
rmSync(base, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ─── getWorktreeLog ───────────────────────────────────────────────────────────
|
||||
|
||||
test("getWorktreeLog shows commits", () => {
|
||||
const { base } = makeRepoWithChanges("feature-x");
|
||||
try {
|
||||
describe("getWorktreeLog", () => {
|
||||
let base: string;
|
||||
beforeEach(() => {
|
||||
const repo = makeRepoWithChanges("feature-x");
|
||||
base = repo.base;
|
||||
});
|
||||
afterEach(() => { rmSync(base, { recursive: true, force: true }); });
|
||||
|
||||
test("shows commits", () => {
|
||||
const log = getWorktreeLog(base, "feature-x");
|
||||
assert.ok(log.includes("add M002"), "log should include the commit message");
|
||||
} finally {
|
||||
rmSync(base, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ─── removeWorktree ───────────────────────────────────────────────────────────
|
||||
|
||||
test("removeWorktree removes directory and branch", () => {
|
||||
const { base, wtPath } = makeRepoWithWorktree("feature-x");
|
||||
try {
|
||||
describe("removeWorktree", () => {
|
||||
let base: string;
|
||||
let wtPath: string;
|
||||
beforeEach(() => {
|
||||
const repo = makeRepoWithWorktree("feature-x");
|
||||
base = repo.base;
|
||||
wtPath = repo.wtPath;
|
||||
});
|
||||
afterEach(() => { rmSync(base, { recursive: true, force: true }); });
|
||||
|
||||
test("removes directory and branch", () => {
|
||||
removeWorktree(base, "feature-x", { deleteBranch: true });
|
||||
assert.ok(!existsSync(wtPath), "worktree directory should be gone");
|
||||
const branches = run("git branch", base);
|
||||
assert.ok(!branches.includes("worktree/feature-x"), "branch should be deleted");
|
||||
} finally {
|
||||
rmSync(base, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test("removeWorktree on missing worktree does not throw", () => {
|
||||
const base = makeBaseRepo();
|
||||
try {
|
||||
describe("removeWorktree — missing worktree", () => {
|
||||
let base: string;
|
||||
beforeEach(() => { base = makeBaseRepo(); });
|
||||
afterEach(() => { rmSync(base, { recursive: true, force: true }); });
|
||||
|
||||
test("on missing worktree does not throw", () => {
|
||||
assert.doesNotThrow(
|
||||
() => removeWorktree(base, "nonexistent"),
|
||||
"should not throw when worktree does not exist",
|
||||
);
|
||||
} finally {
|
||||
rmSync(base, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue