From 77460942ac57c4b45fd19ebfd690627ebca4aeb0 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Tue, 24 Mar 2026 23:33:39 -0400 Subject: [PATCH] refactor(test): migrate gsd/tests s-z from custom harness to node:test (#2397) --- .../gsd/tests/session-lock-multipath.test.ts | 43 ++-- .../gsd/tests/session-lock-regression.test.ts | 81 ++++---- .../extensions/gsd/tests/shared-wal.test.ts | 45 ++--- .../gsd/tests/stalled-tool-recovery.test.ts | 14 +- .../tests/symlink-numbered-variants.test.ts | 50 ++--- .../gsd/tests/token-savings.test.ts | 110 +++++----- .../gsd/tests/tool-call-loop-guard.test.ts | 48 +++-- .../extensions/gsd/tests/tool-naming.test.ts | 20 +- .../gsd/tests/unique-milestone-ids.test.ts | 148 ++++++-------- .../extensions/gsd/tests/unit-runtime.test.ts | 93 +++++---- .../tests/visualizer-critical-path.test.ts | 42 ++-- .../gsd/tests/visualizer-data.test.ts | 170 ++++++++-------- .../gsd/tests/visualizer-overlay.test.ts | 84 ++++---- .../gsd/tests/visualizer-views.test.ts | 190 +++++++++--------- .../tests/windows-path-normalization.test.ts | 24 +-- .../gsd/tests/worker-registry.test.ts | 56 +++--- .../gsd/tests/workflow-templates.test.ts | 102 +++++----- .../gsd/tests/worktree-bugfix.test.ts | 23 +-- .../gsd/tests/worktree-db-integration.test.ts | 32 ++- .../extensions/gsd/tests/worktree-db.test.ts | 77 ++++--- .../extensions/gsd/tests/worktree-e2e.test.ts | 38 ++-- .../gsd/tests/worktree-health.test.ts | 55 +++-- .../gsd/tests/worktree-integration.test.ts | 67 +++--- .../tests/worktree-symlink-removal.test.ts | 37 ++-- .../tests/worktree-sync-milestones.test.ts | 125 ++++++------ .../extensions/gsd/tests/worktree.test.ts | 94 ++++----- 26 files changed, 879 insertions(+), 989 deletions(-) diff --git a/src/resources/extensions/gsd/tests/session-lock-multipath.test.ts b/src/resources/extensions/gsd/tests/session-lock-multipath.test.ts index e50cc8e8a..66ed062b6 100644 --- a/src/resources/extensions/gsd/tests/session-lock-multipath.test.ts +++ b/src/resources/extensions/gsd/tests/session-lock-multipath.test.ts @@ -20,11 +20,11 @@ import { _getRegisteredLockDirs, } from '../session-lock.ts'; import { gsdRoot } from '../paths.ts'; -import { createTestContext } from './test-helpers.ts'; +import { describe, test } from 'node:test'; +import assert from 'node:assert/strict'; -const { assertEq, assertTrue, report } = createTestContext(); -async function main(): Promise { +describe('session-lock-multipath', async () => { // ─── 1. Lock dir registry tracks gsdDir on acquisition ────────────────── console.log('\n=== 1. Lock dir registry tracks gsdDir on acquisition ==='); @@ -34,17 +34,17 @@ async function main(): Promise { try { const result = acquireSessionLock(base); - assertTrue(result.acquired, 'lock acquired'); + assert.ok(result.acquired, 'lock acquired'); const registered = _getRegisteredLockDirs(); const gsdDir = gsdRoot(base); - assertTrue(registered.includes(gsdDir), 'gsdDir is registered in lock dir registry'); + assert.ok(registered.includes(gsdDir), 'gsdDir is registered in lock dir registry'); releaseSessionLock(base); // After release, registry should be cleared const afterRelease = _getRegisteredLockDirs(); - assertEq(afterRelease.length, 0, 'lock dir registry cleared after release'); + assert.deepStrictEqual(afterRelease.length, 0, 'lock dir registry cleared after release'); } finally { rmSync(base, { recursive: true, force: true }); } @@ -62,7 +62,7 @@ async function main(): Promise { try { const result = acquireSessionLock(base); - assertTrue(result.acquired, 'lock acquired'); + assert.ok(result.acquired, 'lock acquired'); // Manually plant a stale lock file at the secondary path to simulate // multi-path lock accumulation @@ -72,8 +72,8 @@ async function main(): Promise { mkdirSync(secondaryLockDir, { recursive: true }); // Verify they exist before release - assertTrue(existsSync(secondaryLockFile), 'secondary lock file exists before release'); - assertTrue(existsSync(secondaryLockDir), 'secondary lock dir exists before release'); + assert.ok(existsSync(secondaryLockFile), 'secondary lock file exists before release'); + assert.ok(existsSync(secondaryLockDir), 'secondary lock dir exists before release'); // Manually add the secondary dir to the registry (simulating ensureExitHandler call) // We do this by acquiring knowledge of internals — the registry is populated @@ -83,10 +83,10 @@ async function main(): Promise { // Primary lock artifacts should be cleaned const primaryLockFile = join(gsdRoot(base), 'auto.lock'); - assertTrue(!existsSync(primaryLockFile), 'primary auto.lock removed after release'); + assert.ok(!existsSync(primaryLockFile), 'primary auto.lock removed after release'); const primaryLockDir = gsdRoot(base) + '.lock'; - assertTrue(!existsSync(primaryLockDir), 'primary .gsd.lock/ removed after release'); + assert.ok(!existsSync(primaryLockDir), 'primary .gsd.lock/ removed after release'); } finally { rmSync(base, { recursive: true, force: true }); } @@ -106,7 +106,7 @@ async function main(): Promise { const gsdDir = gsdRoot(base); // Should only appear once (Set deduplication) const count = registered.filter(d => d === gsdDir).length; - assertEq(count, 1, 'gsdDir registered exactly once after re-entrant acquisition'); + assert.deepStrictEqual(count, 1, 'gsdDir registered exactly once after re-entrant acquisition'); releaseSessionLock(base); } finally { @@ -124,17 +124,17 @@ async function main(): Promise { try { const r1 = acquireSessionLock(base1); - assertTrue(r1.acquired, 'first base lock acquired'); + assert.ok(r1.acquired, 'first base lock acquired'); // Release first to acquire second (module state is single-lock) releaseSessionLock(base1); const r2 = acquireSessionLock(base2); - assertTrue(r2.acquired, 'second base lock acquired'); + assert.ok(r2.acquired, 'second base lock acquired'); const registered = _getRegisteredLockDirs(); const gsd2 = gsdRoot(base2); - assertTrue(registered.includes(gsd2), 'second gsdDir is registered'); + assert.ok(registered.includes(gsd2), 'second gsdDir is registered'); releaseSessionLock(base2); } finally { @@ -156,18 +156,11 @@ async function main(): Promise { // Verify everything is clean const lockFile = join(gsdRoot(base), 'auto.lock'); const lockDir = gsdRoot(base) + '.lock'; - assertTrue(!existsSync(lockFile), 'auto.lock cleaned'); - assertTrue(!existsSync(lockDir), '.gsd.lock/ cleaned'); - assertEq(_getRegisteredLockDirs().length, 0, 'registry empty'); + assert.ok(!existsSync(lockFile), 'auto.lock cleaned'); + assert.ok(!existsSync(lockDir), '.gsd.lock/ cleaned'); + assert.deepStrictEqual(_getRegisteredLockDirs().length, 0, 'registry empty'); } finally { rmSync(base, { recursive: true, force: true }); } } - - report(); -} - -main().catch((error) => { - console.error(error); - process.exit(1); }); diff --git a/src/resources/extensions/gsd/tests/session-lock-regression.test.ts b/src/resources/extensions/gsd/tests/session-lock-regression.test.ts index 22bc3d397..dd763640a 100644 --- a/src/resources/extensions/gsd/tests/session-lock-regression.test.ts +++ b/src/resources/extensions/gsd/tests/session-lock-regression.test.ts @@ -25,9 +25,9 @@ import { isSessionLockHeld, } from '../session-lock.ts'; import { gsdRoot } from '../paths.ts'; -import { createTestContext } from './test-helpers.ts'; +import { describe, test } from 'node:test'; +import assert from 'node:assert/strict'; -const { assertEq, assertTrue, report } = createTestContext(); const require = createRequire(import.meta.url); function hasProperLockfile(): boolean { @@ -41,7 +41,7 @@ function hasProperLockfile(): boolean { const properLockfileAvailable = hasProperLockfile(); -async function main(): Promise { +describe('session-lock-regression', async () => { // ─── 1. Basic acquire/release lifecycle ─────────────────────────────── console.log('\n=== 1. acquire → validate → release lifecycle ==='); @@ -51,22 +51,22 @@ async function main(): Promise { try { const result = acquireSessionLock(base); - assertTrue(result.acquired, 'lock acquired successfully'); + assert.ok(result.acquired, 'lock acquired successfully'); const valid = validateSessionLock(base); - assertTrue(valid, 'lock validates after acquisition'); + assert.ok(valid, 'lock validates after acquisition'); - assertTrue(isSessionLockHeld(base), 'isSessionLockHeld returns true'); + assert.ok(isSessionLockHeld(base), 'isSessionLockHeld returns true'); releaseSessionLock(base); // After release, the lock file should be cleaned up const lockFile = join(gsdRoot(base), 'auto.lock'); - assertTrue(!existsSync(lockFile), 'lock file removed after release'); + assert.ok(!existsSync(lockFile), 'lock file removed after release'); // The .gsd.lock/ directory should be cleaned up const lockDir = gsdRoot(base) + '.lock'; - assertTrue(!existsSync(lockDir), '.gsd.lock/ directory removed after release (#1245)'); + assert.ok(!existsSync(lockDir), '.gsd.lock/ directory removed after release (#1245)'); } finally { rmSync(base, { recursive: true, force: true }); } @@ -88,7 +88,7 @@ async function main(): Promise { } catch { threw = true; } - assertTrue(!threw, 'double release does not throw'); + assert.ok(!threw, 'double release does not throw'); } finally { rmSync(base, { recursive: true, force: true }); } @@ -106,13 +106,13 @@ async function main(): Promise { updateSessionLock(base, 'execute-task', 'M001/S01/T01', 5, '/tmp/session.json'); const data = readSessionLockData(base); - assertTrue(data !== null, 'lock data readable after update'); + assert.ok(data !== null, 'lock data readable after update'); if (data) { - assertEq(data.pid, process.pid, 'lock data has correct PID'); - assertEq(data.unitType, 'execute-task', 'lock data has correct unit type'); - assertEq(data.unitId, 'M001/S01/T01', 'lock data has correct unit ID'); - assertEq(data.completedUnits, 5, 'lock data has correct completed count'); - assertEq(data.sessionFile, '/tmp/session.json', 'lock data has session file'); + assert.deepStrictEqual(data.pid, process.pid, 'lock data has correct PID'); + assert.deepStrictEqual(data.unitType, 'execute-task', 'lock data has correct unit type'); + assert.deepStrictEqual(data.unitId, 'M001/S01/T01', 'lock data has correct unit ID'); + assert.deepStrictEqual(data.completedUnits, 5, 'lock data has correct completed count'); + assert.deepStrictEqual(data.sessionFile, '/tmp/session.json', 'lock data has session file'); } releaseSessionLock(base); @@ -142,7 +142,7 @@ async function main(): Promise { // Should be able to acquire despite the stale lock const result = acquireSessionLock(base); - assertTrue(result.acquired, '#1245: stale lock from dead PID → re-acquirable'); + assert.ok(result.acquired, '#1245: stale lock from dead PID → re-acquirable'); releaseSessionLock(base); } finally { @@ -158,7 +158,7 @@ async function main(): Promise { try { const data = readSessionLockData(base); - assertEq(data, null, 'no lock file → null'); + assert.deepStrictEqual(data, null, 'no lock file → null'); } finally { rmSync(base, { recursive: true, force: true }); } @@ -176,7 +176,7 @@ async function main(): Promise { // Multiple validations should all return true (regression for #1257) for (let i = 0; i < 5; i++) { const valid = validateSessionLock(base); - assertTrue(valid, `#1257: validation ${i + 1} returns true for own lock`); + assert.ok(valid, `#1257: validation ${i + 1} returns true for own lock`); } releaseSessionLock(base); @@ -196,7 +196,7 @@ async function main(): Promise { writeFileSync(lockFile, 'NOT VALID JSON {{{'); const data = readSessionLockData(base); - assertEq(data, null, 'corrupt JSON → null'); + assert.deepStrictEqual(data, null, 'corrupt JSON → null'); } finally { rmSync(base, { recursive: true, force: true }); } @@ -210,9 +210,9 @@ async function main(): Promise { try { const status = getSessionLockStatus(base); - assertEq(status.valid, false, 'missing lock metadata is invalid'); - assertEq(status.failureReason, 'missing-metadata', 'missing metadata reason is surfaced'); - assertEq(status.expectedPid, process.pid, 'expected PID is included'); + assert.deepStrictEqual(status.valid, false, 'missing lock metadata is invalid'); + assert.deepStrictEqual(status.failureReason, 'missing-metadata', 'missing metadata reason is surfaced'); + assert.deepStrictEqual(status.expectedPid, process.pid, 'expected PID is included'); } finally { rmSync(base, { recursive: true, force: true }); } @@ -237,10 +237,10 @@ async function main(): Promise { }, null, 2)); const status = getSessionLockStatus(base); - assertEq(status.valid, false, 'foreign PID lock is invalid'); - assertEq(status.failureReason, 'pid-mismatch', 'PID mismatch reason is surfaced'); - assertEq(status.existingPid, foreignPid, 'existing PID is included'); - assertEq(status.expectedPid, process.pid, 'expected PID is included'); + assert.deepStrictEqual(status.valid, false, 'foreign PID lock is invalid'); + assert.deepStrictEqual(status.failureReason, 'pid-mismatch', 'PID mismatch reason is surfaced'); + assert.deepStrictEqual(status.existingPid, foreignPid, 'existing PID is included'); + assert.deepStrictEqual(status.expectedPid, process.pid, 'expected PID is included'); } finally { rmSync(base, { recursive: true, force: true }); } @@ -254,11 +254,11 @@ async function main(): Promise { try { const r1 = acquireSessionLock(base); - assertTrue(r1.acquired, 'first acquisition'); + assert.ok(r1.acquired, 'first acquisition'); releaseSessionLock(base); const r2 = acquireSessionLock(base); - assertTrue(r2.acquired, 're-acquisition after release'); + assert.ok(r2.acquired, 're-acquisition after release'); releaseSessionLock(base); } finally { rmSync(base, { recursive: true, force: true }); @@ -273,13 +273,13 @@ async function main(): Promise { try { const r1 = acquireSessionLock(base); - assertTrue(r1.acquired, 'first acquisition succeeds'); + assert.ok(r1.acquired, 'first acquisition succeeds'); const r2 = acquireSessionLock(base); - assertTrue(r2.acquired, 're-entrant acquisition succeeds'); + assert.ok(r2.acquired, 're-entrant acquisition succeeds'); const valid = validateSessionLock(base); - assertTrue(valid, 're-entrant acquisition does not corrupt validation state'); + assert.ok(valid, 're-entrant acquisition does not corrupt validation state'); releaseSessionLock(base); } finally { @@ -295,31 +295,24 @@ async function main(): Promise { try { const r1 = acquireSessionLock(base); - assertTrue(r1.acquired, 'first acquisition succeeds'); + assert.ok(r1.acquired, 'first acquisition succeeds'); const lockDir = gsdRoot(base) + '.lock'; if (properLockfileAvailable) { - assertTrue(existsSync(lockDir), '.gsd.lock/ exists after first acquisition'); + assert.ok(existsSync(lockDir), '.gsd.lock/ exists after first acquisition'); } const r2 = acquireSessionLock(base); - assertTrue(r2.acquired, 'second acquisition succeeds'); + assert.ok(r2.acquired, 'second acquisition succeeds'); if (properLockfileAvailable) { - assertTrue(existsSync(lockDir), '.gsd.lock/ exists after re-entrant acquisition'); + assert.ok(existsSync(lockDir), '.gsd.lock/ exists after re-entrant acquisition'); } - assertTrue(validateSessionLock(base), 'lock remains valid after re-entrant acquisition'); + assert.ok(validateSessionLock(base), 'lock remains valid after re-entrant acquisition'); releaseSessionLock(base); - assertTrue(!existsSync(lockDir), '.gsd.lock/ is removed after release'); + assert.ok(!existsSync(lockDir), '.gsd.lock/ is removed after release'); } finally { rmSync(base, { recursive: true, force: true }); } } - - report(); -} - -main().catch((error) => { - console.error(error); - process.exit(1); }); diff --git a/src/resources/extensions/gsd/tests/shared-wal.test.ts b/src/resources/extensions/gsd/tests/shared-wal.test.ts index d4f3cb2cc..6fb425854 100644 --- a/src/resources/extensions/gsd/tests/shared-wal.test.ts +++ b/src/resources/extensions/gsd/tests/shared-wal.test.ts @@ -14,9 +14,9 @@ import { getAllMilestones, _getAdapter, } from '../gsd-db.ts'; -import { createTestContext } from './test-helpers.ts'; +import { describe, test } from 'node:test'; +import assert from 'node:assert/strict'; -const { assertEq, assertTrue, report } = createTestContext(); // ─── Helpers ────────────────────────────────────────────────────────────── @@ -30,14 +30,14 @@ function cleanup(dir: string): void { // ─── Tests ──────────────────────────────────────────────────────────────── -async function main() { +describe('shared-wal', async () => { // ─── Test (a): resolveProjectRootDbPath returns project root DB for worktree path ─── console.log('\n=== shared-wal: resolve worktree path to project root DB ==='); { const projectRoot = '/home/user/myproject'; const worktreePath = join(projectRoot, '.gsd', 'worktrees', 'M001'); const result = resolveProjectRootDbPath(worktreePath); - assertEq(result, join(projectRoot, '.gsd', 'gsd.db'), + assert.deepStrictEqual(result, join(projectRoot, '.gsd', 'gsd.db'), 'worktree path resolves to project root DB'); } @@ -46,7 +46,7 @@ async function main() { { const projectRoot = '/home/user/myproject'; const result = resolveProjectRootDbPath(projectRoot); - assertEq(result, join(projectRoot, '.gsd', 'gsd.db'), + assert.deepStrictEqual(result, join(projectRoot, '.gsd', 'gsd.db'), 'project root path stays at project root DB'); } @@ -56,7 +56,7 @@ async function main() { const projectRoot = '/home/user/myproject'; const nestedPath = join(projectRoot, '.gsd', 'worktrees', 'M002', 'src', 'lib'); const result = resolveProjectRootDbPath(nestedPath); - assertEq(result, join(projectRoot, '.gsd', 'gsd.db'), + assert.deepStrictEqual(result, join(projectRoot, '.gsd', 'gsd.db'), 'nested worktree subdir resolves to project root DB'); } @@ -64,7 +64,7 @@ async function main() { console.log('\n=== shared-wal: resolve forward-slash path ==='); { const result = resolveProjectRootDbPath('/proj/.gsd/worktrees/M001'); - assertEq(result, join('/proj', '.gsd', 'gsd.db'), + assert.deepStrictEqual(result, join('/proj', '.gsd', 'gsd.db'), 'forward-slash worktree path resolves correctly'); } @@ -99,9 +99,9 @@ async function main() { // Verify all 3 milestones are visible const all = getAllMilestones(); - assertEq(all.length, 3, 'concurrent: all 3 milestones visible'); + assert.deepStrictEqual(all.length, 3, 'concurrent: all 3 milestones visible'); const ids = all.map(m => m.id).sort(); - assertEq(ids, ['M001', 'M002', 'M003'], 'concurrent: correct IDs'); + assert.deepStrictEqual(ids, ['M001', 'M002', 'M003'], 'concurrent: correct IDs'); closeDatabase(); } finally { @@ -132,7 +132,7 @@ async function main() { // Connection 2: write M002, verify sees M001 openDatabase(dbPath); const afterConn2Before = getAllMilestones(); - assertTrue(afterConn2Before.some(m => m.id === 'M001'), + assert.ok(afterConn2Before.some(m => m.id === 'M001'), 'rawconc: conn2 sees M001 from conn1'); insertMilestone({ id: 'M002', title: 'Writer 2', status: 'active' }); closeDatabase(); @@ -140,16 +140,16 @@ async function main() { // Connection 3: write M003, verify sees M001 + M002 openDatabase(dbPath); const afterConn3Before = getAllMilestones(); - assertTrue(afterConn3Before.some(m => m.id === 'M001'), + assert.ok(afterConn3Before.some(m => m.id === 'M001'), 'rawconc: conn3 sees M001'); - assertTrue(afterConn3Before.some(m => m.id === 'M002'), + assert.ok(afterConn3Before.some(m => m.id === 'M002'), 'rawconc: conn3 sees M002'); insertMilestone({ id: 'M003', title: 'Writer 3', status: 'active' }); // Final read: all 3 visible const finalAll = getAllMilestones(); - assertEq(finalAll.length, 3, 'rawconc: all 3 milestones visible'); - assertEq( + assert.deepStrictEqual(finalAll.length, 3, 'rawconc: all 3 milestones visible'); + assert.deepStrictEqual( finalAll.map(m => m.id).sort(), ['M001', 'M002', 'M003'], 'rawconc: all IDs present', @@ -177,7 +177,7 @@ async function main() { // Verify it committed const all = getAllMilestones(); - assertEq(all.length, 1, 'busy: M001 committed via transaction'); + assert.deepStrictEqual(all.length, 1, 'busy: M001 committed via transaction'); // Verify transaction rolls back on error let errorCaught = false; @@ -188,17 +188,17 @@ async function main() { }); } catch (err) { errorCaught = true; - assertTrue( + assert.ok( (err as Error).message.includes('Simulated failure'), 'busy: error propagated from transaction', ); } - assertTrue(errorCaught, 'busy: transaction threw on error'); + assert.ok(errorCaught, 'busy: transaction threw on error'); // M002 should NOT be visible (rolled back) const afterRollback = getAllMilestones(); - assertEq(afterRollback.length, 1, 'busy: M002 rolled back — still only 1 milestone'); - assertEq(afterRollback[0]!.id, 'M001', 'busy: only M001 survives'); + assert.deepStrictEqual(afterRollback.length, 1, 'busy: M002 rolled back — still only 1 milestone'); + assert.deepStrictEqual(afterRollback[0]!.id, 'M001', 'busy: only M001 survives'); closeDatabase(); } finally { @@ -206,11 +206,4 @@ async function main() { cleanup(tmp); } } - - report(); -} - -main().catch((error) => { - console.error(error); - process.exit(1); }); diff --git a/src/resources/extensions/gsd/tests/stalled-tool-recovery.test.ts b/src/resources/extensions/gsd/tests/stalled-tool-recovery.test.ts index 7d46c1128..bbdaa68ad 100644 --- a/src/resources/extensions/gsd/tests/stalled-tool-recovery.test.ts +++ b/src/resources/extensions/gsd/tests/stalled-tool-recovery.test.ts @@ -19,9 +19,9 @@ import { mkdtempSync, mkdirSync, rmSync } from "node:fs"; import { join } from "node:path"; import { tmpdir } from "node:os"; import { recoverTimedOutUnit, type RecoveryContext } from "../auto-timeout-recovery.ts"; -import { createTestContext } from './test-helpers.ts'; +import { test } from 'node:test'; +import assert from 'node:assert/strict'; -const { assertTrue, report } = createTestContext(); // Minimal mock for ExtensionContext — only the fields recoverTimedOutUnit touches. function makeMockCtx() { @@ -55,12 +55,12 @@ function makeMockPi() { await recoverTimedOutUnit(ctx, pi, "execute-task", "M001/S01/T01", "idle", emptyRctx); } catch (err: any) { crashed = true; - assertTrue( + assert.ok( err.message.includes("path") || err.message.includes("string") || err.code === "ERR_INVALID_ARG_TYPE", `should crash with path/type error, got: ${err.message}`, ); } - assertTrue(crashed, "should crash when basePath is undefined (reproduces #1855)"); + assert.ok(crashed, "should crash when basePath is undefined (reproduces #1855)"); } // ═══ #1855: valid RecoveryContext does not crash ═════════════════════════════ @@ -90,13 +90,11 @@ function makeMockPi() { crashed = true; console.error(` Unexpected crash: ${err.message}`); } - assertTrue(!crashed, "should not crash with valid basePath"); + assert.ok(!crashed, "should not crash with valid basePath"); // With no runtime record on disk and recoveryAttempts=0, the function // should attempt steering recovery (sendMessage) and return "recovered". - assertTrue(result === "recovered", `should return 'recovered', got '${result}'`); + assert.ok(result === "recovered", `should return 'recovered', got '${result}'`); } finally { rmSync(base, { recursive: true, force: true }); } } - -report(); diff --git a/src/resources/extensions/gsd/tests/symlink-numbered-variants.test.ts b/src/resources/extensions/gsd/tests/symlink-numbered-variants.test.ts index ed14dfb47..5a332dd6c 100644 --- a/src/resources/extensions/gsd/tests/symlink-numbered-variants.test.ts +++ b/src/resources/extensions/gsd/tests/symlink-numbered-variants.test.ts @@ -23,15 +23,15 @@ import { tmpdir } from "node:os"; import { execSync } from "node:child_process"; import { ensureGsdSymlink, externalGsdRoot } from "../repo-identity.ts"; -import { createTestContext } from "./test-helpers.ts"; +import { describe, test } from 'node:test'; +import assert from 'node:assert/strict'; -const { assertEq, assertTrue, report } = createTestContext(); function run(command: string, cwd: string): string { return execSync(command, { cwd, stdio: ["ignore", "pipe", "pipe"], encoding: "utf-8" }).trim(); } -async function main(): Promise { +describe('symlink-numbered-variants', async () => { const base = realpathSync(mkdtempSync(join(tmpdir(), "gsd-symlink-variants-"))); const stateDir = realpathSync(mkdtempSync(join(tmpdir(), "gsd-state-variants-"))); @@ -58,14 +58,14 @@ async function main(): Promise { mkdirSync(join(base, ".gsd 4"), { recursive: true }); const result = ensureGsdSymlink(base); - assertEq(result, externalPath, "ensureGsdSymlink returns external path"); - assertTrue(existsSync(join(base, ".gsd")), ".gsd exists after ensureGsdSymlink"); - assertTrue(lstatSync(join(base, ".gsd")).isSymbolicLink(), ".gsd is a symlink"); + assert.deepStrictEqual(result, externalPath, "ensureGsdSymlink returns external path"); + assert.ok(existsSync(join(base, ".gsd")), ".gsd exists after ensureGsdSymlink"); + assert.ok(lstatSync(join(base, ".gsd")).isSymbolicLink(), ".gsd is a symlink"); // The numbered variants must have been removed - assertTrue(!existsSync(join(base, ".gsd 2")), '".gsd 2" directory was cleaned up'); - assertTrue(!existsSync(join(base, ".gsd 3")), '".gsd 3" directory was cleaned up'); - assertTrue(!existsSync(join(base, ".gsd 4")), '".gsd 4" directory was cleaned up'); + assert.ok(!existsSync(join(base, ".gsd 2")), '".gsd 2" directory was cleaned up'); + assert.ok(!existsSync(join(base, ".gsd 3")), '".gsd 3" directory was cleaned up'); + assert.ok(!existsSync(join(base, ".gsd 4")), '".gsd 4" directory was cleaned up'); } // ── Test: numbered variant symlinks are cleaned up ───────────────── @@ -82,12 +82,12 @@ async function main(): Promise { symlinkSync(staleTarget, join(base, ".gsd 3"), "junction"); const result = ensureGsdSymlink(base); - assertEq(result, externalPath, "ensureGsdSymlink returns external path when variants exist"); - assertTrue(existsSync(join(base, ".gsd")), ".gsd exists"); - assertTrue(lstatSync(join(base, ".gsd")).isSymbolicLink(), ".gsd is a symlink"); + assert.deepStrictEqual(result, externalPath, "ensureGsdSymlink returns external path when variants exist"); + assert.ok(existsSync(join(base, ".gsd")), ".gsd exists"); + assert.ok(lstatSync(join(base, ".gsd")).isSymbolicLink(), ".gsd is a symlink"); - assertTrue(!existsSync(join(base, ".gsd 2")), '".gsd 2" symlink variant was cleaned up'); - assertTrue(!existsSync(join(base, ".gsd 3")), '".gsd 3" symlink variant was cleaned up'); + assert.ok(!existsSync(join(base, ".gsd 2")), '".gsd 2" symlink variant was cleaned up'); + assert.ok(!existsSync(join(base, ".gsd 3")), '".gsd 3" symlink variant was cleaned up'); } // ── Test: real .gsd directory blocks symlink, but variants still cleaned ── @@ -104,12 +104,12 @@ async function main(): Promise { const result = ensureGsdSymlink(base); // When .gsd is a real directory, ensureGsdSymlink preserves it - assertEq(result, join(base, ".gsd"), "real .gsd directory preserved"); - assertTrue(lstatSync(join(base, ".gsd")).isDirectory(), ".gsd remains a directory"); + assert.deepStrictEqual(result, join(base, ".gsd"), "real .gsd directory preserved"); + assert.ok(lstatSync(join(base, ".gsd")).isDirectory(), ".gsd remains a directory"); // But the numbered variants should still be cleaned up - assertTrue(!existsSync(join(base, ".gsd 2")), '".gsd 2" cleaned even when .gsd is a directory'); - assertTrue(!existsSync(join(base, ".gsd 3")), '".gsd 3" cleaned even when .gsd is a directory'); + assert.ok(!existsSync(join(base, ".gsd 2")), '".gsd 2" cleaned even when .gsd is a directory'); + assert.ok(!existsSync(join(base, ".gsd 3")), '".gsd 3" cleaned even when .gsd is a directory'); } // ── Test: only numeric-suffixed variants are removed ─────────────── @@ -127,10 +127,10 @@ async function main(): Promise { ensureGsdSymlink(base); - assertTrue(existsSync(join(base, ".gsd-backup")), ".gsd-backup is NOT removed"); - assertTrue(existsSync(join(base, ".gsd_old")), ".gsd_old is NOT removed"); - assertTrue(!existsSync(join(base, ".gsd 2")), '".gsd 2" removed'); - assertTrue(!existsSync(join(base, ".gsd 10")), '".gsd 10" removed'); + assert.ok(existsSync(join(base, ".gsd-backup")), ".gsd-backup is NOT removed"); + assert.ok(existsSync(join(base, ".gsd_old")), ".gsd_old is NOT removed"); + assert.ok(!existsSync(join(base, ".gsd 2")), '".gsd 2" removed'); + assert.ok(!existsSync(join(base, ".gsd 10")), '".gsd 10" removed'); // Cleanup non-variant dirs rmSync(join(base, ".gsd-backup"), { recursive: true, force: true }); @@ -141,11 +141,5 @@ async function main(): Promise { delete process.env.GSD_STATE_DIR; try { rmSync(base, { recursive: true, force: true }); } catch { /* ignore */ } try { rmSync(stateDir, { recursive: true, force: true }); } catch { /* ignore */ } - report(); } -} - -main().catch((error) => { - console.error(error); - process.exit(1); }); diff --git a/src/resources/extensions/gsd/tests/token-savings.test.ts b/src/resources/extensions/gsd/tests/token-savings.test.ts index 517ac7f9a..a8bf5e669 100644 --- a/src/resources/extensions/gsd/tests/token-savings.test.ts +++ b/src/resources/extensions/gsd/tests/token-savings.test.ts @@ -18,9 +18,9 @@ import { formatDecisionsForPrompt, formatRequirementsForPrompt, } from '../context-store.ts'; -import { createTestContext } from './test-helpers.ts'; +import { test } from 'node:test'; +import assert from 'node:assert/strict'; -const { assertEq, assertTrue, assertMatch, assertNoMatch, report } = createTestContext(); // ─── Fixture Generators ──────────────────────────────────────────────────── @@ -154,8 +154,8 @@ console.log('\n=== token-savings: plan-slice prompt ≥30% character savings === openDatabase(':memory:'); const result = migrateFromMarkdown(base); - assertTrue(result.decisions === DECISIONS_COUNT, `imported ${result.decisions} decisions, expected ${DECISIONS_COUNT}`); - assertTrue(result.requirements === REQUIREMENTS_COUNT, `imported ${result.requirements} requirements, expected ${REQUIREMENTS_COUNT}`); + assert.ok(result.decisions === DECISIONS_COUNT, `imported ${result.decisions} decisions, expected ${DECISIONS_COUNT}`); + assert.ok(result.requirements === REQUIREMENTS_COUNT, `imported ${result.requirements} requirements, expected ${REQUIREMENTS_COUNT}`); // ── DB-scoped content for plan-slice (M001 decisions + S01 requirements) ── const scopedDecisions = queryDecisions({ milestoneId: 'M001' }); @@ -174,31 +174,31 @@ console.log('\n=== token-savings: plan-slice prompt ≥30% character savings === const savingsPercent = ((fullTotal - dbTotal) / fullTotal) * 100; console.log(` Plan-slice savings: ${savingsPercent.toFixed(1)}% (DB: ${dbTotal} chars, full: ${fullTotal} chars)`); - assertTrue(dbTotal > 0, 'DB-scoped content is non-empty'); - assertTrue(dbDecisionsContent.length > 0, 'DB-scoped decisions content is non-empty'); - assertTrue(dbRequirementsContent.length > 0, 'DB-scoped requirements content is non-empty'); - assertTrue(savingsPercent >= 30, `plan-slice savings ≥30% (actual: ${savingsPercent.toFixed(1)}%)`); - assertTrue(dbTotal < fullTotal * 0.70, `DB total (${dbTotal}) < 70% of full total (${fullTotal})`); + assert.ok(dbTotal > 0, 'DB-scoped content is non-empty'); + assert.ok(dbDecisionsContent.length > 0, 'DB-scoped decisions content is non-empty'); + assert.ok(dbRequirementsContent.length > 0, 'DB-scoped requirements content is non-empty'); + assert.ok(savingsPercent >= 30, `plan-slice savings ≥30% (actual: ${savingsPercent.toFixed(1)}%)`); + assert.ok(dbTotal < fullTotal * 0.70, `DB total (${dbTotal}) < 70% of full total (${fullTotal})`); // ── Verify correct scoping: decisions ── // M001 decisions: those with when_context containing 'M001' — indices 1,4,7,10,13,16,19,22 // (24 decisions round-robin across M001/M002/M003 → 8 for M001) - assertTrue(scopedDecisions.length === 8, `M001 decisions: expected 8, got ${scopedDecisions.length}`); + assert.ok(scopedDecisions.length === 8, `M001 decisions: expected 8, got ${scopedDecisions.length}`); for (const d of scopedDecisions) { - assertTrue(d.when_context.includes('M001'), `decision ${d.id} should have M001 in when_context, got "${d.when_context}"`); + assert.ok(d.when_context.includes('M001'), `decision ${d.id} should have M001 in when_context, got "${d.when_context}"`); } // Verify NO decisions from other milestones leak in for (const d of scopedDecisions) { - assertNoMatch(d.when_context, /M002|M003/, `decision ${d.id} should not contain M002 or M003`); + assert.doesNotMatch(d.when_context, /M002|M003/, `decision ${d.id} should not contain M002 or M003`); } // ── Verify correct scoping: requirements ── // S01 requirements: those assigned to S01 as primary_owner // S01 appears in positions 1,6,11,16,21 (5 assignments cycling, 21 reqs → indices 0,5,10,15,20) - assertTrue(scopedRequirements.length > 0, 'S01 requirements non-empty'); + assert.ok(scopedRequirements.length > 0, 'S01 requirements non-empty'); for (const r of scopedRequirements) { - assertTrue( + assert.ok( r.primary_owner.includes('S01') || r.supporting_slices.includes('S01'), `requirement ${r.id} should be owned by or support S01`, ); @@ -206,13 +206,13 @@ console.log('\n=== token-savings: plan-slice prompt ≥30% character savings === // Verify specific expected IDs are present const scopedDecisionIds = scopedDecisions.map(d => d.id); - assertTrue(scopedDecisionIds.includes('D001'), 'M001 scoped decisions includes D001'); - assertTrue(scopedDecisionIds.includes('D004'), 'M001 scoped decisions includes D004'); - assertTrue(!scopedDecisionIds.includes('D002'), 'M001 scoped decisions excludes D002 (M002)'); - assertTrue(!scopedDecisionIds.includes('D003'), 'M001 scoped decisions excludes D003 (M003)'); + assert.ok(scopedDecisionIds.includes('D001'), 'M001 scoped decisions includes D001'); + assert.ok(scopedDecisionIds.includes('D004'), 'M001 scoped decisions includes D004'); + assert.ok(!scopedDecisionIds.includes('D002'), 'M001 scoped decisions excludes D002 (M002)'); + assert.ok(!scopedDecisionIds.includes('D003'), 'M001 scoped decisions excludes D003 (M003)'); const scopedReqIds = scopedRequirements.map(r => r.id); - assertTrue(scopedReqIds.includes('R001'), 'S01 scoped requirements includes R001'); + assert.ok(scopedReqIds.includes('R001'), 'S01 scoped requirements includes R001'); closeDatabase(); rmSync(base, { recursive: true, force: true }); @@ -246,9 +246,9 @@ console.log('\n=== token-savings: research-milestone prompt shows meaningful sav const decisionsSavings = ((fullDecisionsContent.length - dbDecisionsContent.length) / fullDecisionsContent.length) * 100; console.log(` Decisions savings (M001): ${decisionsSavings.toFixed(1)}% (DB: ${dbDecisionsContent.length}, full: ${fullDecisionsContent.length})`); - assertTrue(decisionsSavings > 0, `decisions savings > 0% (actual: ${decisionsSavings.toFixed(1)}%)`); - assertTrue(scopedDecisions.length === 8, `M001 decisions: 8 of 24 total`); - assertTrue(allRequirements.length === REQUIREMENTS_COUNT, `all requirements returned: ${allRequirements.length}`); + assert.ok(decisionsSavings > 0, `decisions savings > 0% (actual: ${decisionsSavings.toFixed(1)}%)`); + assert.ok(scopedDecisions.length === 8, `M001 decisions: 8 of 24 total`); + assert.ok(allRequirements.length === REQUIREMENTS_COUNT, `all requirements returned: ${allRequirements.length}`); // Requirements: DB-formatted vs raw markdown — formatted output may differ in size // but decisions savings alone should make the composite meaningful @@ -259,8 +259,8 @@ console.log('\n=== token-savings: research-milestone prompt shows meaningful sav // With 8/24 decisions = 66% reduction in decisions, even if requirements are equal, // the composite should show meaningful savings - assertTrue(compositeSavings > 10, `research-milestone shows >10% composite savings (actual: ${compositeSavings.toFixed(1)}%)`); - assertTrue(decisionsSavings >= 30, `decisions-only savings ≥30% for M001 scope (actual: ${decisionsSavings.toFixed(1)}%)`); + assert.ok(compositeSavings > 10, `research-milestone shows >10% composite savings (actual: ${compositeSavings.toFixed(1)}%)`); + assert.ok(decisionsSavings >= 30, `decisions-only savings ≥30% for M001 scope (actual: ${decisionsSavings.toFixed(1)}%)`); closeDatabase(); rmSync(base, { recursive: true, force: true }); @@ -283,17 +283,17 @@ console.log('\n=== token-savings: quality — correct scoping, no cross-contamin // ── M002-scoped decisions should not contain M001/M003 items ── const m002Decisions = queryDecisions({ milestoneId: 'M002' }); - assertTrue(m002Decisions.length === 8, `M002 decisions: expected 8, got ${m002Decisions.length}`); + assert.ok(m002Decisions.length === 8, `M002 decisions: expected 8, got ${m002Decisions.length}`); for (const d of m002Decisions) { - assertTrue(d.when_context.includes('M002'), `M002 decision ${d.id} has M002 in when_context`); - assertNoMatch(d.when_context, /M001|M003/, `M002 decision ${d.id} should not contain M001/M003`); + assert.ok(d.when_context.includes('M002'), `M002 decision ${d.id} has M002 in when_context`); + assert.doesNotMatch(d.when_context, /M001|M003/, `M002 decision ${d.id} should not contain M001/M003`); } // ── S04-scoped requirements should only include S04-related items ── const s04Requirements = queryRequirements({ sliceId: 'S04' }); - assertTrue(s04Requirements.length > 0, 'S04 requirements non-empty'); + assert.ok(s04Requirements.length > 0, 'S04 requirements non-empty'); for (const r of s04Requirements) { - assertTrue( + assert.ok( r.primary_owner.includes('S04') || r.supporting_slices.includes('S04'), `S04 requirement ${r.id} should be owned by or support S04`, ); @@ -301,13 +301,13 @@ console.log('\n=== token-savings: quality — correct scoping, no cross-contamin // ── Verify formatted output is well-formed and non-empty ── const formattedDecisions = formatDecisionsForPrompt(m002Decisions); - assertTrue(formattedDecisions.length > 0, 'formatted M002 decisions is non-empty'); - assertMatch(formattedDecisions, /\| D/, 'formatted decisions contains decision rows'); - assertMatch(formattedDecisions, /\| # \|/, 'formatted decisions has table header'); + assert.ok(formattedDecisions.length > 0, 'formatted M002 decisions is non-empty'); + assert.match(formattedDecisions, /\| D/, 'formatted decisions contains decision rows'); + assert.match(formattedDecisions, /\| # \|/, 'formatted decisions has table header'); const formattedReqs = formatRequirementsForPrompt(s04Requirements); - assertTrue(formattedReqs.length > 0, 'formatted S04 requirements is non-empty'); - assertMatch(formattedReqs, /### R\d+/, 'formatted requirements has requirement headings'); + assert.ok(formattedReqs.length > 0, 'formatted S04 requirements is non-empty'); + assert.match(formattedReqs, /### R\d+/, 'formatted requirements has requirement headings'); // ── Verify all milestones have decisions and counts add up ── const m001Count = queryDecisions({ milestoneId: 'M001' }).length; @@ -315,11 +315,11 @@ console.log('\n=== token-savings: quality — correct scoping, no cross-contamin const m003Count = queryDecisions({ milestoneId: 'M003' }).length; const allCount = queryDecisions().length; - assertTrue(m001Count === 8, `M001: 8 decisions (got ${m001Count})`); - assertTrue(m002Count === 8, `M002: 8 decisions (got ${m002Count})`); - assertTrue(m003Count === 8, `M003: 8 decisions (got ${m003Count})`); - assertTrue(allCount === DECISIONS_COUNT, `all: ${DECISIONS_COUNT} decisions (got ${allCount})`); - assertTrue(m001Count + m002Count + m003Count === allCount, 'milestone decision counts sum to total'); + assert.ok(m001Count === 8, `M001: 8 decisions (got ${m001Count})`); + assert.ok(m002Count === 8, `M002: 8 decisions (got ${m002Count})`); + assert.ok(m003Count === 8, `M003: 8 decisions (got ${m003Count})`); + assert.ok(allCount === DECISIONS_COUNT, `all: ${DECISIONS_COUNT} decisions (got ${allCount})`); + assert.ok(m001Count + m002Count + m003Count === allCount, 'milestone decision counts sum to total'); // ── Verify all slices have requirements ── const s01Reqs = queryRequirements({ sliceId: 'S01' }); @@ -328,11 +328,11 @@ console.log('\n=== token-savings: quality — correct scoping, no cross-contamin const s04Reqs = queryRequirements({ sliceId: 'S04' }); const s05Reqs = queryRequirements({ sliceId: 'S05' }); - assertTrue(s01Reqs.length > 0, 'S01 has requirements'); - assertTrue(s02Reqs.length > 0, 'S02 has requirements'); - assertTrue(s03Reqs.length > 0, 'S03 has requirements'); - assertTrue(s04Reqs.length > 0, 'S04 has requirements'); - assertTrue(s05Reqs.length > 0, 'S05 has requirements'); + assert.ok(s01Reqs.length > 0, 'S01 has requirements'); + assert.ok(s02Reqs.length > 0, 'S02 has requirements'); + assert.ok(s03Reqs.length > 0, 'S03 has requirements'); + assert.ok(s04Reqs.length > 0, 'S04 has requirements'); + assert.ok(s05Reqs.length > 0, 'S05 has requirements'); closeDatabase(); rmSync(base, { recursive: true, force: true }); @@ -345,22 +345,20 @@ console.log('\n=== token-savings: quality — correct scoping, no cross-contamin console.log('\n=== token-savings: fixture data realism ==='); { // Verify fixture generators produce sufficient volume - assertTrue(DECISIONS_COUNT >= 20, `decisions count ≥ 20 (actual: ${DECISIONS_COUNT})`); - assertTrue(REQUIREMENTS_COUNT >= 20, `requirements count ≥ 20 (actual: ${REQUIREMENTS_COUNT})`); - assertTrue(MILESTONES.length >= 3, `milestones ≥ 3 (actual: ${MILESTONES.length})`); - assertTrue(SLICE_ASSIGNMENTS.length >= 5, `slice assignments ≥ 5 (actual: ${SLICE_ASSIGNMENTS.length})`); + assert.ok(DECISIONS_COUNT >= 20, `decisions count ≥ 20 (actual: ${DECISIONS_COUNT})`); + assert.ok(REQUIREMENTS_COUNT >= 20, `requirements count ≥ 20 (actual: ${REQUIREMENTS_COUNT})`); + assert.ok(MILESTONES.length >= 3, `milestones ≥ 3 (actual: ${MILESTONES.length})`); + assert.ok(SLICE_ASSIGNMENTS.length >= 5, `slice assignments ≥ 5 (actual: ${SLICE_ASSIGNMENTS.length})`); // Verify markdown content is substantial - assertTrue(decisionsMarkdown.length > 1000, `decisions markdown > 1000 chars (actual: ${decisionsMarkdown.length})`); - assertTrue(requirementsMarkdown.length > 1000, `requirements markdown > 1000 chars (actual: ${requirementsMarkdown.length})`); + assert.ok(decisionsMarkdown.length > 1000, `decisions markdown > 1000 chars (actual: ${decisionsMarkdown.length})`); + assert.ok(requirementsMarkdown.length > 1000, `requirements markdown > 1000 chars (actual: ${requirementsMarkdown.length})`); // Verify content structure - assertMatch(decisionsMarkdown, /\| D001 \|/, 'decisions markdown has D001'); - assertMatch(decisionsMarkdown, /\| D024 \|/, 'decisions markdown has D024'); - assertMatch(requirementsMarkdown, /### R001/, 'requirements markdown has R001'); - assertMatch(requirementsMarkdown, /### R021/, 'requirements markdown has R021'); + assert.match(decisionsMarkdown, /\| D001 \|/, 'decisions markdown has D001'); + assert.match(decisionsMarkdown, /\| D024 \|/, 'decisions markdown has D024'); + assert.match(requirementsMarkdown, /### R001/, 'requirements markdown has R001'); + assert.match(requirementsMarkdown, /### R021/, 'requirements markdown has R021'); } // ─── Report ──────────────────────────────────────────────────────────────── - -report(); diff --git a/src/resources/extensions/gsd/tests/tool-call-loop-guard.test.ts b/src/resources/extensions/gsd/tests/tool-call-loop-guard.test.ts index fbe3e0670..c1fcecd2c 100644 --- a/src/resources/extensions/gsd/tests/tool-call-loop-guard.test.ts +++ b/src/resources/extensions/gsd/tests/tool-call-loop-guard.test.ts @@ -3,7 +3,8 @@ // Verifies that identical consecutive tool calls are detected and blocked // after exceeding the threshold, and that the guard resets properly. -import { createTestContext } from './test-helpers.ts'; +import { test } from 'node:test'; +import assert from 'node:assert/strict'; import { checkToolCallLoop, resetToolCallLoopGuard, @@ -11,7 +12,6 @@ import { getToolCallLoopCount, } from '../bootstrap/tool-call-loop-guard.ts'; -const { assertEq, assertTrue, report } = createTestContext(); // ═══════════════════════════════════════════════════════════════════════════ // Allows first N calls, blocks after threshold @@ -25,15 +25,15 @@ console.log('\n── Loop guard: blocks after threshold ──'); // First 4 identical calls should be allowed (threshold is 4) for (let i = 1; i <= 4; i++) { const result = checkToolCallLoop('web_search', { query: 'same query' }); - assertTrue(result.block === false, `Call ${i} should be allowed`); - assertEq(result.count, i, `Count should be ${i} after call ${i}`); + assert.ok(result.block === false, `Call ${i} should be allowed`); + assert.deepStrictEqual(result.count, i, `Count should be ${i} after call ${i}`); } // 5th identical call should be blocked const blocked = checkToolCallLoop('web_search', { query: 'same query' }); - assertTrue(blocked.block === true, '5th identical call should be blocked'); - assertTrue(blocked.reason!.includes('web_search'), 'Reason should mention tool name'); - assertTrue(blocked.reason!.includes('5'), 'Reason should mention count'); + assert.ok(blocked.block === true, '5th identical call should be blocked'); + assert.ok(blocked.reason!.includes('web_search'), 'Reason should mention tool name'); + assert.ok(blocked.reason!.includes('5'), 'Reason should mention count'); } // ═══════════════════════════════════════════════════════════════════════════ @@ -48,17 +48,17 @@ console.log('\n── Loop guard: different calls reset streak ──'); checkToolCallLoop('web_search', { query: 'query A' }); checkToolCallLoop('web_search', { query: 'query A' }); checkToolCallLoop('web_search', { query: 'query A' }); - assertEq(getToolCallLoopCount(), 3, 'Count should be 3 after 3 identical calls'); + assert.deepStrictEqual(getToolCallLoopCount(), 3, 'Count should be 3 after 3 identical calls'); // A different call resets the streak const different = checkToolCallLoop('bash', { command: 'ls' }); - assertTrue(different.block === false, 'Different tool call should be allowed'); - assertEq(getToolCallLoopCount(), 1, 'Count should reset to 1 after different call'); + assert.ok(different.block === false, 'Different tool call should be allowed'); + assert.deepStrictEqual(getToolCallLoopCount(), 1, 'Count should reset to 1 after different call'); // Same tool but different args also resets checkToolCallLoop('web_search', { query: 'query A' }); checkToolCallLoop('web_search', { query: 'query B' }); // different args - assertEq(getToolCallLoopCount(), 1, 'Different args should reset count'); + assert.deepStrictEqual(getToolCallLoopCount(), 1, 'Different args should reset count'); } // ═══════════════════════════════════════════════════════════════════════════ @@ -72,15 +72,15 @@ console.log('\n── Loop guard: reset clears state ──'); checkToolCallLoop('web_search', { query: 'q' }); checkToolCallLoop('web_search', { query: 'q' }); checkToolCallLoop('web_search', { query: 'q' }); - assertEq(getToolCallLoopCount(), 3, 'Count should be 3 before reset'); + assert.deepStrictEqual(getToolCallLoopCount(), 3, 'Count should be 3 before reset'); resetToolCallLoopGuard(); - assertEq(getToolCallLoopCount(), 0, 'Count should be 0 after reset'); + assert.deepStrictEqual(getToolCallLoopCount(), 0, 'Count should be 0 after reset'); // After reset, the same call starts fresh const result = checkToolCallLoop('web_search', { query: 'q' }); - assertTrue(result.block === false, 'Call after reset should be allowed'); - assertEq(getToolCallLoopCount(), 1, 'Count should be 1 after first call post-reset'); + assert.ok(result.block === false, 'Call after reset should be allowed'); + assert.deepStrictEqual(getToolCallLoopCount(), 1, 'Count should be 1 after first call post-reset'); } // ═══════════════════════════════════════════════════════════════════════════ @@ -94,13 +94,13 @@ console.log('\n── Loop guard: disable allows everything ──'); for (let i = 0; i < 10; i++) { const result = checkToolCallLoop('web_search', { query: 'same' }); - assertTrue(result.block === false, `Call ${i + 1} should be allowed when disabled`); + assert.ok(result.block === false, `Call ${i + 1} should be allowed when disabled`); } // Re-enable via reset resetToolCallLoopGuard(); checkToolCallLoop('web_search', { query: 'q' }); - assertEq(getToolCallLoopCount(), 1, 'Guard should be active again after reset'); + assert.deepStrictEqual(getToolCallLoopCount(), 1, 'Guard should be active again after reset'); } // ═══════════════════════════════════════════════════════════════════════════ @@ -114,8 +114,8 @@ console.log('\n── Loop guard: arg order is normalized ──'); checkToolCallLoop('web_search', { query: 'test', limit: 5 }); const result = checkToolCallLoop('web_search', { limit: 5, query: 'test' }); // same args, different order - assertTrue(result.block === false, 'Same args in different order should count as consecutive'); - assertEq(getToolCallLoopCount(), 2, 'Should detect as same call regardless of key order'); + assert.ok(result.block === false, 'Same args in different order should count as consecutive'); + assert.deepStrictEqual(getToolCallLoopCount(), 2, 'Should detect as same call regardless of key order'); } // ═══════════════════════════════════════════════════════════════════════════ @@ -132,8 +132,8 @@ console.log('\n── Loop guard: nested args are not stripped ──'); const result = checkToolCallLoop('ask_user_questions', { questions: [{ id: `q${i}`, question: `Question ${i}?` }], }); - assertTrue(result.block === false, `Nested call ${i} with unique content should be allowed`); - assertEq(getToolCallLoopCount(), 1, `Each unique nested call should reset count to 1`); + assert.ok(result.block === false, `Nested call ${i} with unique content should be allowed`); + assert.deepStrictEqual(getToolCallLoopCount(), 1, `Each unique nested call should reset count to 1`); } // Truly identical nested calls should still be detected @@ -146,7 +146,7 @@ console.log('\n── Loop guard: nested args are not stripped ──'); const blocked = checkToolCallLoop('ask_user_questions', { questions: [{ id: 'same', question: 'Same?' }], }); - assertTrue(blocked.block === true, 'Identical nested calls should still be blocked'); + assert.ok(blocked.block === true, 'Identical nested calls should still be blocked'); } // ═══════════════════════════════════════════════════════════════════════════ @@ -160,9 +160,7 @@ console.log('\n── Loop guard: nested key order is normalized ──'); checkToolCallLoop('tool', { outer: { b: 2, a: 1 } }); const result = checkToolCallLoop('tool', { outer: { a: 1, b: 2 } }); - assertEq(getToolCallLoopCount(), 2, 'Same nested args in different key order should match'); + assert.deepStrictEqual(getToolCallLoopCount(), 2, 'Same nested args in different key order should match'); } // ═══════════════════════════════════════════════════════════════════════════ - -report(); diff --git a/src/resources/extensions/gsd/tests/tool-naming.test.ts b/src/resources/extensions/gsd/tests/tool-naming.test.ts index 786713c25..1ce5ebe1d 100644 --- a/src/resources/extensions/gsd/tests/tool-naming.test.ts +++ b/src/resources/extensions/gsd/tests/tool-naming.test.ts @@ -4,10 +4,10 @@ // AND under a backward-compatible alias name. // The alias must share the exact same execute function reference as the canonical tool. -import { createTestContext } from './test-helpers.ts'; +import { test } from 'node:test'; +import assert from 'node:assert/strict'; import { registerDbTools } from '../bootstrap/db-tools.ts'; -const { assertEq, assertTrue, report } = createTestContext(); // ─── Mock PI ────────────────────────────────────────────────────────────────── @@ -43,7 +43,7 @@ console.log('\n── Tool naming: registration count ──'); const pi = makeMockPi(); registerDbTools(pi); -assertEq(pi.tools.length, 24, 'Should register exactly 24 tools (12 canonical + 12 aliases)'); +assert.deepStrictEqual(pi.tools.length, 24, 'Should register exactly 24 tools (12 canonical + 12 aliases)'); // ─── Both names exist for each pair ────────────────────────────────────────── @@ -53,8 +53,8 @@ for (const { canonical, alias } of RENAME_MAP) { const canonicalTool = pi.tools.find((t: any) => t.name === canonical); const aliasTool = pi.tools.find((t: any) => t.name === alias); - assertTrue(canonicalTool !== undefined, `Canonical tool "${canonical}" should be registered`); - assertTrue(aliasTool !== undefined, `Alias tool "${alias}" should be registered`); + assert.ok(canonicalTool !== undefined, `Canonical tool "${canonical}" should be registered`); + assert.ok(aliasTool !== undefined, `Alias tool "${alias}" should be registered`); } // ─── Execute function identity ─────────────────────────────────────────────── @@ -66,7 +66,7 @@ for (const { canonical, alias } of RENAME_MAP) { const aliasTool = pi.tools.find((t: any) => t.name === alias); if (canonicalTool && aliasTool) { - assertTrue( + assert.ok( canonicalTool.execute === aliasTool.execute, `"${canonical}" and "${alias}" should share the same execute function reference`, ); @@ -81,7 +81,7 @@ for (const { canonical, alias } of RENAME_MAP) { const aliasTool = pi.tools.find((t: any) => t.name === alias); if (aliasTool) { - assertTrue( + assert.ok( aliasTool.description.includes(`alias for ${canonical}`), `Alias "${alias}" description should include "alias for ${canonical}"`, ); @@ -97,7 +97,7 @@ for (const { canonical } of RENAME_MAP) { if (canonicalTool) { const guidelinesText = canonicalTool.promptGuidelines.join(' '); - assertTrue( + assert.ok( guidelinesText.includes(canonical), `Canonical tool "${canonical}" promptGuidelines should reference its own name`, ); @@ -113,7 +113,7 @@ for (const { canonical, alias } of RENAME_MAP) { if (aliasTool) { const guidelinesText = aliasTool.promptGuidelines.join(' '); - assertTrue( + assert.ok( guidelinesText.includes(`Alias for ${canonical}`), `Alias "${alias}" promptGuidelines should say "Alias for ${canonical}"`, ); @@ -121,5 +121,3 @@ for (const { canonical, alias } of RENAME_MAP) { } // ═══════════════════════════════════════════════════════════════════════════ - -report(); diff --git a/src/resources/extensions/gsd/tests/unique-milestone-ids.test.ts b/src/resources/extensions/gsd/tests/unique-milestone-ids.test.ts index 859095c10..9e1875bff 100644 --- a/src/resources/extensions/gsd/tests/unique-milestone-ids.test.ts +++ b/src/resources/extensions/gsd/tests/unique-milestone-ids.test.ts @@ -22,72 +22,72 @@ import { import { renderPreferencesForSystemPrompt } from '../preferences.ts'; import type { GSDPreferences } from '../preferences.ts'; -import { createTestContext } from './test-helpers.ts'; +import { describe, test } from 'node:test'; +import assert from 'node:assert/strict'; -const { assertEq, assertTrue, assertMatch, report } = createTestContext(); // ─── Tests ───────────────────────────────────────────────────────────────── -async function main(): Promise { +describe('unique-milestone-ids', async () => { console.log('unique-milestone-ids tests'); // (a) MILESTONE_ID_RE { console.log(' (a) MILESTONE_ID_RE'); // Should match - assertTrue(MILESTONE_ID_RE.test('M001'), 'matches M001'); - assertTrue(MILESTONE_ID_RE.test('M999'), 'matches M999'); - assertTrue(MILESTONE_ID_RE.test('M001-abc123'), 'matches M001-abc123'); - assertTrue(MILESTONE_ID_RE.test('M042-z9a8b7'), 'matches M042-z9a8b7'); + assert.ok(MILESTONE_ID_RE.test('M001'), 'matches M001'); + assert.ok(MILESTONE_ID_RE.test('M999'), 'matches M999'); + assert.ok(MILESTONE_ID_RE.test('M001-abc123'), 'matches M001-abc123'); + assert.ok(MILESTONE_ID_RE.test('M042-z9a8b7'), 'matches M042-z9a8b7'); // Should reject - assertTrue(!MILESTONE_ID_RE.test('M1'), 'rejects M1 (too few digits)'); - assertTrue(!MILESTONE_ID_RE.test('M0001'), 'rejects M0001 (too many digits)'); - assertTrue(!MILESTONE_ID_RE.test('M001-ABCDEF'), 'rejects M001-ABCDEF (uppercase prefix)'); - assertTrue(!MILESTONE_ID_RE.test('M001-short'), 'rejects M001-short (5-char prefix)'); - assertTrue(!MILESTONE_ID_RE.test('M001-toolong1'), 'rejects M001-toolong1 (>6-char prefix)'); - assertTrue(!MILESTONE_ID_RE.test('IM001'), 'rejects IM001 (prefix before M)'); - assertTrue(!MILESTONE_ID_RE.test(''), 'rejects empty string'); - assertTrue(!MILESTONE_ID_RE.test('M001extra'), 'rejects M001extra (trailing chars)'); - assertTrue(!MILESTONE_ID_RE.test('notes'), 'rejects non-milestone string'); + assert.ok(!MILESTONE_ID_RE.test('M1'), 'rejects M1 (too few digits)'); + assert.ok(!MILESTONE_ID_RE.test('M0001'), 'rejects M0001 (too many digits)'); + assert.ok(!MILESTONE_ID_RE.test('M001-ABCDEF'), 'rejects M001-ABCDEF (uppercase prefix)'); + assert.ok(!MILESTONE_ID_RE.test('M001-short'), 'rejects M001-short (5-char prefix)'); + assert.ok(!MILESTONE_ID_RE.test('M001-toolong1'), 'rejects M001-toolong1 (>6-char prefix)'); + assert.ok(!MILESTONE_ID_RE.test('IM001'), 'rejects IM001 (prefix before M)'); + assert.ok(!MILESTONE_ID_RE.test(''), 'rejects empty string'); + assert.ok(!MILESTONE_ID_RE.test('M001extra'), 'rejects M001extra (trailing chars)'); + assert.ok(!MILESTONE_ID_RE.test('notes'), 'rejects non-milestone string'); } // (b) extractMilestoneSeq { console.log(' (b) extractMilestoneSeq'); // Old format - assertEq(extractMilestoneSeq('M001'), 1, 'M001 → 1'); - assertEq(extractMilestoneSeq('M042'), 42, 'M042 → 42'); - assertEq(extractMilestoneSeq('M999'), 999, 'M999 → 999'); + assert.deepStrictEqual(extractMilestoneSeq('M001'), 1, 'M001 → 1'); + assert.deepStrictEqual(extractMilestoneSeq('M042'), 42, 'M042 → 42'); + assert.deepStrictEqual(extractMilestoneSeq('M999'), 999, 'M999 → 999'); // Unique format - assertEq(extractMilestoneSeq('M001-abc123'), 1, 'M001-abc123 → 1'); - assertEq(extractMilestoneSeq('M042-z9a8b7'), 42, 'M042-z9a8b7 → 42'); + assert.deepStrictEqual(extractMilestoneSeq('M001-abc123'), 1, 'M001-abc123 → 1'); + assert.deepStrictEqual(extractMilestoneSeq('M042-z9a8b7'), 42, 'M042-z9a8b7 → 42'); // Invalid → 0 - assertEq(extractMilestoneSeq(''), 0, 'empty → 0'); - assertEq(extractMilestoneSeq('notes'), 0, 'notes → 0'); - assertEq(extractMilestoneSeq('M1'), 0, 'M1 → 0'); - assertEq(extractMilestoneSeq('.DS_Store'), 0, '.DS_Store → 0'); - assertEq(extractMilestoneSeq('M-ABC-001'), 0, 'M-ABC-001 (old format) → 0'); + assert.deepStrictEqual(extractMilestoneSeq(''), 0, 'empty → 0'); + assert.deepStrictEqual(extractMilestoneSeq('notes'), 0, 'notes → 0'); + assert.deepStrictEqual(extractMilestoneSeq('M1'), 0, 'M1 → 0'); + assert.deepStrictEqual(extractMilestoneSeq('.DS_Store'), 0, '.DS_Store → 0'); + assert.deepStrictEqual(extractMilestoneSeq('M-ABC-001'), 0, 'M-ABC-001 (old format) → 0'); } // (c) parseMilestoneId { console.log(' (c) parseMilestoneId'); // Old format — no suffix - assertEq(parseMilestoneId('M001'), { num: 1 }, 'M001 → { num: 1 }'); - assertEq(parseMilestoneId('M042'), { num: 42 }, 'M042 → { num: 42 }'); + assert.deepStrictEqual(parseMilestoneId('M001'), { num: 1 }, 'M001 → { num: 1 }'); + assert.deepStrictEqual(parseMilestoneId('M042'), { num: 42 }, 'M042 → { num: 42 }'); // Unique format — with suffix - assertEq(parseMilestoneId('M001-abc123'), { suffix: 'abc123', num: 1 }, 'M001-abc123 → { suffix, num }'); - assertEq(parseMilestoneId('M042-z9a8b7'), { suffix: 'z9a8b7', num: 42 }, 'M042-z9a8b7 → { suffix, num }'); + assert.deepStrictEqual(parseMilestoneId('M001-abc123'), { suffix: 'abc123', num: 1 }, 'M001-abc123 → { suffix, num }'); + assert.deepStrictEqual(parseMilestoneId('M042-z9a8b7'), { suffix: 'z9a8b7', num: 42 }, 'M042-z9a8b7 → { suffix, num }'); // Invalid → { num: 0 } - assertEq(parseMilestoneId(''), { num: 0 }, 'empty → { num: 0 }'); - assertEq(parseMilestoneId('notes'), { num: 0 }, 'notes → { num: 0 }'); - assertEq(parseMilestoneId('M001-ABCDEF'), { num: 0 }, 'uppercase suffix → { num: 0 }'); - assertEq(parseMilestoneId('M1'), { num: 0 }, 'M1 → { num: 0 }'); + assert.deepStrictEqual(parseMilestoneId(''), { num: 0 }, 'empty → { num: 0 }'); + assert.deepStrictEqual(parseMilestoneId('notes'), { num: 0 }, 'notes → { num: 0 }'); + assert.deepStrictEqual(parseMilestoneId('M001-ABCDEF'), { num: 0 }, 'uppercase suffix → { num: 0 }'); + assert.deepStrictEqual(parseMilestoneId('M1'), { num: 0 }, 'M1 → { num: 0 }'); } // (d) milestoneIdSort @@ -95,81 +95,81 @@ async function main(): Promise { console.log(' (d) milestoneIdSort'); const mixed = ['M003-abc123', 'M001', 'M002-z9a8b7']; const sorted = [...mixed].sort(milestoneIdSort); - assertEq(sorted, ['M001', 'M002-z9a8b7', 'M003-abc123'], 'sorts mixed IDs by sequence number'); + assert.deepStrictEqual(sorted, ['M001', 'M002-z9a8b7', 'M003-abc123'], 'sorts mixed IDs by sequence number'); // All old format const oldOnly = ['M003', 'M001', 'M002']; - assertEq([...oldOnly].sort(milestoneIdSort), ['M001', 'M002', 'M003'], 'sorts old-format IDs'); + assert.deepStrictEqual([...oldOnly].sort(milestoneIdSort), ['M001', 'M002', 'M003'], 'sorts old-format IDs'); // Invalid entries sort to front (seq 0) const withInvalid = ['M002', 'notes', 'M001']; - assertEq([...withInvalid].sort(milestoneIdSort), ['notes', 'M001', 'M002'], 'invalid entries (seq 0) sort first'); + assert.deepStrictEqual([...withInvalid].sort(milestoneIdSort), ['notes', 'M001', 'M002'], 'invalid entries (seq 0) sort first'); } // (e) generateMilestoneSuffix { console.log(' (e) generateMilestoneSuffix'); const suffix1 = generateMilestoneSuffix(); - assertEq(suffix1.length, 6, 'suffix length is 6'); - assertMatch(suffix1, /^[a-z0-9]{6}$/, 'suffix matches [a-z0-9]{6}'); + assert.deepStrictEqual(suffix1.length, 6, 'suffix length is 6'); + assert.match(suffix1, /^[a-z0-9]{6}$/, 'suffix matches [a-z0-9]{6}'); const suffix2 = generateMilestoneSuffix(); - assertEq(suffix2.length, 6, 'second suffix length is 6'); - assertMatch(suffix2, /^[a-z0-9]{6}$/, 'second suffix matches [a-z0-9]{6}'); + assert.deepStrictEqual(suffix2.length, 6, 'second suffix length is 6'); + assert.match(suffix2, /^[a-z0-9]{6}$/, 'second suffix matches [a-z0-9]{6}'); // Two calls should produce different results (36^6 = ~2.2B possibilities) - assertTrue(suffix1 !== suffix2, 'two calls produce different suffixes'); + assert.ok(suffix1 !== suffix2, 'two calls produce different suffixes'); } // (f) nextMilestoneId { console.log(' (f) nextMilestoneId'); // uniqueEnabled=false (default) → old format - assertEq(nextMilestoneId([]), 'M001', 'empty + uniqueEnabled=false → M001'); - assertEq(nextMilestoneId(['M001', 'M002']), 'M003', 'sequential + uniqueEnabled=false → M003'); - assertEq(nextMilestoneId(['M001', 'M002'], false), 'M003', 'explicit false → M003'); + assert.deepStrictEqual(nextMilestoneId([]), 'M001', 'empty + uniqueEnabled=false → M001'); + assert.deepStrictEqual(nextMilestoneId(['M001', 'M002']), 'M003', 'sequential + uniqueEnabled=false → M003'); + assert.deepStrictEqual(nextMilestoneId(['M001', 'M002'], false), 'M003', 'explicit false → M003'); // uniqueEnabled=true → unique format const newId = nextMilestoneId([], true); - assertMatch(newId, MILESTONE_ID_RE, 'uniqueEnabled=true produces valid ID'); - assertTrue(newId.startsWith('M001-'), 'uniqueEnabled=true starts with M001-'); - assertMatch(newId, /^M001-[a-z0-9]{6}$/, 'empty + uniqueEnabled=true → M001-{rand6}'); + assert.match(newId, MILESTONE_ID_RE, 'uniqueEnabled=true produces valid ID'); + assert.ok(newId.startsWith('M001-'), 'uniqueEnabled=true starts with M001-'); + assert.match(newId, /^M001-[a-z0-9]{6}$/, 'empty + uniqueEnabled=true → M001-{rand6}'); // Mixed array with uniqueEnabled=true const mixedIds = ['M001', 'M003-abc123', 'M002']; const nextNew = nextMilestoneId(mixedIds, true); - assertMatch(nextNew, MILESTONE_ID_RE, 'mixed array + uniqueEnabled=true → valid ID'); - assertMatch(nextNew, /^M004-[a-z0-9]{6}$/, 'mixed array max=3 → M004-{rand6}'); + assert.match(nextNew, MILESTONE_ID_RE, 'mixed array + uniqueEnabled=true → valid ID'); + assert.match(nextNew, /^M004-[a-z0-9]{6}$/, 'mixed array max=3 → M004-{rand6}'); // Mixed array with uniqueEnabled=false - assertEq(nextMilestoneId(mixedIds, false), 'M004', 'mixed array + uniqueEnabled=false → M004'); + assert.deepStrictEqual(nextMilestoneId(mixedIds, false), 'M004', 'mixed array + uniqueEnabled=false → M004'); // Correct sequential number from mixed arrays const mixedIds2 = ['M005-xyz999', 'M002']; - assertEq(nextMilestoneId(mixedIds2, false), 'M006', 'mixed max=5 → M006'); + assert.deepStrictEqual(nextMilestoneId(mixedIds2, false), 'M006', 'mixed max=5 → M006'); const nextNew2 = nextMilestoneId(mixedIds2, true); - assertMatch(nextNew2, /^M006-[a-z0-9]{6}$/, 'mixed max=5 + unique → M006-{rand6}'); + assert.match(nextNew2, /^M006-[a-z0-9]{6}$/, 'mixed max=5 + unique → M006-{rand6}'); } // (g) maxMilestoneNum { console.log(' (g) maxMilestoneNum'); // Empty - assertEq(maxMilestoneNum([]), 0, 'empty → 0'); + assert.deepStrictEqual(maxMilestoneNum([]), 0, 'empty → 0'); // Old format only - assertEq(maxMilestoneNum(['M001', 'M002', 'M003']), 3, 'old format only → 3'); + assert.deepStrictEqual(maxMilestoneNum(['M001', 'M002', 'M003']), 3, 'old format only → 3'); // Unique format only — must not return NaN - assertEq(maxMilestoneNum(['M001-abc123', 'M002-def456']), 2, 'unique format only → 2'); - assertTrue(!Number.isNaN(maxMilestoneNum(['M001-abc123'])), 'unique format does not return NaN'); + assert.deepStrictEqual(maxMilestoneNum(['M001-abc123', 'M002-def456']), 2, 'unique format only → 2'); + assert.ok(!Number.isNaN(maxMilestoneNum(['M001-abc123'])), 'unique format does not return NaN'); // Mixed formats - assertEq(maxMilestoneNum(['M001', 'M003-abc123', 'M002']), 3, 'mixed → 3'); + assert.deepStrictEqual(maxMilestoneNum(['M001', 'M003-abc123', 'M002']), 3, 'mixed → 3'); // Non-matching entries ignored - assertEq(maxMilestoneNum(['M001', 'notes', '.DS_Store', 'M003']), 3, 'non-matching ignored → 3'); - assertEq(maxMilestoneNum(['notes', '.DS_Store']), 0, 'all non-matching → 0'); + assert.deepStrictEqual(maxMilestoneNum(['M001', 'notes', '.DS_Store', 'M003']), 3, 'non-matching ignored → 3'); + assert.deepStrictEqual(maxMilestoneNum(['notes', '.DS_Store']), 0, 'all non-matching → 0'); } // (h) Preferences round-trip via renderPreferencesForSystemPrompt @@ -179,41 +179,25 @@ async function main(): Promise { // validate { unique_milestone_ids: true } → field preserved (no validation error) const prefsTrue: GSDPreferences = { unique_milestone_ids: true }; const renderedTrue = renderPreferencesForSystemPrompt(prefsTrue); - assertTrue(!renderedTrue.includes('some preference values were ignored'), 'unique_milestone_ids: true validates without error'); + assert.ok(!renderedTrue.includes('some preference values were ignored'), 'unique_milestone_ids: true validates without error'); // validate { unique_milestone_ids: undefined } → field absent (no error) const prefsUndefined: GSDPreferences = {}; const renderedUndefined = renderPreferencesForSystemPrompt(prefsUndefined); - assertTrue(!renderedUndefined.includes('some preference values were ignored'), 'undefined unique_milestone_ids validates without error'); + assert.ok(!renderedUndefined.includes('some preference values were ignored'), 'undefined unique_milestone_ids validates without error'); // validate { unique_milestone_ids: false } → also valid const prefsFalse: GSDPreferences = { unique_milestone_ids: false }; const renderedFalse = renderPreferencesForSystemPrompt(prefsFalse); - assertTrue(!renderedFalse.includes('some preference values were ignored'), 'unique_milestone_ids: false validates without error'); + assert.ok(!renderedFalse.includes('some preference values were ignored'), 'unique_milestone_ids: false validates without error'); // validate coercion: truthy non-boolean → coerced to boolean (no crash) const prefsCoerced: GSDPreferences = { unique_milestone_ids: 1 as unknown as boolean }; const renderedCoerced = renderPreferencesForSystemPrompt(prefsCoerced); - assertTrue(!renderedCoerced.includes('some preference values were ignored'), 'truthy non-boolean coerces without validation error'); + assert.ok(!renderedCoerced.includes('some preference values were ignored'), 'truthy non-boolean coerces without validation error'); // GSDPreferences interface accepts the field (compile-time check — if this compiles, it works) const prefs: GSDPreferences = { unique_milestone_ids: true, version: 1 }; - assertTrue(prefs.unique_milestone_ids === true, 'GSDPreferences interface accepts unique_milestone_ids'); + assert.ok(prefs.unique_milestone_ids === true, 'GSDPreferences interface accepts unique_milestone_ids'); } - - report(); -} - -// When run via vitest, wrap in test(); when run via tsx, call directly. -const isVitest = typeof globalThis !== 'undefined' && (globalThis as any).__vitest_worker__?.config?.defines != null && 'vitest' in (globalThis as any).__vitest_worker__.config.defines || process.env.VITEST; -if (isVitest) { - const { test } = await import('node:test'); - test('unique-milestone-ids: all ID primitives handle both formats', async () => { - await main(); - }); -} else { - main().catch((error) => { - console.error(error); - process.exit(1); - }); -} +}); diff --git a/src/resources/extensions/gsd/tests/unit-runtime.test.ts b/src/resources/extensions/gsd/tests/unit-runtime.test.ts index 69e21d131..6f892d5b5 100644 --- a/src/resources/extensions/gsd/tests/unit-runtime.test.ts +++ b/src/resources/extensions/gsd/tests/unit-runtime.test.ts @@ -9,9 +9,9 @@ import { writeUnitRuntimeRecord, } from "../unit-runtime.ts"; import { clearPathCache } from '../paths.ts'; -import { createTestContext } from './test-helpers.ts'; +import { test } from 'node:test'; +import assert from 'node:assert/strict'; -const { assertEq, assertTrue, report } = createTestContext(); const base = mkdtempSync(join(tmpdir(), "gsd-unit-runtime-test-")); const tasksDir = join(base, ".gsd", "milestones", "M100", "slices", "S02", "tasks"); mkdirSync(tasksDir, { recursive: true }); @@ -25,22 +25,22 @@ writeFileSync( console.log("\n=== runtime record write/read/update ==="); { const first = writeUnitRuntimeRecord(base, "execute-task", "M100/S02/T09", 1000, { phase: "dispatched" }); - assertEq(first.phase, "dispatched", "initial phase"); + assert.deepStrictEqual(first.phase, "dispatched", "initial phase"); const second = writeUnitRuntimeRecord(base, "execute-task", "M100/S02/T09", 1000, { phase: "wrapup-warning-sent", wrapupWarningSent: true }); - assertEq(second.wrapupWarningSent, true, "warning persisted"); + assert.deepStrictEqual(second.wrapupWarningSent, true, "warning persisted"); const loaded = readUnitRuntimeRecord(base, "execute-task", "M100/S02/T09"); - assertTrue(loaded !== null, "record readable"); - assertEq(loaded!.phase, "wrapup-warning-sent", "updated phase readable"); + assert.ok(loaded !== null, "record readable"); + assert.deepStrictEqual(loaded!.phase, "wrapup-warning-sent", "updated phase readable"); } console.log("\n=== execute-task durability inspection ==="); { let status = await inspectExecuteTaskDurability(base, "M100/S02/T09"); - assertTrue(status !== null, "status exists"); - assertEq(status!.summaryExists, false, "summary initially missing"); - assertEq(status!.taskChecked, false, "task initially unchecked"); - assertEq(status!.nextActionAdvanced, false, "next action initially stale"); - assertTrue(/summary missing/i.test(formatExecuteTaskRecoveryStatus(status!)), "diagnostic mentions summary"); + assert.ok(status !== null, "status exists"); + assert.deepStrictEqual(status!.summaryExists, false, "summary initially missing"); + assert.deepStrictEqual(status!.taskChecked, false, "task initially unchecked"); + assert.deepStrictEqual(status!.nextActionAdvanced, false, "next action initially stale"); + assert.ok(/summary missing/i.test(formatExecuteTaskRecoveryStatus(status!)), "diagnostic mentions summary"); writeFileSync(join(tasksDir, "T09-SUMMARY.md"), "# done\n", "utf-8"); writeFileSync( @@ -52,17 +52,17 @@ console.log("\n=== execute-task durability inspection ==="); clearPathCache(); status = await inspectExecuteTaskDurability(base, "M100/S02/T09"); - assertEq(status!.summaryExists, true, "summary found after write"); - assertEq(status!.taskChecked, true, "task checked after update"); - assertEq(status!.nextActionAdvanced, true, "next action advanced after update"); - assertEq(formatExecuteTaskRecoveryStatus(status!), "all durable task artifacts present", "clean diagnostic when complete"); + assert.deepStrictEqual(status!.summaryExists, true, "summary found after write"); + assert.deepStrictEqual(status!.taskChecked, true, "task checked after update"); + assert.deepStrictEqual(status!.nextActionAdvanced, true, "next action advanced after update"); + assert.deepStrictEqual(formatExecuteTaskRecoveryStatus(status!), "all durable task artifacts present", "clean diagnostic when complete"); } console.log("\n=== runtime record cleanup ==="); { clearUnitRuntimeRecord(base, "execute-task", "M100/S02/T09"); const loaded = readUnitRuntimeRecord(base, "execute-task", "M100/S02/T09"); - assertEq(loaded, null, "record removed"); + assert.deepStrictEqual(loaded, null, "record removed"); } console.log("\n=== hook unit type sanitization (slash in unitType) ==="); @@ -70,23 +70,23 @@ console.log("\n=== hook unit type sanitization (slash in unitType) ==="); // Hook units have unitType like "hook/code-review" with a slash // This should NOT create a subdirectory - the slash must be sanitized const hookRecord = writeUnitRuntimeRecord(base, "hook/code-review", "M100/S02/T10", 2000, { phase: "dispatched" }); - assertEq(hookRecord.unitType, "hook/code-review", "unitType preserved in record"); - assertEq(hookRecord.unitId, "M100/S02/T10", "unitId preserved in record"); + assert.deepStrictEqual(hookRecord.unitType, "hook/code-review", "unitType preserved in record"); + assert.deepStrictEqual(hookRecord.unitId, "M100/S02/T10", "unitId preserved in record"); const loaded = readUnitRuntimeRecord(base, "hook/code-review", "M100/S02/T10"); - assertTrue(loaded !== null, "hook record readable"); - assertEq(loaded!.phase, "dispatched", "hook phase correct"); + assert.ok(loaded !== null, "hook record readable"); + assert.deepStrictEqual(loaded!.phase, "dispatched", "hook phase correct"); // Verify the file is in the units dir, not in a subdirectory const unitsDir = join(base, ".gsd", "runtime", "units"); const files = readdirSync(unitsDir); const hookFile = files.find((f: string) => f.includes("hook-code-review")); - assertTrue(hookFile !== undefined, "hook file exists with sanitized name"); - assertTrue(!files.some((f: string) => f === "hook"), "no 'hook' subdirectory created"); + assert.ok(hookFile !== undefined, "hook file exists with sanitized name"); + assert.ok(!files.some((f: string) => f === "hook"), "no 'hook' subdirectory created"); clearUnitRuntimeRecord(base, "hook/code-review", "M100/S02/T10"); const cleared = readUnitRuntimeRecord(base, "hook/code-review", "M100/S02/T10"); - assertEq(cleared, null, "hook record removed"); + assert.deepStrictEqual(cleared, null, "hook record removed"); } // ─── Must-have durability integration tests ─────────────────────────────── @@ -121,13 +121,13 @@ console.log("\n=== must-haves: all mentioned in summary ==="); writeFileSync(join(mhBase, ".gsd", "STATE.md"), "## Next Action\nExecute T02 for S01: next thing\n", "utf-8"); const status = await inspectExecuteTaskDurability(mhBase, "M200/S01/T01"); - assertTrue(status !== null, "mh-all: status exists"); - assertEq(status!.mustHaveCount, 3, "mh-all: mustHaveCount is 3"); - assertEq(status!.mustHavesMentionedInSummary, 3, "mh-all: all 3 must-haves mentioned"); - assertEq(status!.summaryExists, true, "mh-all: summary exists"); - assertEq(status!.taskChecked, true, "mh-all: task checked"); + assert.ok(status !== null, "mh-all: status exists"); + assert.deepStrictEqual(status!.mustHaveCount, 3, "mh-all: mustHaveCount is 3"); + assert.deepStrictEqual(status!.mustHavesMentionedInSummary, 3, "mh-all: all 3 must-haves mentioned"); + assert.deepStrictEqual(status!.summaryExists, true, "mh-all: summary exists"); + assert.deepStrictEqual(status!.taskChecked, true, "mh-all: task checked"); const diag = formatExecuteTaskRecoveryStatus(status!); - assertEq(diag, "all durable task artifacts present", "mh-all: diagnostic is clean when all must-haves met"); + assert.deepStrictEqual(diag, "all durable task artifacts present", "mh-all: diagnostic is clean when all must-haves met"); } console.log("\n=== must-haves: partially mentioned in summary ==="); @@ -156,12 +156,12 @@ console.log("\n=== must-haves: partially mentioned in summary ==="); clearPathCache(); const status = await inspectExecuteTaskDurability(mhBase, "M200/S02/T01"); - assertTrue(status !== null, "mh-partial: status exists"); - assertEq(status!.mustHaveCount, 3, "mh-partial: mustHaveCount is 3"); - assertEq(status!.mustHavesMentionedInSummary, 1, "mh-partial: only 1 must-have mentioned"); + assert.ok(status !== null, "mh-partial: status exists"); + assert.deepStrictEqual(status!.mustHaveCount, 3, "mh-partial: mustHaveCount is 3"); + assert.deepStrictEqual(status!.mustHavesMentionedInSummary, 1, "mh-partial: only 1 must-have mentioned"); const diag = formatExecuteTaskRecoveryStatus(status!); - assertTrue(diag.includes("must-have gap"), "mh-partial: diagnostic includes 'must-have gap'"); - assertTrue(diag.includes("1 of 3"), "mh-partial: diagnostic includes '1 of 3'"); + assert.ok(diag.includes("must-have gap"), "mh-partial: diagnostic includes 'must-have gap'"); + assert.ok(diag.includes("1 of 3"), "mh-partial: diagnostic includes '1 of 3'"); } console.log("\n=== must-haves: no task plan file ==="); @@ -184,9 +184,9 @@ console.log("\n=== must-haves: no task plan file ==="); clearPathCache(); const status = await inspectExecuteTaskDurability(mhBase, "M200/S03/T01"); - assertTrue(status !== null, "mh-noplan: status exists"); - assertEq(status!.mustHaveCount, 0, "mh-noplan: mustHaveCount is 0 when no task plan"); - assertEq(status!.mustHavesMentionedInSummary, 0, "mh-noplan: mustHavesMentionedInSummary is 0"); + assert.ok(status !== null, "mh-noplan: status exists"); + assert.deepStrictEqual(status!.mustHaveCount, 0, "mh-noplan: mustHaveCount is 0 when no task plan"); + assert.deepStrictEqual(status!.mustHavesMentionedInSummary, 0, "mh-noplan: mustHavesMentionedInSummary is 0"); } console.log("\n=== must-haves: present but no summary file ==="); @@ -209,10 +209,10 @@ console.log("\n=== must-haves: present but no summary file ==="); clearPathCache(); const status = await inspectExecuteTaskDurability(mhBase, "M200/S04/T01"); - assertTrue(status !== null, "mh-nosummary: status exists"); - assertEq(status!.mustHaveCount, 2, "mh-nosummary: mustHaveCount is 2"); - assertEq(status!.mustHavesMentionedInSummary, 0, "mh-nosummary: mustHavesMentionedInSummary is 0 with no summary"); - assertEq(status!.summaryExists, false, "mh-nosummary: summary doesn't exist"); + assert.ok(status !== null, "mh-nosummary: status exists"); + assert.deepStrictEqual(status!.mustHaveCount, 2, "mh-nosummary: mustHaveCount is 2"); + assert.deepStrictEqual(status!.mustHavesMentionedInSummary, 0, "mh-nosummary: mustHavesMentionedInSummary is 0 with no summary"); + assert.deepStrictEqual(status!.summaryExists, false, "mh-nosummary: summary doesn't exist"); } console.log("\n=== must-haves: substring matching (no backtick tokens) ==="); @@ -241,18 +241,17 @@ console.log("\n=== must-haves: substring matching (no backtick tokens) ==="); clearPathCache(); const status = await inspectExecuteTaskDurability(mhBase, "M200/S05/T01"); - assertTrue(status !== null, "mh-substr: status exists"); - assertEq(status!.mustHaveCount, 3, "mh-substr: mustHaveCount is 3"); + assert.ok(status !== null, "mh-substr: status exists"); + assert.deepStrictEqual(status!.mustHaveCount, 3, "mh-substr: mustHaveCount is 3"); // "heuristic" appears in summary for item 1, "diagnostic" for item 2, // "assertions" appears in summary? No — let's check // Item 3: "All assertions pass" — words: "assertions", "pass" (<4 chars excluded) // summary doesn't contain "assertions" → not matched - assertEq(status!.mustHavesMentionedInSummary, 2, "mh-substr: 2 of 3 matched via substring"); + assert.deepStrictEqual(status!.mustHavesMentionedInSummary, 2, "mh-substr: 2 of 3 matched via substring"); const diag = formatExecuteTaskRecoveryStatus(status!); - assertTrue(diag.includes("must-have gap"), "mh-substr: diagnostic includes gap info"); - assertTrue(diag.includes("2 of 3"), "mh-substr: diagnostic includes '2 of 3'"); + assert.ok(diag.includes("must-have gap"), "mh-substr: diagnostic includes gap info"); + assert.ok(diag.includes("2 of 3"), "mh-substr: diagnostic includes '2 of 3'"); } rmSync(mhBase, { recursive: true, force: true }); rmSync(base, { recursive: true, force: true }); -report(); diff --git a/src/resources/extensions/gsd/tests/visualizer-critical-path.test.ts b/src/resources/extensions/gsd/tests/visualizer-critical-path.test.ts index 520e488fa..8abd48af4 100644 --- a/src/resources/extensions/gsd/tests/visualizer-critical-path.test.ts +++ b/src/resources/extensions/gsd/tests/visualizer-critical-path.test.ts @@ -3,9 +3,9 @@ import { computeCriticalPath } from "../visualizer-data.js"; import type { VisualizerMilestone } from "../visualizer-data.js"; -import { createTestContext } from "./test-helpers.ts"; +import { test } from 'node:test'; +import assert from 'node:assert/strict'; -const { assertEq, assertTrue, report } = createTestContext(); function makeMs(id: string, status: "complete" | "active" | "pending", dependsOn: string[], slices: any[] = []): VisualizerMilestone { return { id, title: id, status, dependsOn, slices }; @@ -31,11 +31,11 @@ console.log("\n=== Critical Path: Linear Chain ==="); ]; const cp = computeCriticalPath(milestones); - assertTrue(cp.milestonePath.length > 0, "linear chain has critical path"); - assertTrue(cp.milestonePath.includes("M002"), "M002 is on critical path"); - assertTrue(cp.milestonePath.includes("M003"), "M003 is on critical path"); - assertEq(cp.milestoneSlack.get("M002"), 0, "M002 has zero slack"); - assertEq(cp.milestoneSlack.get("M003"), 0, "M003 has zero slack"); + assert.ok(cp.milestonePath.length > 0, "linear chain has critical path"); + assert.ok(cp.milestonePath.includes("M002"), "M002 is on critical path"); + assert.ok(cp.milestonePath.includes("M003"), "M003 is on critical path"); + assert.deepStrictEqual(cp.milestoneSlack.get("M002"), 0, "M002 has zero slack"); + assert.deepStrictEqual(cp.milestoneSlack.get("M003"), 0, "M003 has zero slack"); } // ─── Diamond DAG ──────────────────────────────────────────────────────────── @@ -60,14 +60,14 @@ console.log("\n=== Critical Path: Diamond DAG ==="); ]; const cp = computeCriticalPath(milestones); - assertTrue(cp.milestonePath.length >= 2, "diamond DAG has critical path"); + assert.ok(cp.milestonePath.length >= 2, "diamond DAG has critical path"); // M002 has weight 3 (3 incomplete), M003 has weight 1 // Critical path should go through M002 (longer) - assertTrue(cp.milestonePath.includes("M002"), "M002 (heavier) is on critical path"); + assert.ok(cp.milestonePath.includes("M002"), "M002 (heavier) is on critical path"); // M003 should have non-zero slack since it's lighter const m003Slack = cp.milestoneSlack.get("M003") ?? -1; - assertTrue(m003Slack > 0, "M003 has positive slack (lighter branch)"); + assert.ok(m003Slack > 0, "M003 has positive slack (lighter branch)"); } // ─── Independent branches ─────────────────────────────────────────────────── @@ -83,9 +83,9 @@ console.log("\n=== Critical Path: Independent Branches ==="); ]; const cp = computeCriticalPath(milestones); - assertTrue(cp.milestonePath.length >= 1, "independent branches have at least one critical node"); + assert.ok(cp.milestonePath.length >= 1, "independent branches have at least one critical node"); // M002 has the most incomplete slices, should be critical - assertTrue(cp.milestonePath.includes("M002"), "M002 (longest) is on critical path"); + assert.ok(cp.milestonePath.includes("M002"), "M002 (longest) is on critical path"); } // ─── Slice-level critical path ────────────────────────────────────────────── @@ -104,13 +104,13 @@ console.log("\n=== Critical Path: Slice-level ==="); ]; const cp = computeCriticalPath(milestones); - assertTrue(cp.slicePath.length > 0, "has slice-level critical path"); - assertTrue(cp.slicePath.includes("S02"), "S02 is on slice critical path"); - assertTrue(cp.slicePath.includes("S04"), "S04 is on slice critical path"); + assert.ok(cp.slicePath.length > 0, "has slice-level critical path"); + assert.ok(cp.slicePath.includes("S02"), "S02 is on slice critical path"); + assert.ok(cp.slicePath.includes("S04"), "S04 is on slice critical path"); // S03 should have non-zero slack (it's a shorter branch) const s03Slack = cp.sliceSlack.get("S03") ?? -1; - assertTrue(s03Slack > 0, "S03 has positive slack (shorter branch)"); + assert.ok(s03Slack > 0, "S03 has positive slack (shorter branch)"); } // ─── Empty milestones ─────────────────────────────────────────────────────── @@ -119,8 +119,8 @@ console.log("\n=== Critical Path: Empty ==="); { const cp = computeCriticalPath([]); - assertEq(cp.milestonePath.length, 0, "empty milestones produce empty path"); - assertEq(cp.slicePath.length, 0, "empty milestones produce empty slice path"); + assert.deepStrictEqual(cp.milestonePath.length, 0, "empty milestones produce empty path"); + assert.deepStrictEqual(cp.slicePath.length, 0, "empty milestones produce empty slice path"); } // ─── Single milestone ─────────────────────────────────────────────────────── @@ -136,10 +136,8 @@ console.log("\n=== Critical Path: Single Milestone ==="); ]; const cp = computeCriticalPath(milestones); - assertTrue(cp.milestonePath.length === 1, "single milestone is its own critical path"); - assertEq(cp.milestonePath[0], "M001", "M001 is the critical node"); + assert.ok(cp.milestonePath.length === 1, "single milestone is its own critical path"); + assert.deepStrictEqual(cp.milestonePath[0], "M001", "M001 is the critical node"); } // ─── Report ───────────────────────────────────────────────────────────────── - -report(); diff --git a/src/resources/extensions/gsd/tests/visualizer-data.test.ts b/src/resources/extensions/gsd/tests/visualizer-data.test.ts index 9f9548169..9881cdd04 100644 --- a/src/resources/extensions/gsd/tests/visualizer-data.test.ts +++ b/src/resources/extensions/gsd/tests/visualizer-data.test.ts @@ -4,10 +4,10 @@ import { readFileSync } from "node:fs"; import { join, dirname } from "node:path"; import { fileURLToPath } from "node:url"; -import { createTestContext } from "./test-helpers.ts"; +import { test } from 'node:test'; +import assert from 'node:assert/strict'; const __dirname = dirname(fileURLToPath(import.meta.url)); -const { assertTrue, report } = createTestContext(); const dataPath = join(__dirname, "..", "visualizer-data.ts"); const dataSrc = readFileSync(dataPath, "utf-8"); @@ -15,293 +15,293 @@ const dataSrc = readFileSync(dataPath, "utf-8"); console.log("\n=== visualizer-data.ts source contracts ==="); // Interface exports -assertTrue( +assert.ok( dataSrc.includes("export interface VisualizerData"), "exports VisualizerData interface", ); -assertTrue( +assert.ok( dataSrc.includes("export interface VisualizerMilestone"), "exports VisualizerMilestone interface", ); -assertTrue( +assert.ok( dataSrc.includes("export interface VisualizerSlice"), "exports VisualizerSlice interface", ); -assertTrue( +assert.ok( dataSrc.includes("export interface VisualizerTask"), "exports VisualizerTask interface", ); // New interfaces -assertTrue( +assert.ok( dataSrc.includes("export interface CriticalPathInfo"), "exports CriticalPathInfo interface", ); -assertTrue( +assert.ok( dataSrc.includes("export interface AgentActivityInfo"), "exports AgentActivityInfo interface", ); -assertTrue( +assert.ok( dataSrc.includes("export interface ChangelogEntry"), "exports ChangelogEntry interface", ); -assertTrue( +assert.ok( dataSrc.includes("export interface ChangelogInfo"), "exports ChangelogInfo interface", ); -assertTrue( +assert.ok( dataSrc.includes("export interface SliceVerification"), "exports SliceVerification interface", ); -assertTrue( +assert.ok( dataSrc.includes("export interface KnowledgeInfo"), "exports KnowledgeInfo interface", ); -assertTrue( +assert.ok( dataSrc.includes("export interface CapturesInfo"), "exports CapturesInfo interface", ); -assertTrue( +assert.ok( dataSrc.includes("export interface HealthInfo"), "exports HealthInfo interface", ); -assertTrue( +assert.ok( dataSrc.includes("export interface VisualizerDiscussionState"), "exports VisualizerDiscussionState interface", ); -assertTrue( +assert.ok( dataSrc.includes("export type DiscussionState"), "exports DiscussionState type", ); -assertTrue( +assert.ok( dataSrc.includes("export interface VisualizerSliceRef"), "exports VisualizerSliceRef interface", ); -assertTrue( +assert.ok( dataSrc.includes("export interface VisualizerSliceActivity"), "exports VisualizerSliceActivity interface", ); -assertTrue( +assert.ok( dataSrc.includes("export interface VisualizerStats"), "exports VisualizerStats interface", ); // Function export -assertTrue( +assert.ok( dataSrc.includes("export async function loadVisualizerData"), "exports loadVisualizerData function", ); -assertTrue( +assert.ok( dataSrc.includes("export function computeCriticalPath"), "exports computeCriticalPath function", ); // Data source usage -assertTrue( +assert.ok( dataSrc.includes("deriveState"), "uses deriveState for state derivation", ); -assertTrue( +assert.ok( dataSrc.includes("findMilestoneIds"), "uses findMilestoneIds to enumerate milestones", ); -assertTrue( +assert.ok( dataSrc.includes("parseRoadmap"), "uses parseRoadmap for roadmap parsing", ); -assertTrue( +assert.ok( dataSrc.includes("parsePlan"), "uses parsePlan for plan parsing", ); -assertTrue( +assert.ok( dataSrc.includes("parseSummary"), "uses parseSummary for changelog parsing", ); -assertTrue( +assert.ok( dataSrc.includes("getLedger"), "uses getLedger for in-memory metrics", ); -assertTrue( +assert.ok( dataSrc.includes("loadLedgerFromDisk"), "uses loadLedgerFromDisk as fallback", ); -assertTrue( +assert.ok( dataSrc.includes("getProjectTotals"), "uses getProjectTotals for aggregation", ); -assertTrue( +assert.ok( dataSrc.includes("aggregateByPhase"), "uses aggregateByPhase", ); -assertTrue( +assert.ok( dataSrc.includes("aggregateBySlice"), "uses aggregateBySlice", ); -assertTrue( +assert.ok( dataSrc.includes("aggregateByModel"), "uses aggregateByModel", ); -assertTrue( +assert.ok( dataSrc.includes("aggregateByTier"), "uses aggregateByTier", ); -assertTrue( +assert.ok( dataSrc.includes("formatTierSavings"), "uses formatTierSavings", ); -assertTrue( +assert.ok( dataSrc.includes("loadAllCaptures"), "uses loadAllCaptures", ); -assertTrue( +assert.ok( dataSrc.includes("countPendingCaptures"), "uses countPendingCaptures", ); -assertTrue( +assert.ok( dataSrc.includes("loadEffectiveGSDPreferences"), "uses loadEffectiveGSDPreferences", ); -assertTrue( +assert.ok( dataSrc.includes("resolveGsdRootFile"), "uses resolveGsdRootFile for KNOWLEDGE path", ); // Interface fields -assertTrue( +assert.ok( dataSrc.includes("dependsOn: string[]"), "VisualizerMilestone has dependsOn field", ); -assertTrue( +assert.ok( dataSrc.includes("depends: string[]"), "VisualizerSlice has depends field", ); -assertTrue( +assert.ok( dataSrc.includes("totals: ProjectTotals | null"), "VisualizerData has nullable totals", ); -assertTrue( +assert.ok( dataSrc.includes("units: UnitMetrics[]"), "VisualizerData has units array", ); -assertTrue( +assert.ok( dataSrc.includes("estimate?: string"), "VisualizerTask has optional estimate field", ); // New data model fields -assertTrue( +assert.ok( dataSrc.includes("criticalPath: CriticalPathInfo"), "VisualizerData has criticalPath field", ); -assertTrue( +assert.ok( dataSrc.includes("remainingSliceCount: number"), "VisualizerData has remainingSliceCount field", ); -assertTrue( +assert.ok( dataSrc.includes("agentActivity: AgentActivityInfo | null"), "VisualizerData has agentActivity field", ); -assertTrue( +assert.ok( dataSrc.includes("changelog: ChangelogInfo"), "VisualizerData has changelog field", ); -assertTrue( +assert.ok( dataSrc.includes("sliceVerifications: SliceVerification[]"), "VisualizerData has sliceVerifications field", ); -assertTrue( +assert.ok( dataSrc.includes("knowledge: KnowledgeInfo"), "VisualizerData has knowledge field", ); -assertTrue( +assert.ok( dataSrc.includes("captures: CapturesInfo"), "VisualizerData has captures field", ); -assertTrue( +assert.ok( dataSrc.includes("health: HealthInfo"), "VisualizerData has health field", ); -assertTrue( +assert.ok( dataSrc.includes("stats: VisualizerStats"), "VisualizerData has stats field", ); -assertTrue( +assert.ok( dataSrc.includes("discussion: VisualizerDiscussionState[]"), "VisualizerData has discussion field", ); -assertTrue( +assert.ok( dataSrc.includes("loadDiscussionState"), "uses loadDiscussionState helper", ); -assertTrue( +assert.ok( dataSrc.includes("buildVisualizerStats"), "uses buildVisualizerStats helper", ); -assertTrue( +assert.ok( dataSrc.includes("byTier: TierAggregate[]"), "VisualizerData has byTier field", ); -assertTrue( +assert.ok( dataSrc.includes("tierSavingsLine: string"), "VisualizerData has tierSavingsLine field", ); // completedAt must be coerced to String() to handle YAML Date objects (issue #644) -assertTrue( +assert.ok( dataSrc.includes("String(summary.frontmatter.completed_at"), "completedAt assignment coerces to String() for YAML Date safety", ); -assertTrue( +assert.ok( dataSrc.includes("String(b.completedAt") && dataSrc.includes("String(a.completedAt"), "changelog sort coerces completedAt to String() for YAML Date safety", ); @@ -312,112 +312,112 @@ const overlaySrc = readFileSync(overlayPath, "utf-8"); console.log("\n=== visualizer-overlay.ts source contracts ==="); -assertTrue( +assert.ok( overlaySrc.includes("export class GSDVisualizerOverlay"), "exports GSDVisualizerOverlay class", ); -assertTrue( +assert.ok( overlaySrc.includes("loadVisualizerData"), "overlay uses loadVisualizerData", ); -assertTrue( +assert.ok( overlaySrc.includes("renderProgressView"), "overlay delegates to renderProgressView", ); -assertTrue( +assert.ok( overlaySrc.includes("renderDepsView"), "overlay delegates to renderDepsView", ); -assertTrue( +assert.ok( overlaySrc.includes("renderMetricsView"), "overlay delegates to renderMetricsView", ); -assertTrue( +assert.ok( overlaySrc.includes("renderTimelineView"), "overlay delegates to renderTimelineView", ); -assertTrue( +assert.ok( overlaySrc.includes("renderAgentView"), "overlay delegates to renderAgentView", ); -assertTrue( +assert.ok( overlaySrc.includes("renderChangelogView"), "overlay delegates to renderChangelogView", ); -assertTrue( +assert.ok( overlaySrc.includes("renderExportView"), "overlay delegates to renderExportView", ); -assertTrue( +assert.ok( overlaySrc.includes("renderKnowledgeView"), "overlay delegates to renderKnowledgeView", ); -assertTrue( +assert.ok( overlaySrc.includes("renderCapturesView"), "overlay delegates to renderCapturesView", ); -assertTrue( +assert.ok( overlaySrc.includes("renderHealthView"), "overlay delegates to renderHealthView", ); -assertTrue( +assert.ok( overlaySrc.includes("handleInput"), "overlay has handleInput method", ); -assertTrue( +assert.ok( overlaySrc.includes("dispose"), "overlay has dispose method", ); -assertTrue( +assert.ok( overlaySrc.includes("wrapInBox"), "overlay has wrapInBox helper", ); -assertTrue( +assert.ok( overlaySrc.includes("activeTab"), "overlay tracks active tab", ); -assertTrue( +assert.ok( overlaySrc.includes("scrollOffsets"), "overlay tracks per-tab scroll offsets", ); -assertTrue( +assert.ok( overlaySrc.includes("filterMode"), "overlay has filterMode state", ); -assertTrue( +assert.ok( overlaySrc.includes("filterText"), "overlay has filterText state", ); -assertTrue( +assert.ok( overlaySrc.includes("filterField"), "overlay has filterField state", ); -assertTrue( +assert.ok( overlaySrc.includes("TAB_COUNT"), "overlay defines TAB_COUNT", ); -assertTrue( +assert.ok( overlaySrc.includes("0 Export"), "overlay has 10 tab labels", ); @@ -428,19 +428,17 @@ const coreHandlerSrc = readFileSync(coreHandlerPath, "utf-8"); console.log("\n=== commands/handlers/core.ts integration ==="); -assertTrue( +assert.ok( coreHandlerSrc.includes('"visualize"'), "core.ts has visualize in subcommands array", ); -assertTrue( +assert.ok( coreHandlerSrc.includes("GSDVisualizerOverlay"), "core.ts imports GSDVisualizerOverlay", ); -assertTrue( +assert.ok( coreHandlerSrc.includes("handleVisualize"), "core.ts has handleVisualize handler", ); - -report(); diff --git a/src/resources/extensions/gsd/tests/visualizer-overlay.test.ts b/src/resources/extensions/gsd/tests/visualizer-overlay.test.ts index 13baf07e4..db3e18d4e 100644 --- a/src/resources/extensions/gsd/tests/visualizer-overlay.test.ts +++ b/src/resources/extensions/gsd/tests/visualizer-overlay.test.ts @@ -4,90 +4,90 @@ import { readFileSync } from "node:fs"; import { join, dirname } from "node:path"; import { fileURLToPath } from "node:url"; -import { createTestContext } from "./test-helpers.ts"; +import { test } from 'node:test'; +import assert from 'node:assert/strict'; const __dirname = dirname(fileURLToPath(import.meta.url)); -const { assertTrue, assertEq, report } = createTestContext(); const overlaySrc = readFileSync(join(__dirname, "..", "visualizer-overlay.ts"), "utf-8"); console.log("\n=== Overlay: Tab Configuration ==="); -assertTrue( +assert.ok( overlaySrc.includes("TAB_COUNT = 10"), "TAB_COUNT is 10", ); -assertTrue( +assert.ok( overlaySrc.includes('"1 Progress"'), "has Progress tab label", ); -assertTrue( +assert.ok( overlaySrc.includes('"2 Timeline"'), "has Timeline tab label", ); -assertTrue( +assert.ok( overlaySrc.includes('"3 Deps"'), "has Deps tab label", ); -assertTrue( +assert.ok( overlaySrc.includes('"5 Health"'), "has Health tab label", ); -assertTrue( +assert.ok( overlaySrc.includes('"6 Agent"'), "has Agent tab label", ); -assertTrue( +assert.ok( overlaySrc.includes('"7 Changes"'), "has Changes tab label", ); -assertTrue( +assert.ok( overlaySrc.includes('"8 Knowledge"'), "has Knowledge tab label", ); -assertTrue( +assert.ok( overlaySrc.includes('"9 Captures"'), "has Captures tab label", ); -assertTrue( +assert.ok( overlaySrc.includes('"0 Export"'), "has Export tab label", ); console.log("\n=== Overlay: Filter Mode ==="); -assertTrue( +assert.ok( overlaySrc.includes('filterMode = false'), "filterMode initialized to false", ); -assertTrue( +assert.ok( overlaySrc.includes('filterText = ""'), "filterText initialized to empty string", ); -assertTrue( +assert.ok( overlaySrc.includes('filterField:'), "has filterField state", ); // Filter mode entry via "/" -assertTrue( +assert.ok( overlaySrc.includes('data === "/"') || overlaySrc.includes("data === '/'"), "/ key enters filter mode", ); // Filter field cycling via "f" -assertTrue( +assert.ok( overlaySrc.includes('data === "f"') || overlaySrc.includes("data === 'f'"), "f key cycles filter field", ); @@ -95,143 +95,141 @@ assertTrue( console.log("\n=== Overlay: Tab Switching ==="); // Supports 1-9,0 keys -assertTrue( +assert.ok( overlaySrc.includes('"1234567890"'), "supports keys 1-9,0 for tab switching", ); // Tab wraps with TAB_COUNT -assertTrue( +assert.ok( overlaySrc.includes("% TAB_COUNT"), "tab key wraps around TAB_COUNT", ); -assertTrue( +assert.ok( overlaySrc.includes('Key.shift("tab")') || overlaySrc.includes("Key.shift('tab')"), "supports Shift+Tab for reverse tab switching", ); console.log("\n=== Overlay: Page/Half-Page Scroll ==="); -assertTrue( +assert.ok( overlaySrc.includes("Key.pageUp"), "has Key.pageUp handler", ); -assertTrue( +assert.ok( overlaySrc.includes("Key.pageDown"), "has Key.pageDown handler", ); -assertTrue( +assert.ok( overlaySrc.includes('Key.ctrl("u")'), "has Ctrl+U half-page scroll", ); -assertTrue( +assert.ok( overlaySrc.includes('Key.ctrl("d")'), "has Ctrl+D half-page scroll", ); console.log("\n=== Overlay: Mouse Support ==="); -assertTrue( +assert.ok( overlaySrc.includes("parseSGRMouse"), "has parseSGRMouse method", ); -assertTrue( +assert.ok( overlaySrc.includes("?1003h"), "enables mouse tracking in constructor", ); -assertTrue( +assert.ok( overlaySrc.includes("?1003l"), "disables mouse tracking in dispose", ); console.log("\n=== Overlay: Collapsible Milestones ==="); -assertTrue( +assert.ok( overlaySrc.includes("collapsedMilestones"), "has collapsedMilestones state", ); console.log("\n=== Overlay: Help Overlay ==="); -assertTrue( +assert.ok( overlaySrc.includes("showHelp"), "has showHelp state", ); -assertTrue( +assert.ok( overlaySrc.includes('data === "?"'), "? key toggles help", ); console.log("\n=== Overlay: Export Key Interception ==="); -assertTrue( +assert.ok( overlaySrc.includes("activeTab === 9"), "export key handling checks for tab 0 (index 9)", ); -assertTrue( +assert.ok( overlaySrc.includes('handleExportKey'), "has handleExportKey method", ); -assertTrue( +assert.ok( overlaySrc.includes('"m"') && overlaySrc.includes('"j"') && overlaySrc.includes('"s"'), "handles m, j, s keys for export", ); console.log("\n=== Overlay: Footer ==="); -assertTrue( +assert.ok( overlaySrc.includes("1-9,0"), "footer hint shows 1-9,0 tab range", ); -assertTrue( +assert.ok( overlaySrc.includes("PgUp/PgDn"), "footer hint mentions PgUp/PgDn", ); -assertTrue( +assert.ok( overlaySrc.includes("? help"), "footer hint mentions ? for help", ); console.log("\n=== Overlay: Scroll Offsets ==="); -assertTrue( +assert.ok( overlaySrc.includes(`new Array(TAB_COUNT).fill(0)`), "scroll offsets sized to TAB_COUNT", ); console.log("\n=== Overlay: Terminal Resize Handling ==="); -assertTrue( +assert.ok( overlaySrc.includes('resizeHandler'), "has resizeHandler property", ); -assertTrue( +assert.ok( overlaySrc.includes('"resize"'), "listens for resize events", ); -assertTrue( +assert.ok( overlaySrc.includes('removeListener("resize"'), "removes resize listener on dispose", ); console.log("\n=== Overlay: Shared Imports ==="); -assertTrue( +assert.ok( overlaySrc.includes('from "../shared/mod.js"'), "imports from shared barrel", ); - -report(); diff --git a/src/resources/extensions/gsd/tests/visualizer-views.test.ts b/src/resources/extensions/gsd/tests/visualizer-views.test.ts index e899cd379..9286a6660 100644 --- a/src/resources/extensions/gsd/tests/visualizer-views.test.ts +++ b/src/resources/extensions/gsd/tests/visualizer-views.test.ts @@ -14,9 +14,9 @@ import { renderHealthView, } from "../visualizer-views.js"; import type { VisualizerData } from "../visualizer-data.js"; -import { createTestContext } from "./test-helpers.ts"; +import { test } from 'node:test'; +import assert from 'node:assert/strict'; -const { assertEq, assertTrue, report } = createTestContext(); // ─── Mock theme ───────────────────────────────────────────────────────────── @@ -165,19 +165,19 @@ console.log("\n=== renderProgressView ==="); }); const lines = renderProgressView(data, mockTheme, 80); - assertTrue(lines.length > 0, "progress view produces output"); - assertTrue(lines.some(l => l.includes("M001")), "shows milestone M001"); - assertTrue(lines.some(l => l.includes("S01")), "shows slice S01"); - assertTrue(lines.some(l => l.includes("T01")), "shows task T01 for active slice"); - assertTrue(lines.some(l => l.includes("M002")), "shows milestone M002"); - assertTrue(lines.some(l => l.includes("depends on M001")), "shows dependency note"); - assertTrue(lines.some(l => l.includes("30m")), "shows task estimate"); - assertTrue(lines.some(l => l.includes("Feature Snapshot")), "shows stats header"); - assertTrue(lines.some(l => l.includes("Missing slices")), "shows missing slices count"); - assertTrue(lines.some(l => l.includes("State Engine")), "shows missing slice preview"); - assertTrue(lines.some(l => l.includes("Updated (last 7 days)")), "shows updated count"); - assertTrue(lines.some(l => l.includes("Recent completions")), "shows recent completions section"); - assertTrue(lines.some(l => l.includes("Core structures assembled")), "shows recent one-liner entry"); + assert.ok(lines.length > 0, "progress view produces output"); + assert.ok(lines.some(l => l.includes("M001")), "shows milestone M001"); + assert.ok(lines.some(l => l.includes("S01")), "shows slice S01"); + assert.ok(lines.some(l => l.includes("T01")), "shows task T01 for active slice"); + assert.ok(lines.some(l => l.includes("M002")), "shows milestone M002"); + assert.ok(lines.some(l => l.includes("depends on M001")), "shows dependency note"); + assert.ok(lines.some(l => l.includes("30m")), "shows task estimate"); + assert.ok(lines.some(l => l.includes("Feature Snapshot")), "shows stats header"); + assert.ok(lines.some(l => l.includes("Missing slices")), "shows missing slices count"); + assert.ok(lines.some(l => l.includes("State Engine")), "shows missing slice preview"); + assert.ok(lines.some(l => l.includes("Updated (last 7 days)")), "shows updated count"); + assert.ok(lines.some(l => l.includes("Recent completions")), "shows recent completions section"); + assert.ok(lines.some(l => l.includes("Core structures assembled")), "shows recent one-liner entry"); } { @@ -211,10 +211,10 @@ console.log("\n=== renderProgressView ==="); }); const lines = renderProgressView(data, mockTheme, 80); - assertTrue(lines.some(l => l.includes("Discussion Status")), "shows discussion section"); - assertTrue(lines.some(l => l.includes("Discussed: 1")), "counts discussed milestones"); - assertTrue(lines.some(l => l.includes("Draft")), "shows draft badge"); - assertTrue(lines.some(l => l.includes("Pending")), "shows pending badge"); + assert.ok(lines.some(l => l.includes("Discussion Status")), "shows discussion section"); + assert.ok(lines.some(l => l.includes("Discussed: 1")), "counts discussed milestones"); + assert.ok(lines.some(l => l.includes("Draft")), "shows draft badge"); + assert.ok(lines.some(l => l.includes("Pending")), "shows pending badge"); } // Verification badges @@ -239,14 +239,14 @@ console.log("\n=== renderProgressView ==="); const lines = renderProgressView(data, mockTheme, 80); // The verification badge should show check mark and warning - assertTrue(lines.some(l => l.includes("S01")), "shows slice with verification"); + assert.ok(lines.some(l => l.includes("S01")), "shows slice with verification"); } { const data = makeVisualizerData({ milestones: [] }); const lines = renderProgressView(data, mockTheme, 80); - assertTrue(lines.some(l => l.includes("Feature Snapshot")), "shows stats snapshot even when no milestones"); - assertTrue(lines.some(l => l.includes("Missing slices")), "reports missing slices count"); + assert.ok(lines.some(l => l.includes("Feature Snapshot")), "shows stats snapshot even when no milestones"); + assert.ok(lines.some(l => l.includes("Missing slices")), "reports missing slices count"); } // ─── Risk Heatmap ─────────────────────────────────────────────────────────── @@ -272,9 +272,9 @@ console.log("\n=== Risk Heatmap ==="); }); const lines = renderProgressView(data, mockTheme, 80); - assertTrue(lines.some(l => l.includes("Risk Heatmap")), "heatmap header present"); - assertTrue(lines.some(l => l.includes("1 low, 1 med, 2 high")), "risk summary counts"); - assertTrue(lines.some(l => l.includes("1 high-risk not started")), "high-risk not started warning"); + assert.ok(lines.some(l => l.includes("Risk Heatmap")), "heatmap header present"); + assert.ok(lines.some(l => l.includes("1 low, 1 med, 2 high")), "risk summary counts"); + assert.ok(lines.some(l => l.includes("1 high-risk not started")), "high-risk not started warning"); } // ─── Search/Filter ────────────────────────────────────────────────────────── @@ -305,11 +305,11 @@ console.log("\n=== Search/Filter ==="); }); const filtered = renderProgressView(data, mockTheme, 80, { text: "auth", field: "all" }); - assertTrue(filtered.some(l => l.includes("M001")), "filter shows matching milestone"); - assertTrue(filtered.some(l => l.includes("Filter (all): auth")), "filter indicator present"); + assert.ok(filtered.some(l => l.includes("M001")), "filter shows matching milestone"); + assert.ok(filtered.some(l => l.includes("Filter (all): auth")), "filter indicator present"); const riskFiltered = renderProgressView(data, mockTheme, 80, { text: "high", field: "risk" }); - assertTrue(riskFiltered.some(l => l.includes("M001")), "risk filter shows milestone with high-risk slice"); + assert.ok(riskFiltered.some(l => l.includes("M001")), "risk filter shows milestone with high-risk slice"); } // ─── renderDepsView ───────────────────────────────────────────────────────── @@ -354,13 +354,13 @@ console.log("\n=== renderDepsView ==="); }); const lines = renderDepsView(data, mockTheme, 80); - assertTrue(lines.length > 0, "deps view produces output"); - assertTrue(lines.some(l => l.includes("M001") && l.includes("M002")), "shows milestone dep edge"); - assertTrue(lines.some(l => l.includes("S01") && l.includes("S02")), "shows slice dep edge"); - assertTrue(lines.some(l => l.includes("Critical Path")), "shows critical path section"); - assertTrue(lines.some(l => l.includes("[CRITICAL]")), "shows CRITICAL badge"); - assertTrue(lines.some(l => l.includes("Data Flow")), "shows data flow section"); - assertTrue(lines.some(l => l.includes("api-types")), "shows provides artifact"); + assert.ok(lines.length > 0, "deps view produces output"); + assert.ok(lines.some(l => l.includes("M001") && l.includes("M002")), "shows milestone dep edge"); + assert.ok(lines.some(l => l.includes("S01") && l.includes("S02")), "shows slice dep edge"); + assert.ok(lines.some(l => l.includes("Critical Path")), "shows critical path section"); + assert.ok(lines.some(l => l.includes("[CRITICAL]")), "shows CRITICAL badge"); + assert.ok(lines.some(l => l.includes("Data Flow")), "shows data flow section"); + assert.ok(lines.some(l => l.includes("api-types")), "shows provides artifact"); } { @@ -371,7 +371,7 @@ console.log("\n=== renderDepsView ==="); }); const lines = renderDepsView(data, mockTheme, 80); - assertTrue(lines.some(l => l.includes("No milestone dependencies")), "shows no-deps message"); + assert.ok(lines.some(l => l.includes("No milestone dependencies")), "shows no-deps message"); } // ─── renderMetricsView ────────────────────────────────────────────────────── @@ -422,21 +422,21 @@ console.log("\n=== renderMetricsView ==="); }); const lines = renderMetricsView(data, mockTheme, 80); - assertTrue(lines.length > 0, "metrics view produces output"); - assertTrue(lines.some(l => l.includes("$2.50")), "shows total cost"); - assertTrue(lines.some(l => l.includes("execution")), "shows phase name"); - assertTrue(lines.some(l => l.includes("claude-opus-4-6")), "shows model name"); - assertTrue(lines.some(l => l.includes("By Tier")), "shows tier breakdown section"); - assertTrue(lines.some(l => l.includes("standard")), "shows tier name"); - assertTrue(lines.some(l => l.includes("Dynamic routing")), "shows tier savings line"); - assertTrue(lines.some(l => l.includes("Tools: 15")), "shows tool call count"); - assertTrue(lines.some(l => l.includes("10") && l.includes("sent")), "shows message counts"); + assert.ok(lines.length > 0, "metrics view produces output"); + assert.ok(lines.some(l => l.includes("$2.50")), "shows total cost"); + assert.ok(lines.some(l => l.includes("execution")), "shows phase name"); + assert.ok(lines.some(l => l.includes("claude-opus-4-6")), "shows model name"); + assert.ok(lines.some(l => l.includes("By Tier")), "shows tier breakdown section"); + assert.ok(lines.some(l => l.includes("standard")), "shows tier name"); + assert.ok(lines.some(l => l.includes("Dynamic routing")), "shows tier savings line"); + assert.ok(lines.some(l => l.includes("Tools: 15")), "shows tool call count"); + assert.ok(lines.some(l => l.includes("10") && l.includes("sent")), "shows message counts"); } { const data = makeVisualizerData({ totals: null }); const lines = renderMetricsView(data, mockTheme, 80); - assertTrue(lines.some(l => l.includes("No metrics data")), "shows no-data message"); + assert.ok(lines.some(l => l.includes("No metrics data")), "shows no-data message"); } // ─── renderTimelineView ───────────────────────────────────────────────────── @@ -464,16 +464,16 @@ console.log("\n=== renderTimelineView ==="); }); const listLines = renderTimelineView(data, mockTheme, 80); - assertTrue(listLines.length >= 1, "list view produces lines"); - assertTrue(listLines.some(l => l.includes("execute-task")), "shows unit type"); - assertTrue(listLines.some(l => l.includes("[standard]")), "shows tier in timeline"); - assertTrue(listLines.some(l => l.includes("opus-4-6")), "shows shortened model"); + assert.ok(listLines.length >= 1, "list view produces lines"); + assert.ok(listLines.some(l => l.includes("execute-task")), "shows unit type"); + assert.ok(listLines.some(l => l.includes("[standard]")), "shows tier in timeline"); + assert.ok(listLines.some(l => l.includes("opus-4-6")), "shows shortened model"); } { const data = makeVisualizerData({ units: [] }); const lines = renderTimelineView(data, mockTheme, 80); - assertTrue(lines.some(l => l.includes("No execution history")), "shows empty message"); + assert.ok(lines.some(l => l.includes("No execution history")), "shows empty message"); } // ─── renderAgentView ──────────────────────────────────────────────────────── @@ -514,17 +514,17 @@ console.log("\n=== renderAgentView ==="); }); const lines = renderAgentView(data, mockTheme, 80); - assertTrue(lines.length > 0, "agent view produces output"); - assertTrue(lines.some(l => l.includes("ACTIVE")), "shows active status"); - assertTrue(lines.some(l => l.includes("Pressure")), "shows pressure section"); - assertTrue(lines.some(l => l.includes("15.5%")), "shows truncation rate"); - assertTrue(lines.some(l => l.includes("Pending captures: 3")), "shows pending captures"); + assert.ok(lines.length > 0, "agent view produces output"); + assert.ok(lines.some(l => l.includes("ACTIVE")), "shows active status"); + assert.ok(lines.some(l => l.includes("Pressure")), "shows pressure section"); + assert.ok(lines.some(l => l.includes("15.5%")), "shows truncation rate"); + assert.ok(lines.some(l => l.includes("Pending captures: 3")), "shows pending captures"); } { const data = makeVisualizerData({ agentActivity: null }); const lines = renderAgentView(data, mockTheme, 80); - assertTrue(lines.some(l => l.includes("No agent activity")), "shows no-activity message"); + assert.ok(lines.some(l => l.includes("No agent activity")), "shows no-activity message"); } // ─── renderChangelogView ──────────────────────────────────────────────────── @@ -559,17 +559,17 @@ console.log("\n=== renderChangelogView ==="); }); const lines = renderChangelogView(data, mockTheme, 80); - assertTrue(lines.some(l => l.includes("M001/S01")), "shows slice reference"); - assertTrue(lines.some(l => l.includes("Decisions:")), "shows decisions section"); - assertTrue(lines.some(l => l.includes("RS256")), "shows decision content"); - assertTrue(lines.some(l => l.includes("Patterns:")), "shows patterns section"); - assertTrue(lines.some(l => l.includes("Repository pattern")), "shows pattern content"); + assert.ok(lines.some(l => l.includes("M001/S01")), "shows slice reference"); + assert.ok(lines.some(l => l.includes("Decisions:")), "shows decisions section"); + assert.ok(lines.some(l => l.includes("RS256")), "shows decision content"); + assert.ok(lines.some(l => l.includes("Patterns:")), "shows patterns section"); + assert.ok(lines.some(l => l.includes("Repository pattern")), "shows pattern content"); } { const data = makeVisualizerData({ changelog: { entries: [] } }); const lines = renderChangelogView(data, mockTheme, 80); - assertTrue(lines.some(l => l.includes("No completed slices")), "shows empty state"); + assert.ok(lines.some(l => l.includes("No completed slices")), "shows empty state"); } // ─── renderExportView ─────────────────────────────────────────────────────── @@ -579,10 +579,10 @@ console.log("\n=== renderExportView ==="); { const data = makeVisualizerData(); const lines = renderExportView(data, mockTheme, 80); - assertTrue(lines.some(l => l.includes("Export Options")), "shows export header"); - assertTrue(lines.some(l => l.includes("[m]")), "shows markdown option"); - assertTrue(lines.some(l => l.includes("[j]")), "shows json option"); - assertTrue(lines.some(l => l.includes("[s]")), "shows snapshot option"); + assert.ok(lines.some(l => l.includes("Export Options")), "shows export header"); + assert.ok(lines.some(l => l.includes("[m]")), "shows markdown option"); + assert.ok(lines.some(l => l.includes("[j]")), "shows json option"); + assert.ok(lines.some(l => l.includes("[s]")), "shows snapshot option"); } // ─── renderKnowledgeView ──────────────────────────────────────────────────── @@ -600,13 +600,13 @@ console.log("\n=== renderKnowledgeView ==="); }); const lines = renderKnowledgeView(data, mockTheme, 80); - assertTrue(lines.some(l => l.includes("Rules")), "shows rules section"); - assertTrue(lines.some(l => l.includes("K001")), "shows rule ID"); - assertTrue(lines.some(l => l.includes("Always use transactions")), "shows rule content"); - assertTrue(lines.some(l => l.includes("Patterns")), "shows patterns section"); - assertTrue(lines.some(l => l.includes("P001")), "shows pattern ID"); - assertTrue(lines.some(l => l.includes("Lessons Learned")), "shows lessons section"); - assertTrue(lines.some(l => l.includes("L001")), "shows lesson ID"); + assert.ok(lines.some(l => l.includes("Rules")), "shows rules section"); + assert.ok(lines.some(l => l.includes("K001")), "shows rule ID"); + assert.ok(lines.some(l => l.includes("Always use transactions")), "shows rule content"); + assert.ok(lines.some(l => l.includes("Patterns")), "shows patterns section"); + assert.ok(lines.some(l => l.includes("P001")), "shows pattern ID"); + assert.ok(lines.some(l => l.includes("Lessons Learned")), "shows lessons section"); + assert.ok(lines.some(l => l.includes("L001")), "shows lesson ID"); } { @@ -614,7 +614,7 @@ console.log("\n=== renderKnowledgeView ==="); knowledge: { exists: false, rules: [], patterns: [], lessons: [] }, }); const lines = renderKnowledgeView(data, mockTheme, 80); - assertTrue(lines.some(l => l.includes("No KNOWLEDGE.md found")), "shows no-knowledge message"); + assert.ok(lines.some(l => l.includes("No KNOWLEDGE.md found")), "shows no-knowledge message"); } // ─── renderCapturesView ───────────────────────────────────────────────────── @@ -635,11 +635,11 @@ console.log("\n=== renderCapturesView ==="); }); const lines = renderCapturesView(data, mockTheme, 80); - assertTrue(lines.some(l => l.includes("3") && l.includes("total")), "shows total count"); - assertTrue(lines.some(l => l.includes("1") && l.includes("pending")), "shows pending count"); - assertTrue(lines.some(l => l.includes("CAP-abc123")), "shows capture ID"); - assertTrue(lines.some(l => l.includes("(inject)")), "shows classification badge"); - assertTrue(lines.some(l => l.includes("[pending]")), "shows status badge"); + assert.ok(lines.some(l => l.includes("3") && l.includes("total")), "shows total count"); + assert.ok(lines.some(l => l.includes("1") && l.includes("pending")), "shows pending count"); + assert.ok(lines.some(l => l.includes("CAP-abc123")), "shows capture ID"); + assert.ok(lines.some(l => l.includes("(inject)")), "shows classification badge"); + assert.ok(lines.some(l => l.includes("[pending]")), "shows status badge"); } { @@ -647,7 +647,7 @@ console.log("\n=== renderCapturesView ==="); captures: { entries: [], pendingCount: 0, totalCount: 0 }, }); const lines = renderCapturesView(data, mockTheme, 80); - assertTrue(lines.some(l => l.includes("No captures recorded")), "shows empty state"); + assert.ok(lines.some(l => l.includes("No captures recorded")), "shows empty state"); } // ─── renderHealthView ─────────────────────────────────────────────────────── @@ -682,17 +682,17 @@ console.log("\n=== renderHealthView ==="); }); const lines = renderHealthView(data, mockTheme, 80); - assertTrue(lines.some(l => l.includes("Budget")), "shows budget section"); - assertTrue(lines.some(l => l.includes("Ceiling")), "shows budget ceiling"); - assertTrue(lines.some(l => l.includes("$20.00")), "shows ceiling amount"); - assertTrue(lines.some(l => l.includes("Pressure")), "shows pressure section"); - assertTrue(lines.some(l => l.includes("30.0%")), "shows truncation rate"); - assertTrue(lines.some(l => l.includes("Routing")), "shows routing section"); - assertTrue(lines.some(l => l.includes("standard")), "shows tier name"); - assertTrue(lines.some(l => l.includes("2 downgraded")), "shows downgraded count"); - assertTrue(lines.some(l => l.includes("Dynamic routing")), "shows savings line"); - assertTrue(lines.some(l => l.includes("Session")), "shows session section"); - assertTrue(lines.some(l => l.includes("Tool calls: 50")), "shows tool calls"); + assert.ok(lines.some(l => l.includes("Budget")), "shows budget section"); + assert.ok(lines.some(l => l.includes("Ceiling")), "shows budget ceiling"); + assert.ok(lines.some(l => l.includes("$20.00")), "shows ceiling amount"); + assert.ok(lines.some(l => l.includes("Pressure")), "shows pressure section"); + assert.ok(lines.some(l => l.includes("30.0%")), "shows truncation rate"); + assert.ok(lines.some(l => l.includes("Routing")), "shows routing section"); + assert.ok(lines.some(l => l.includes("standard")), "shows tier name"); + assert.ok(lines.some(l => l.includes("2 downgraded")), "shows downgraded count"); + assert.ok(lines.some(l => l.includes("Dynamic routing")), "shows savings line"); + assert.ok(lines.some(l => l.includes("Session")), "shows session section"); + assert.ok(lines.some(l => l.includes("Tool calls: 50")), "shows tool calls"); } { @@ -709,10 +709,8 @@ console.log("\n=== renderHealthView ==="); }); const lines = renderHealthView(data, mockTheme, 80); - assertTrue(lines.some(l => l.includes("No budget ceiling set")), "shows no-ceiling message"); - assertTrue(lines.some(l => l.includes("compact")), "shows token profile"); + assert.ok(lines.some(l => l.includes("No budget ceiling set")), "shows no-ceiling message"); + assert.ok(lines.some(l => l.includes("compact")), "shows token profile"); } // ─── Report ───────────────────────────────────────────────────────────────── - -report(); diff --git a/src/resources/extensions/gsd/tests/windows-path-normalization.test.ts b/src/resources/extensions/gsd/tests/windows-path-normalization.test.ts index 3b119b426..419c1cf7a 100644 --- a/src/resources/extensions/gsd/tests/windows-path-normalization.test.ts +++ b/src/resources/extensions/gsd/tests/windows-path-normalization.test.ts @@ -6,9 +6,9 @@ * strips backslashes (escape characters), producing `C:Usersuserproject`. */ -import { createTestContext } from "./test-helpers.ts"; +import { test } from 'node:test'; +import assert from 'node:assert/strict'; -const { assertEq, assertTrue, report } = createTestContext(); // ─── shellEscape + path normalization ────────────────────────────────────── @@ -25,42 +25,42 @@ function bashPath(p: string): string { console.log("\n=== Windows backslash path normalization (#1436) ==="); // Backslash paths are converted to forward slashes -assertEq( +assert.deepStrictEqual( bashPath("C:\\Users\\user\\project"), "'C:/Users/user/project'", "backslash path normalised to forward slashes in shell-escaped string", ); // Unix paths pass through unchanged -assertEq( +assert.deepStrictEqual( bashPath("/home/user/project"), "'/home/user/project'", "Unix path unchanged", ); // Mixed separators are normalised -assertEq( +assert.deepStrictEqual( bashPath("C:\\Users/user\\project/src"), "'C:/Users/user/project/src'", "mixed separators normalised", ); // Paths with single quotes are still properly escaped -assertEq( +assert.deepStrictEqual( bashPath("C:\\Users\\o'brien\\project"), "'C:/Users/o'\\''brien/project'", "single quote in path is escaped after normalisation", ); // UNC paths -assertEq( +assert.deepStrictEqual( bashPath("\\\\server\\share\\dir"), "'//server/share/dir'", "UNC path normalised", ); // Empty string -assertEq( +assert.deepStrictEqual( bashPath(""), "''", "empty string handled", @@ -72,14 +72,14 @@ console.log("\n=== cd command construction with normalised paths ==="); const windowsCwd = "C:\\Users\\user\\project\\.gsd\\worktrees\\M001"; const cdCommand = `cd ${bashPath(windowsCwd)}`; -assertEq( +assert.deepStrictEqual( cdCommand, "cd 'C:/Users/user/project/.gsd/worktrees/M001'", "cd command uses forward slashes for Windows worktree path", ); // Verify the mangled form from #1436 is NOT produced -assertTrue( +assert.ok( !cdCommand.includes("C:Users"), "mangled path C:Usersuserproject must not appear", ); @@ -90,10 +90,8 @@ console.log("\n=== teardown orphan warning path formatting ==="); const windowsWtDir = "C:\\Users\\user\\project\\.gsd\\worktrees\\M001"; const helpCommand = `rm -rf "${windowsWtDir.replaceAll("\\", "/")}"`; -assertEq( +assert.deepStrictEqual( helpCommand, 'rm -rf "C:/Users/user/project/.gsd/worktrees/M001"', "orphan cleanup help command uses forward slashes", ); - -report(); diff --git a/src/resources/extensions/gsd/tests/worker-registry.test.ts b/src/resources/extensions/gsd/tests/worker-registry.test.ts index 3f09981ad..ac99e6a9a 100644 --- a/src/resources/extensions/gsd/tests/worker-registry.test.ts +++ b/src/resources/extensions/gsd/tests/worker-registry.test.ts @@ -5,7 +5,8 @@ * and the hasActiveWorkers() status check. */ -import { createTestContext } from './test-helpers.ts'; +import { test } from 'node:test'; +import assert from 'node:assert/strict'; import { registerWorker, updateWorker, @@ -15,7 +16,6 @@ import { resetWorkerRegistry, } from '../../subagent/worker-registry.ts'; -const { assertEq, assertTrue, report } = createTestContext(); // ─── Setup ──────────────────────────────────────────────────────────────────── @@ -28,15 +28,15 @@ console.log("\n=== Worker Registration ==="); { resetWorkerRegistry(); const id = registerWorker("scout", "Explore codebase", 0, 3, "batch-1"); - assertTrue(id.startsWith("worker-"), "worker ID has correct prefix"); + assert.ok(id.startsWith("worker-"), "worker ID has correct prefix"); const workers = getActiveWorkers(); - assertEq(workers.length, 1, "one worker registered"); - assertEq(workers[0].agent, "scout", "worker agent name correct"); - assertEq(workers[0].task, "Explore codebase", "worker task correct"); - assertEq(workers[0].status, "running", "worker starts as running"); - assertEq(workers[0].index, 0, "worker index correct"); - assertEq(workers[0].batchSize, 3, "worker batch size correct"); - assertEq(workers[0].batchId, "batch-1", "worker batch ID correct"); + assert.deepStrictEqual(workers.length, 1, "one worker registered"); + assert.deepStrictEqual(workers[0].agent, "scout", "worker agent name correct"); + assert.deepStrictEqual(workers[0].task, "Explore codebase", "worker task correct"); + assert.deepStrictEqual(workers[0].status, "running", "worker starts as running"); + assert.deepStrictEqual(workers[0].index, 0, "worker index correct"); + assert.deepStrictEqual(workers[0].batchSize, 3, "worker batch size correct"); + assert.deepStrictEqual(workers[0].batchId, "batch-1", "worker batch ID correct"); } // ─── Multiple workers in a batch ────────────────────────────────────────────── @@ -50,14 +50,14 @@ console.log("\n=== Multiple Workers in a Batch ==="); const id3 = registerWorker("worker", "Task C", 2, 3, "batch-2"); const workers = getActiveWorkers(); - assertEq(workers.length, 3, "three workers registered"); - assertTrue(hasActiveWorkers(), "has active workers"); + assert.deepStrictEqual(workers.length, 3, "three workers registered"); + assert.ok(hasActiveWorkers(), "has active workers"); const batches = getWorkerBatches(); - assertEq(batches.size, 1, "one batch"); + assert.deepStrictEqual(batches.size, 1, "one batch"); const batch = batches.get("batch-2"); - assertTrue(batch !== undefined, "batch-2 exists"); - assertEq(batch!.length, 3, "batch has 3 workers"); + assert.ok(batch !== undefined, "batch-2 exists"); + assert.deepStrictEqual(batch!.length, 3, "batch has 3 workers"); } // ─── Worker status updates ──────────────────────────────────────────────────── @@ -72,11 +72,11 @@ console.log("\n=== Worker Status Updates ==="); updateWorker(id1, "completed"); const workers = getActiveWorkers(); const w1 = workers.find(w => w.id === id1); - assertEq(w1?.status, "completed", "worker 1 marked completed"); + assert.deepStrictEqual(w1?.status, "completed", "worker 1 marked completed"); const w2 = workers.find(w => w.id === id2); - assertEq(w2?.status, "running", "worker 2 still running"); - assertTrue(hasActiveWorkers(), "still has active workers (worker 2 running)"); + assert.deepStrictEqual(w2?.status, "running", "worker 2 still running"); + assert.ok(hasActiveWorkers(), "still has active workers (worker 2 running)"); } // ─── Failed worker ──────────────────────────────────────────────────────────── @@ -88,7 +88,7 @@ console.log("\n=== Failed Worker ==="); const id = registerWorker("scout", "Task A", 0, 1, "batch-4"); updateWorker(id, "failed"); const workers = getActiveWorkers(); - assertEq(workers[0].status, "failed", "worker marked failed"); + assert.deepStrictEqual(workers[0].status, "failed", "worker marked failed"); } // ─── Multiple batches ───────────────────────────────────────────────────────── @@ -102,9 +102,9 @@ console.log("\n=== Multiple Batches ==="); registerWorker("researcher", "Task C", 0, 1, "batch-6"); const batches = getWorkerBatches(); - assertEq(batches.size, 2, "two batches"); - assertEq(batches.get("batch-5")!.length, 2, "batch-5 has 2 workers"); - assertEq(batches.get("batch-6")!.length, 1, "batch-6 has 1 worker"); + assert.deepStrictEqual(batches.size, 2, "two batches"); + assert.deepStrictEqual(batches.get("batch-5")!.length, 2, "batch-5 has 2 workers"); + assert.deepStrictEqual(batches.get("batch-6")!.length, 1, "batch-6 has 1 worker"); } // ─── hasActiveWorkers with all completed ────────────────────────────────────── @@ -117,7 +117,7 @@ console.log("\n=== hasActiveWorkers — all completed ==="); const id2 = registerWorker("worker", "Task B", 1, 2, "batch-7"); updateWorker(id1, "completed"); updateWorker(id2, "completed"); - assertTrue(!hasActiveWorkers(), "no active workers when all completed"); + assert.ok(!hasActiveWorkers(), "no active workers when all completed"); } // ─── Reset clears everything ───────────────────────────────────────────────── @@ -126,10 +126,10 @@ console.log("\n=== Reset ==="); { registerWorker("scout", "Task", 0, 1, "batch-8"); - assertTrue(getActiveWorkers().length > 0, "workers exist before reset"); + assert.ok(getActiveWorkers().length > 0, "workers exist before reset"); resetWorkerRegistry(); - assertEq(getActiveWorkers().length, 0, "no workers after reset"); - assertTrue(!hasActiveWorkers(), "hasActiveWorkers false after reset"); + assert.deepStrictEqual(getActiveWorkers().length, 0, "no workers after reset"); + assert.ok(!hasActiveWorkers(), "hasActiveWorkers false after reset"); } // ─── Update non-existent worker is no-op ────────────────────────────────────── @@ -140,9 +140,7 @@ console.log("\n=== Update non-existent worker ==="); resetWorkerRegistry(); // Should not throw updateWorker("nonexistent-id", "completed"); - assertEq(getActiveWorkers().length, 0, "no workers created by updating nonexistent"); + assert.deepStrictEqual(getActiveWorkers().length, 0, "no workers created by updating nonexistent"); } // ─── Summary ────────────────────────────────────────────────────────────────── - -report(); diff --git a/src/resources/extensions/gsd/tests/workflow-templates.test.ts b/src/resources/extensions/gsd/tests/workflow-templates.test.ts index 05a169dce..3aa0c9673 100644 --- a/src/resources/extensions/gsd/tests/workflow-templates.test.ts +++ b/src/resources/extensions/gsd/tests/workflow-templates.test.ts @@ -2,7 +2,8 @@ // // Tests registry loading, template resolution, auto-detection, and listing. -import { createTestContext } from './test-helpers.ts'; +import { test } from 'node:test'; +import assert from 'node:assert/strict'; import { loadRegistry, resolveByName, @@ -12,7 +13,6 @@ import { loadWorkflowTemplate, } from '../workflow-templates.ts'; -const { assertEq, assertTrue, assertMatch, report } = createTestContext(); // ═══════════════════════════════════════════════════════════════════════════ // Registry Loading @@ -22,23 +22,23 @@ console.log('\n── Registry Loading ──'); { const registry = loadRegistry(); - assertTrue(registry !== null, 'Registry should load'); - assertEq(registry.version, 1, 'Registry version should be 1'); - assertTrue(Object.keys(registry.templates).length >= 8, 'Should have at least 8 templates'); + assert.ok(registry !== null, 'Registry should load'); + assert.deepStrictEqual(registry.version, 1, 'Registry version should be 1'); + assert.ok(Object.keys(registry.templates).length >= 8, 'Should have at least 8 templates'); // Verify required template keys exist const expectedIds = ['full-project', 'bugfix', 'small-feature', 'refactor', 'spike', 'hotfix', 'security-audit', 'dep-upgrade']; for (const id of expectedIds) { - assertTrue(id in registry.templates, `Template "${id}" should exist in registry`); + assert.ok(id in registry.templates, `Template "${id}" should exist in registry`); } // Verify each template has required fields for (const [id, entry] of Object.entries(registry.templates)) { - assertTrue(typeof entry.name === 'string' && entry.name.length > 0, `${id}: name should be non-empty string`); - assertTrue(typeof entry.description === 'string' && entry.description.length > 0, `${id}: description should be non-empty`); - assertTrue(typeof entry.file === 'string' && entry.file.endsWith('.md'), `${id}: file should be a .md path`); - assertTrue(Array.isArray(entry.phases) && entry.phases.length > 0, `${id}: phases should be non-empty array`); - assertTrue(Array.isArray(entry.triggers) && entry.triggers.length > 0, `${id}: triggers should be non-empty array`); + assert.ok(typeof entry.name === 'string' && entry.name.length > 0, `${id}: name should be non-empty string`); + assert.ok(typeof entry.description === 'string' && entry.description.length > 0, `${id}: description should be non-empty`); + assert.ok(typeof entry.file === 'string' && entry.file.endsWith('.md'), `${id}: file should be a .md path`); + assert.ok(Array.isArray(entry.phases) && entry.phases.length > 0, `${id}: phases should be non-empty array`); + assert.ok(Array.isArray(entry.triggers) && entry.triggers.length > 0, `${id}: triggers should be non-empty array`); } } @@ -51,31 +51,31 @@ console.log('\n── Resolve by Name ──'); { // Exact match const bugfix = resolveByName('bugfix'); - assertTrue(bugfix !== null, 'Should resolve "bugfix"'); - assertEq(bugfix!.id, 'bugfix', 'ID should be "bugfix"'); - assertEq(bugfix!.confidence, 'exact', 'Exact name should have exact confidence'); + assert.ok(bugfix !== null, 'Should resolve "bugfix"'); + assert.deepStrictEqual(bugfix!.id, 'bugfix', 'ID should be "bugfix"'); + assert.deepStrictEqual(bugfix!.confidence, 'exact', 'Exact name should have exact confidence'); // Case-insensitive name match const spike = resolveByName('Research Spike'); - assertTrue(spike !== null, 'Should resolve "Research Spike" by name'); - assertEq(spike!.id, 'spike', 'Should resolve to spike'); + assert.ok(spike !== null, 'Should resolve "Research Spike" by name'); + assert.deepStrictEqual(spike!.id, 'spike', 'Should resolve to spike'); // Alias match const bug = resolveByName('bug'); - assertTrue(bug !== null, 'Should resolve "bug" alias'); - assertEq(bug!.id, 'bugfix', 'Alias "bug" should map to bugfix'); + assert.ok(bug !== null, 'Should resolve "bug" alias'); + assert.deepStrictEqual(bug!.id, 'bugfix', 'Alias "bug" should map to bugfix'); const feat = resolveByName('feat'); - assertTrue(feat !== null, 'Should resolve "feat" alias'); - assertEq(feat!.id, 'small-feature', 'Alias "feat" should map to small-feature'); + assert.ok(feat !== null, 'Should resolve "feat" alias'); + assert.deepStrictEqual(feat!.id, 'small-feature', 'Alias "feat" should map to small-feature'); const deps = resolveByName('deps'); - assertTrue(deps !== null, 'Should resolve "deps" alias'); - assertEq(deps!.id, 'dep-upgrade', 'Alias "deps" should map to dep-upgrade'); + assert.ok(deps !== null, 'Should resolve "deps" alias'); + assert.deepStrictEqual(deps!.id, 'dep-upgrade', 'Alias "deps" should map to dep-upgrade'); // No match const missing = resolveByName('nonexistent-template'); - assertTrue(missing === null, 'Should return null for unknown template'); + assert.ok(missing === null, 'Should return null for unknown template'); } // ═══════════════════════════════════════════════════════════════════════════ @@ -87,32 +87,32 @@ console.log('\n── Auto-Detection ──'); { // Should detect bugfix from "fix" keyword const fixMatches = autoDetect('fix the login button'); - assertTrue(fixMatches.length > 0, 'Should detect matches for "fix the login button"'); - assertTrue(fixMatches.some(m => m.id === 'bugfix'), 'Should include bugfix in matches'); + assert.ok(fixMatches.length > 0, 'Should detect matches for "fix the login button"'); + assert.ok(fixMatches.some(m => m.id === 'bugfix'), 'Should include bugfix in matches'); // Should detect spike from "research" keyword const researchMatches = autoDetect('research authentication libraries'); - assertTrue(researchMatches.length > 0, 'Should detect matches for "research"'); - assertTrue(researchMatches.some(m => m.id === 'spike'), 'Should include spike in matches'); + assert.ok(researchMatches.length > 0, 'Should detect matches for "research"'); + assert.ok(researchMatches.some(m => m.id === 'spike'), 'Should include spike in matches'); // Should detect hotfix from "urgent" keyword const urgentMatches = autoDetect('urgent production is down'); - assertTrue(urgentMatches.length > 0, 'Should detect matches for "urgent"'); - assertTrue(urgentMatches.some(m => m.id === 'hotfix'), 'Should include hotfix in matches'); + assert.ok(urgentMatches.length > 0, 'Should detect matches for "urgent"'); + assert.ok(urgentMatches.some(m => m.id === 'hotfix'), 'Should include hotfix in matches'); // Should detect dep-upgrade from "upgrade" keyword const upgradeMatches = autoDetect('upgrade react to v19'); - assertTrue(upgradeMatches.length > 0, 'Should detect matches for "upgrade"'); - assertTrue(upgradeMatches.some(m => m.id === 'dep-upgrade'), 'Should include dep-upgrade in matches'); + assert.ok(upgradeMatches.length > 0, 'Should detect matches for "upgrade"'); + assert.ok(upgradeMatches.some(m => m.id === 'dep-upgrade'), 'Should include dep-upgrade in matches'); // Multi-word triggers should score higher const projectMatches = autoDetect('create a new project from scratch'); const projectMatch = projectMatches.find(m => m.id === 'full-project'); - assertTrue(projectMatch !== undefined, 'Should detect full-project for "from scratch"'); + assert.ok(projectMatch !== undefined, 'Should detect full-project for "from scratch"'); // Empty input should return no matches const emptyMatches = autoDetect(''); - assertEq(emptyMatches.length, 0, 'Empty input should return no matches'); + assert.deepStrictEqual(emptyMatches.length, 0, 'Empty input should return no matches'); } // ═══════════════════════════════════════════════════════════════════════════ @@ -123,11 +123,11 @@ console.log('\n── List Templates ──'); { const output = listTemplates(); - assertTrue(output.includes('Workflow Templates'), 'Should have header'); - assertTrue(output.includes('bugfix'), 'Should list bugfix'); - assertTrue(output.includes('spike'), 'Should list spike'); - assertTrue(output.includes('hotfix'), 'Should list hotfix'); - assertTrue(output.includes('/gsd start'), 'Should include usage hint'); + assert.ok(output.includes('Workflow Templates'), 'Should have header'); + assert.ok(output.includes('bugfix'), 'Should list bugfix'); + assert.ok(output.includes('spike'), 'Should list spike'); + assert.ok(output.includes('hotfix'), 'Should list hotfix'); + assert.ok(output.includes('/gsd start'), 'Should include usage hint'); } // ═══════════════════════════════════════════════════════════════════════════ @@ -138,13 +138,13 @@ console.log('\n── Template Info ──'); { const info = getTemplateInfo('bugfix'); - assertTrue(info !== null, 'Should return info for bugfix'); - assertTrue(info!.includes('Bug Fix'), 'Should include template name'); - assertTrue(info!.includes('triage'), 'Should include phase names'); - assertTrue(info!.includes('Triggers'), 'Should include triggers section'); + assert.ok(info !== null, 'Should return info for bugfix'); + assert.ok(info!.includes('Bug Fix'), 'Should include template name'); + assert.ok(info!.includes('triage'), 'Should include phase names'); + assert.ok(info!.includes('Triggers'), 'Should include triggers section'); const missing = getTemplateInfo('nonexistent'); - assertTrue(missing === null, 'Should return null for unknown template'); + assert.ok(missing === null, 'Should return null for unknown template'); } // ═══════════════════════════════════════════════════════════════════════════ @@ -155,19 +155,17 @@ console.log('\n── Load Workflow Template ──'); { const content = loadWorkflowTemplate('bugfix'); - assertTrue(content !== null, 'Should load bugfix template'); - assertTrue(content!.includes('Bugfix Workflow'), 'Should contain workflow title'); - assertTrue(content!.includes('Phase 1: Triage'), 'Should contain triage phase'); - assertTrue(content!.includes('Phase 4: Ship'), 'Should contain ship phase'); + assert.ok(content !== null, 'Should load bugfix template'); + assert.ok(content!.includes('Bugfix Workflow'), 'Should contain workflow title'); + assert.ok(content!.includes('Phase 1: Triage'), 'Should contain triage phase'); + assert.ok(content!.includes('Phase 4: Ship'), 'Should contain ship phase'); const hotfixContent = loadWorkflowTemplate('hotfix'); - assertTrue(hotfixContent !== null, 'Should load hotfix template'); - assertTrue(hotfixContent!.includes('Hotfix Workflow'), 'Should contain hotfix title'); + assert.ok(hotfixContent !== null, 'Should load hotfix template'); + assert.ok(hotfixContent!.includes('Hotfix Workflow'), 'Should contain hotfix title'); const missingContent = loadWorkflowTemplate('nonexistent'); - assertTrue(missingContent === null, 'Should return null for unknown template'); + assert.ok(missingContent === null, 'Should return null for unknown template'); } // ═══════════════════════════════════════════════════════════════════════════ - -report(); diff --git a/src/resources/extensions/gsd/tests/worktree-bugfix.test.ts b/src/resources/extensions/gsd/tests/worktree-bugfix.test.ts index e0766c065..8f25e516d 100644 --- a/src/resources/extensions/gsd/tests/worktree-bugfix.test.ts +++ b/src/resources/extensions/gsd/tests/worktree-bugfix.test.ts @@ -14,12 +14,10 @@ import { join } from "node:path"; import { tmpdir } from "node:os"; import { execSync } from "node:child_process"; import { describe, it, after } from "node:test"; +import assert from 'node:assert/strict'; import { resolveGitDir } from "../worktree-manager.ts"; import { detectWorktreeName, captureIntegrationBranch } from "../worktree.ts"; -import { createTestContext } from "./test-helpers.ts"; - -const { assertEq, assertTrue, report } = createTestContext(); // ─── Helpers ────────────────────────────────────────────────────────────── @@ -40,7 +38,6 @@ describe("worktree-bugfix", () => { const dirs: string[] = []; after(() => { for (const d of dirs) rmSync(d, { recursive: true, force: true }); - report(); }); it("resolveGitDir returns .git directory in normal repo", () => { @@ -48,8 +45,8 @@ describe("worktree-bugfix", () => { dirs.push(repo); initRepo(repo); const gitDir = resolveGitDir(repo); - assertTrue(gitDir.endsWith(".git"), "ends with .git"); - assertTrue(existsSync(gitDir), ".git dir exists"); + assert.ok(gitDir.endsWith(".git"), "ends with .git"); + assert.ok(existsSync(gitDir), ".git dir exists"); }); it("resolveGitDir follows gitdir: pointer in worktree", () => { @@ -65,18 +62,18 @@ describe("worktree-bugfix", () => { writeFileSync(join(wtDir, ".git"), `gitdir: ${realGitDir}\n`); const resolved = resolveGitDir(wtDir); - assertEq(resolved, realGitDir, "resolves to real git dir"); + assert.deepStrictEqual(resolved, realGitDir, "resolves to real git dir"); }); it("resolveGitDir returns default when .git doesn't exist", () => { const noGit = mkdtempSync(join(tmpdir(), "gsd-wt-fix-")); dirs.push(noGit); const gitDir = resolveGitDir(noGit); - assertTrue(gitDir.endsWith(".git"), "returns default .git path"); + assert.ok(gitDir.endsWith(".git"), "returns default .git path"); }); it("detectWorktreeName returns name for worktree path", () => { - assertEq( + assert.deepStrictEqual( detectWorktreeName("/project/.gsd/worktrees/M005"), "M005", "detects worktree name", @@ -84,7 +81,7 @@ describe("worktree-bugfix", () => { }); it("detectWorktreeName returns null for normal repo", () => { - assertEq( + assert.deepStrictEqual( detectWorktreeName("/project"), null, "null for non-worktree path", @@ -106,7 +103,7 @@ describe("worktree-bugfix", () => { // captureIntegrationBranch should be a no-op — no META.json written const metaPath = join(wtPath, ".gsd", "milestones", "M005", "M005-META.json"); captureIntegrationBranch(wtPath, "M005"); - assertTrue(!existsSync(metaPath), "no META.json written in worktree"); + assert.ok(!existsSync(metaPath), "no META.json written in worktree"); }); it("detectWorktreeName prevents pull in worktree context", () => { @@ -114,7 +111,7 @@ describe("worktree-bugfix", () => { // the caller should skip pull/fetch operations const inWorktree = detectWorktreeName("/project/.gsd/worktrees/M006"); const inNormal = detectWorktreeName("/project"); - assertTrue(inWorktree !== null, "worktree detected → skip pull"); - assertTrue(inNormal === null, "normal repo → allow pull"); + assert.ok(inWorktree !== null, "worktree detected → skip pull"); + assert.ok(inNormal === null, "normal repo → allow pull"); }); }); diff --git a/src/resources/extensions/gsd/tests/worktree-db-integration.test.ts b/src/resources/extensions/gsd/tests/worktree-db-integration.test.ts index 92728ba23..0d4b098b6 100644 --- a/src/resources/extensions/gsd/tests/worktree-db-integration.test.ts +++ b/src/resources/extensions/gsd/tests/worktree-db-integration.test.ts @@ -29,9 +29,9 @@ import { isDbAvailable, } from "../gsd-db.ts"; -import { createTestContext } from "./test-helpers.ts"; +import { describe, test } from 'node:test'; +import assert from 'node:assert/strict'; -const { assertEq, assertTrue, report } = createTestContext(); function run(command: string, cwd: string): string { return execSync(command, { cwd, stdio: ["ignore", "pipe", "pipe"], encoding: "utf-8" }).trim(); @@ -49,7 +49,7 @@ function createTempRepo(): string { return dir; } -async function main(): Promise { +describe('worktree-db-integration', async () => { const savedCwd = process.cwd(); const tempDirs: string[] = []; @@ -82,7 +82,7 @@ async function main(): Promise { const wtPath = createAutoWorktree(tempDir, "M004"); const worktreeDbPath = join(worktreePath(tempDir, "M004"), ".gsd", "gsd.db"); - assertTrue( + assert.ok( existsSync(worktreeDbPath), "gsd.db exists in worktree .gsd after createAutoWorktree", ); @@ -107,10 +107,10 @@ async function main(): Promise { console.error(" Unexpected throw:", err); } - assertTrue(!threw, "createAutoWorktree does not throw when no source DB"); + assert.ok(!threw, "createAutoWorktree does not throw when no source DB"); const worktreeDbPath = join(worktreePath(tempDir, "M004"), ".gsd", "gsd.db"); - assertTrue( + assert.ok( !existsSync(worktreeDbPath), "gsd.db is absent in worktree when source had none", ); @@ -145,7 +145,7 @@ async function main(): Promise { // Reconcile worktree → main const result = reconcileWorktreeDb(mainDbPath, worktreeDbPath); - assertTrue(result.decisions >= 1, "reconcile reports at least 1 decision merged"); + assert.ok(result.decisions >= 1, "reconcile reports at least 1 decision merged"); // Open main DB and verify the row is present openDatabase(mainDbPath); @@ -153,7 +153,7 @@ async function main(): Promise { closeDatabase(); const found = decisions.some((d) => d.id === "D-WT-001"); - assertTrue(found, "worktree decision D-WT-001 present in main DB after reconcile"); + assert.ok(found, "worktree decision D-WT-001 present in main DB after reconcile"); } // ─── Test 4: reconcile non-fatal when both paths nonexistent ───── @@ -165,7 +165,7 @@ async function main(): Promise { } catch { threw = true; } - assertTrue(!threw, "reconcileWorktreeDb does not throw when worktree DB is absent"); + assert.ok(!threw, "reconcileWorktreeDb does not throw when worktree DB is absent"); } // ─── Test 5: failure path observable via stderr (diagnostic) ───── @@ -181,10 +181,10 @@ async function main(): Promise { closeDatabase(); const result = reconcileWorktreeDb(mainDbPath, "/definitely/does/not/exist.db"); - assertEq(result.decisions, 0, "decisions is 0 when worktree DB absent"); - assertEq(result.requirements, 0, "requirements is 0 when worktree DB absent"); - assertEq(result.artifacts, 0, "artifacts is 0 when worktree DB absent"); - assertEq(result.conflicts.length, 0, "conflicts is empty when worktree DB absent"); + assert.deepStrictEqual(result.decisions, 0, "decisions is 0 when worktree DB absent"); + assert.deepStrictEqual(result.requirements, 0, "requirements is 0 when worktree DB absent"); + assert.deepStrictEqual(result.artifacts, 0, "artifacts is 0 when worktree DB absent"); + assert.deepStrictEqual(result.conflicts.length, 0, "conflicts is empty when worktree DB absent"); } } finally { @@ -199,8 +199,4 @@ async function main(): Promise { } } } - - report(); -} - -main(); +}); diff --git a/src/resources/extensions/gsd/tests/worktree-db.test.ts b/src/resources/extensions/gsd/tests/worktree-db.test.ts index d757947ec..dd97a0495 100644 --- a/src/resources/extensions/gsd/tests/worktree-db.test.ts +++ b/src/resources/extensions/gsd/tests/worktree-db.test.ts @@ -1,4 +1,5 @@ -import { createTestContext } from './test-helpers.ts'; +import { test } from 'node:test'; +import assert from 'node:assert/strict'; import * as fs from 'node:fs'; import * as path from 'node:path'; import * as os from 'node:os'; @@ -16,7 +17,6 @@ import { reconcileWorktreeDb, } from '../gsd-db.ts'; -const { assertEq, assertTrue, report } = createTestContext(); // ═══════════════════════════════════════════════════════════════════════════ // Helpers @@ -91,18 +91,18 @@ console.log('\n=== worktree-db: copyWorktreeDb ==='); closeDatabase(); const result = copyWorktreeDb(srcDb, destDb); - assertTrue(result === true, 'copyWorktreeDb returns true on success'); - assertTrue(fs.existsSync(destDb), 'dest DB file exists after copy'); + assert.ok(result === true, 'copyWorktreeDb returns true on success'); + assert.ok(fs.existsSync(destDb), 'dest DB file exists after copy'); // Open the copy and verify data is queryable openDatabase(destDb); const d = getDecisionById('D001'); - assertTrue(d !== null, 'decision queryable in copied DB'); - assertEq(d?.choice, 'node:sqlite', 'decision data preserved in copy'); + assert.ok(d !== null, 'decision queryable in copied DB'); + assert.deepStrictEqual(d?.choice, 'node:sqlite', 'decision data preserved in copy'); const r = getRequirementById('R001'); - assertTrue(r !== null, 'requirement queryable in copied DB'); - assertEq(r?.description, 'Must store decisions', 'requirement data preserved in copy'); + assert.ok(r !== null, 'requirement queryable in copied DB'); + assert.deepStrictEqual(r?.description, 'Must store decisions', 'requirement data preserved in copy'); cleanup(srcDir, destDir); } @@ -123,9 +123,9 @@ console.log('\n=== worktree-db: copyWorktreeDb ==='); copyWorktreeDb(srcDb, destDb); - assertTrue(fs.existsSync(destDb), 'DB file copied'); - assertTrue(!fs.existsSync(destDb + '-wal'), 'WAL file NOT copied'); - assertTrue(!fs.existsSync(destDb + '-shm'), 'SHM file NOT copied'); + assert.ok(fs.existsSync(destDb), 'DB file copied'); + assert.ok(!fs.existsSync(destDb + '-wal'), 'WAL file NOT copied'); + assert.ok(!fs.existsSync(destDb + '-shm'), 'SHM file NOT copied'); cleanup(srcDir, destDir); } @@ -134,7 +134,7 @@ console.log('\n=== worktree-db: copyWorktreeDb ==='); { const destDir = tempDir(); const result = copyWorktreeDb('/nonexistent/path/gsd.db', path.join(destDir, 'gsd.db')); - assertEq(result, false, 'returns false for missing source'); + assert.deepStrictEqual(result, false, 'returns false for missing source'); cleanup(destDir); } @@ -149,8 +149,8 @@ console.log('\n=== worktree-db: copyWorktreeDb ==='); closeDatabase(); const result = copyWorktreeDb(srcDb, deepDest); - assertTrue(result === true, 'copyWorktreeDb succeeds with nested dest'); - assertTrue(fs.existsSync(deepDest), 'DB file created at deeply nested path'); + assert.ok(result === true, 'copyWorktreeDb succeeds with nested dest'); + assert.ok(fs.existsSync(deepDest), 'DB file created at deeply nested path'); cleanup(srcDir, destDir); } @@ -192,10 +192,10 @@ console.log('\n=== worktree-db: reconcileWorktreeDb ==='); openDatabase(mainDb); const result = reconcileWorktreeDb(mainDb, wtDb); - assertTrue(result.decisions > 0, 'decisions merged count > 0'); + assert.ok(result.decisions > 0, 'decisions merged count > 0'); const d2 = getDecisionById('D002'); - assertTrue(d2 !== null, 'D002 from worktree now in main'); - assertEq(d2?.choice, 'WAL', 'D002 data correct after merge'); + assert.ok(d2 !== null, 'D002 from worktree now in main'); + assert.deepStrictEqual(d2?.choice, 'WAL', 'D002 data correct after merge'); cleanup(mainDir, wtDir); } @@ -231,10 +231,10 @@ console.log('\n=== worktree-db: reconcileWorktreeDb ==='); openDatabase(mainDb); const result = reconcileWorktreeDb(mainDb, wtDb); - assertTrue(result.requirements > 0, 'requirements merged count > 0'); + assert.ok(result.requirements > 0, 'requirements merged count > 0'); const r2 = getRequirementById('R002'); - assertTrue(r2 !== null, 'R002 from worktree now in main'); - assertEq(r2?.description, 'Must be fast', 'R002 data correct after merge'); + assert.ok(r2 !== null, 'R002 from worktree now in main'); + assert.deepStrictEqual(r2?.description, 'Must be fast', 'R002 data correct after merge'); cleanup(mainDir, wtDir); } @@ -264,11 +264,11 @@ console.log('\n=== worktree-db: reconcileWorktreeDb ==='); openDatabase(mainDb); const result = reconcileWorktreeDb(mainDb, wtDb); - assertTrue(result.artifacts > 0, 'artifacts merged count > 0'); + assert.ok(result.artifacts > 0, 'artifacts merged count > 0'); const adapter = _getAdapter()!; const row = adapter.prepare('SELECT * FROM artifacts WHERE path = ?').get('docs/api.md'); - assertTrue(row !== null, 'artifact from worktree now in main'); - assertEq(row?.['artifact_type'], 'reference', 'artifact data correct after merge'); + assert.ok(row !== null, 'artifact from worktree now in main'); + assert.deepStrictEqual(row?.['artifact_type'], 'reference', 'artifact data correct after merge'); cleanup(mainDir, wtDir); } @@ -305,15 +305,15 @@ console.log('\n=== worktree-db: reconcileWorktreeDb ==='); openDatabase(mainDb); const result = reconcileWorktreeDb(mainDb, wtDb); - assertTrue(result.conflicts.length > 0, 'conflicts detected'); - assertTrue( + assert.ok(result.conflicts.length > 0, 'conflicts detected'); + assert.ok( result.conflicts.some(c => c.includes('D001')), 'conflict mentions D001', ); // Worktree-wins: D001 should now have worktree's value const d1 = getDecisionById('D001'); - assertEq(d1?.choice, 'sql.js', 'worktree wins on conflict (INSERT OR REPLACE)'); + assert.deepStrictEqual(d1?.choice, 'sql.js', 'worktree wins on conflict (INSERT OR REPLACE)'); cleanup(mainDir, wtDir); } @@ -326,10 +326,10 @@ console.log('\n=== worktree-db: reconcileWorktreeDb ==='); seedMainDb(mainDb); const result = reconcileWorktreeDb(mainDb, '/nonexistent/worktree.db'); - assertEq(result.decisions, 0, 'no decisions merged for missing worktree DB'); - assertEq(result.requirements, 0, 'no requirements merged for missing worktree DB'); - assertEq(result.artifacts, 0, 'no artifacts merged for missing worktree DB'); - assertEq(result.conflicts.length, 0, 'no conflicts for missing worktree DB'); + assert.deepStrictEqual(result.decisions, 0, 'no decisions merged for missing worktree DB'); + assert.deepStrictEqual(result.requirements, 0, 'no requirements merged for missing worktree DB'); + assert.deepStrictEqual(result.artifacts, 0, 'no artifacts merged for missing worktree DB'); + assert.deepStrictEqual(result.conflicts.length, 0, 'no conflicts for missing worktree DB'); cleanup(mainDir); } @@ -366,9 +366,9 @@ console.log('\n=== worktree-db: reconcileWorktreeDb ==='); openDatabase(mainDb); const result = reconcileWorktreeDb(mainDb, wtDb); - assertTrue(result.decisions > 0, 'reconciliation works with spaces in path'); + assert.ok(result.decisions > 0, 'reconciliation works with spaces in path'); const d3 = getDecisionById('D003'); - assertTrue(d3 !== null, 'D003 merged from worktree with spaces in path'); + assert.ok(d3 !== null, 'D003 merged from worktree with spaces in path'); cleanup(baseDir); } @@ -388,7 +388,7 @@ console.log('\n=== worktree-db: reconcileWorktreeDb ==='); reconcileWorktreeDb(mainDb, wtDb); // Verify main DB is still fully usable after DETACH - assertTrue(isDbAvailable(), 'DB still available after reconciliation'); + assert.ok(isDbAvailable(), 'DB still available after reconciliation'); insertDecision({ id: 'D099', @@ -403,8 +403,8 @@ console.log('\n=== worktree-db: reconcileWorktreeDb ==='); }); const d99 = getDecisionById('D099'); - assertTrue(d99 !== null, 'can insert and query after reconciliation'); - assertEq(d99?.choice, 'works', 'post-reconcile data correct'); + assert.ok(d99 !== null, 'can insert and query after reconciliation'); + assert.deepStrictEqual(d99?.choice, 'works', 'post-reconcile data correct'); // Verify no "wt" database still attached const adapter = _getAdapter()!; @@ -415,7 +415,7 @@ console.log('\n=== worktree-db: reconcileWorktreeDb ==='); } catch { // Expected — wt should be detached } - assertTrue(!wtAccessible, 'wt database is detached after reconciliation'); + assert.ok(!wtAccessible, 'wt database is detached after reconciliation'); cleanup(mainDir, wtDir); } @@ -436,11 +436,10 @@ console.log('\n=== worktree-db: reconcileWorktreeDb ==='); const result = reconcileWorktreeDb(mainDb, wtDb); // Should still report counts for the existing rows (INSERT OR REPLACE touches them) - assertTrue(result.conflicts.length === 0, 'no conflicts when DBs are identical'); - assertTrue(isDbAvailable(), 'DB usable after no-change reconciliation'); + assert.ok(result.conflicts.length === 0, 'no conflicts when DBs are identical'); + assert.ok(isDbAvailable(), 'DB usable after no-change reconciliation'); cleanup(mainDir, wtDir); } // ─── Final Report ────────────────────────────────────────────────────────── -report(); diff --git a/src/resources/extensions/gsd/tests/worktree-e2e.test.ts b/src/resources/extensions/gsd/tests/worktree-e2e.test.ts index 865813e07..43bd272a1 100644 --- a/src/resources/extensions/gsd/tests/worktree-e2e.test.ts +++ b/src/resources/extensions/gsd/tests/worktree-e2e.test.ts @@ -22,9 +22,9 @@ import { import { getSliceBranchName } from "../worktree.ts"; import { abortAndReset } from "../git-self-heal.ts"; import { runGSDDoctor } from "../doctor.ts"; -import { createTestContext } from "./test-helpers.ts"; +import { describe, test } from 'node:test'; +import assert from 'node:assert/strict'; -const { assertEq, assertTrue, assertMatch, report } = createTestContext(); // ---- Helpers ---- @@ -80,7 +80,7 @@ function addSliceToMilestone( run(`git merge --no-ff ${sliceBranch} -m "merge ${sliceId}"`, wtPath); } -async function main(): Promise { +describe('worktree-e2e', async () => { const savedCwd = process.cwd(); const tempDirs: string[] = []; @@ -100,7 +100,7 @@ async function main(): Promise { // Create worktree for M001 const wtPath = createAutoWorktree(repo, "M001"); tempDirs.push(wtPath); - assertTrue(existsSync(wtPath), "worktree directory created"); + assert.ok(existsSync(wtPath), "worktree directory created"); // Add two slices with commits addSliceToMilestone(repo, wtPath, "M001", "S01", "Add auth", [ @@ -124,19 +124,19 @@ async function main(): Promise { // Assert exactly one new commit on main const mainLogAfter = run("git log --oneline main", repo); const commitCountAfter = mainLogAfter.split("\n").length; - assertEq(commitCountAfter, commitCountBefore + 1, "exactly one new commit on main"); + assert.deepStrictEqual(commitCountAfter, commitCountBefore + 1, "exactly one new commit on main"); // Commit message contains both slice titles const lastCommitMsg = run("git log -1 --format=%B main", repo); - assertMatch(lastCommitMsg, /Add auth/, "commit message contains S01 title"); - assertMatch(lastCommitMsg, /Add dashboard/, "commit message contains S02 title"); + assert.match(lastCommitMsg, /Add auth/, "commit message contains S01 title"); + assert.match(lastCommitMsg, /Add dashboard/, "commit message contains S02 title"); // Worktree directory removed - assertTrue(!existsSync(wtPath), "worktree directory removed after merge"); + assert.ok(!existsSync(wtPath), "worktree directory removed after merge"); // Milestone branch deleted const branches = run("git branch", repo); - assertTrue(!branches.includes("milestone/M001"), "milestone branch deleted"); + assert.ok(!branches.includes("milestone/M001"), "milestone branch deleted"); } // ================================================================ @@ -159,11 +159,11 @@ async function main(): Promise { // Trigger merge conflict try { run("git merge feature", repo); } catch { /* expected */ } - assertTrue(existsSync(join(repo, ".git", "MERGE_HEAD")), "MERGE_HEAD exists before abort"); + assert.ok(existsSync(join(repo, ".git", "MERGE_HEAD")), "MERGE_HEAD exists before abort"); const abortResult = abortAndReset(repo); - assertTrue(!existsSync(join(repo, ".git", "MERGE_HEAD")), "MERGE_HEAD removed after abort"); - assertTrue(abortResult.cleaned.length > 0, "abortAndReset reports cleaned items"); + assert.ok(!existsSync(join(repo, ".git", "MERGE_HEAD")), "MERGE_HEAD removed after abort"); + assert.ok(abortResult.cleaned.length > 0, "abortAndReset reports cleaned items"); } // ================================================================ @@ -211,19 +211,19 @@ _None_ // Detect const detect = await runGSDDoctor(repo, { isolationMode: "worktree" }); const orphanIssues = detect.issues.filter(i => i.code === "orphaned_auto_worktree"); - assertTrue(orphanIssues.length > 0, "doctor detects orphaned worktree"); - assertEq(orphanIssues[0]?.unitId, "M001", "orphaned worktree unitId is M001"); + assert.ok(orphanIssues.length > 0, "doctor detects orphaned worktree"); + assert.deepStrictEqual(orphanIssues[0]?.unitId, "M001", "orphaned worktree unitId is M001"); // Fix const fixed = await runGSDDoctor(repo, { fix: true, isolationMode: "worktree" }); - assertTrue( + assert.ok( fixed.fixesApplied.some(f => f.includes("removed orphaned worktree")), "doctor fix removes orphaned worktree", ); // Verify gone const wtList = run("git worktree list", repo); - assertTrue(!wtList.includes("milestone/M001"), "worktree gone after doctor fix"); + assert.ok(!wtList.includes("milestone/M001"), "worktree gone after doctor fix"); } } else { console.log("\n=== Doctor: orphaned worktree detection (skipped on Windows) ==="); @@ -234,8 +234,4 @@ _None_ try { rmSync(d, { recursive: true, force: true }); } catch { /* ignore */ } } } - - report(); -} - -main(); +}); diff --git a/src/resources/extensions/gsd/tests/worktree-health.test.ts b/src/resources/extensions/gsd/tests/worktree-health.test.ts index e6580ecd9..425e63f02 100644 --- a/src/resources/extensions/gsd/tests/worktree-health.test.ts +++ b/src/resources/extensions/gsd/tests/worktree-health.test.ts @@ -12,9 +12,9 @@ import { execSync } from "node:child_process"; import { getWorktreeHealth, formatWorktreeStatusLine } from "../worktree-health.ts"; import { listWorktrees } from "../worktree-manager.ts"; -import { createTestContext } from "./test-helpers.ts"; +import { describe, test } from 'node:test'; +import assert from 'node:assert/strict'; -const { assertEq, assertTrue, report } = createTestContext(); function run(cmd: string, cwd: string): string { return execSync(cmd, { cwd, stdio: ["ignore", "pipe", "pipe"], encoding: "utf-8" }).trim(); @@ -32,11 +32,10 @@ function createBaseRepo(): string { return dir; } -async function main(): Promise { +describe('worktree-health', async () => { // Skip all tests on Windows — git worktree path resolution issues if (process.platform === "win32") { console.log("(all worktree-health tests skipped on Windows)"); - report(); return; } @@ -59,16 +58,16 @@ async function main(): Promise { const worktrees = listWorktrees(dir); const wt = worktrees.find(w => w.name === "done-feature"); - assertTrue(!!wt, "worktree found"); + assert.ok(!!wt, "worktree found"); const health = getWorktreeHealth(dir, wt!); - assertTrue(health.mergedIntoMain, "branch detected as merged"); - assertTrue(!health.dirty, "not dirty"); - assertTrue(health.safeToRemove, "safe to remove"); + assert.ok(health.mergedIntoMain, "branch detected as merged"); + assert.ok(!health.dirty, "not dirty"); + assert.ok(health.safeToRemove, "safe to remove"); const line = formatWorktreeStatusLine(health); - assertTrue(line.includes("merged"), "status line mentions merged"); - assertTrue(line.includes("safe to remove"), "status line mentions safe to remove"); + assert.ok(line.includes("merged"), "status line mentions merged"); + assert.ok(line.includes("safe to remove"), "status line mentions safe to remove"); } // ─── Test: unmerged worktree with dirty files ────────────────────── @@ -89,13 +88,13 @@ async function main(): Promise { const worktrees = listWorktrees(dir); const wt = worktrees.find(w => w.name === "dirty-wip"); - assertTrue(!!wt, "worktree found"); + assert.ok(!!wt, "worktree found"); const health = getWorktreeHealth(dir, wt!); - assertTrue(!health.mergedIntoMain, "not merged"); - assertTrue(health.dirty, "dirty detected"); - assertTrue(health.dirtyFileCount > 0, "dirty file count > 0"); - assertTrue(!health.safeToRemove, "not safe to remove"); + assert.ok(!health.mergedIntoMain, "not merged"); + assert.ok(health.dirty, "dirty detected"); + assert.ok(health.dirtyFileCount > 0, "dirty file count > 0"); + assert.ok(!health.safeToRemove, "not safe to remove"); } // ─── Test: unmerged worktree with unpushed commits ───────────────── @@ -113,12 +112,12 @@ async function main(): Promise { const worktrees = listWorktrees(dir); const wt = worktrees.find(w => w.name === "unpushed"); - assertTrue(!!wt, "worktree found"); + assert.ok(!!wt, "worktree found"); const health = getWorktreeHealth(dir, wt!); - assertTrue(!health.mergedIntoMain, "not merged"); - assertTrue(health.unpushedCommits > 0, "unpushed commits detected"); - assertTrue(!health.safeToRemove, "not safe to remove"); + assert.ok(!health.mergedIntoMain, "not merged"); + assert.ok(health.unpushedCommits > 0, "unpushed commits detected"); + assert.ok(!health.safeToRemove, "not safe to remove"); } // ─── Test: stale detection with short threshold ──────────────────── @@ -137,17 +136,17 @@ async function main(): Promise { const worktrees = listWorktrees(dir); const wt = worktrees.find(w => w.name === "stale-test"); - assertTrue(!!wt, "worktree found"); + assert.ok(!!wt, "worktree found"); // With staleDays=0, any worktree should be stale (commit was just now, but threshold is 0) // Actually, a just-created worktree has lastCommitAgeDays ~0 which is >= 0 const health = getWorktreeHealth(dir, wt!, 0); - assertTrue(health.stale, "stale with 0-day threshold"); - assertTrue(health.lastCommitAgeDays >= 0, "last commit age is non-negative"); + assert.ok(health.stale, "stale with 0-day threshold"); + assert.ok(health.lastCommitAgeDays >= 0, "last commit age is non-negative"); // With staleDays=9999, should NOT be stale const healthNotStale = getWorktreeHealth(dir, wt!, 9999); - assertTrue(!healthNotStale.stale, "not stale with high threshold"); + assert.ok(!healthNotStale.stale, "not stale with high threshold"); } // ─── Test: formatWorktreeStatusLine for clean active worktree ────── @@ -166,12 +165,12 @@ async function main(): Promise { const worktrees = listWorktrees(dir); const wt = worktrees.find(w => w.name === "clean-active"); - assertTrue(!!wt, "worktree found"); + assert.ok(!!wt, "worktree found"); const health = getWorktreeHealth(dir, wt!, 9999); // high threshold so not stale const line = formatWorktreeStatusLine(health); // Should show last commit age since it's not merged and not stale - assertTrue(line.includes("last commit"), "shows last commit age for active worktree"); + assert.ok(line.includes("last commit"), "shows last commit age for active worktree"); } } finally { @@ -179,8 +178,4 @@ async function main(): Promise { try { rmSync(dir, { recursive: true, force: true }); } catch { /* ignore */ } } } - - report(); -} - -main(); +}); diff --git a/src/resources/extensions/gsd/tests/worktree-integration.test.ts b/src/resources/extensions/gsd/tests/worktree-integration.test.ts index 5d153eec1..9c350ff13 100644 --- a/src/resources/extensions/gsd/tests/worktree-integration.test.ts +++ b/src/resources/extensions/gsd/tests/worktree-integration.test.ts @@ -29,9 +29,9 @@ import { } from "../worktree.ts"; import { deriveState } from "../state.ts"; -import { createTestContext } from './test-helpers.ts'; +import { describe, test } from 'node:test'; +import assert from 'node:assert/strict'; -const { assertEq, assertTrue, report } = createTestContext(); function run(command: string, cwd: string): string { return execSync(command, { cwd, stdio: ["ignore", "pipe", "pipe"], encoding: "utf-8" }).trim(); } @@ -73,42 +73,42 @@ writeFileSync( run("git add .", base); run('git commit -m "chore: init"', base); -async function main(): Promise { +describe('worktree-integration', async () => { // ── Verify main tree baseline ────────────────────────────────────────────── console.log("\n=== Main tree baseline ==="); - assertEq(getMainBranch(base), "main", "main tree getMainBranch returns main"); - assertEq(detectWorktreeName(base), null, "main tree not detected as worktree"); + assert.deepStrictEqual(getMainBranch(base), "main", "main tree getMainBranch returns main"); + assert.deepStrictEqual(detectWorktreeName(base), null, "main tree not detected as worktree"); // ── Create worktree and verify detection ─────────────────────────────────── console.log("\n=== Create worktree ==="); const wt = createWorktree(base, "alpha"); - assertTrue(existsSync(wt.path), "worktree created on disk"); - assertEq(wt.branch, "worktree/alpha", "worktree branch name"); + assert.ok(existsSync(wt.path), "worktree created on disk"); + assert.deepStrictEqual(wt.branch, "worktree/alpha", "worktree branch name"); console.log("\n=== Worktree detection ==="); - assertEq(detectWorktreeName(wt.path), "alpha", "detectWorktreeName inside worktree"); - assertEq(getMainBranch(wt.path), "worktree/alpha", "getMainBranch returns worktree branch inside worktree"); + assert.deepStrictEqual(detectWorktreeName(wt.path), "alpha", "detectWorktreeName inside worktree"); + assert.deepStrictEqual(getMainBranch(wt.path), "worktree/alpha", "getMainBranch returns worktree branch inside worktree"); // ── Verify current branch inside worktree ────────────────────────────────── console.log("\n=== Worktree initial branch ==="); - assertEq(getCurrentBranch(wt.path), "worktree/alpha", "worktree starts on its own branch"); + assert.deepStrictEqual(getCurrentBranch(wt.path), "worktree/alpha", "worktree starts on its own branch"); // ── Verify branch name helper ────────────────────────────────────────────── console.log("\n=== getSliceBranchName with worktree ==="); - assertEq(getSliceBranchName("M001", "S01", "alpha"), "gsd/alpha/M001/S01", "explicit worktree param"); - assertEq(getSliceBranchName("M001", "S01"), "gsd/M001/S01", "no worktree param = plain branch"); + assert.deepStrictEqual(getSliceBranchName("M001", "S01", "alpha"), "gsd/alpha/M001/S01", "explicit worktree param"); + assert.deepStrictEqual(getSliceBranchName("M001", "S01"), "gsd/M001/S01", "no worktree param = plain branch"); // ── Slice branch creation and detection inside worktree ──────────────────── console.log("\n=== Slice branch in worktree ==="); const sliceBranch = getSliceBranchName("M001", "S01", "alpha"); run(`git checkout -b ${sliceBranch}`, wt.path); - assertEq(getCurrentBranch(wt.path), "gsd/alpha/M001/S01", "worktree-namespaced slice branch"); - assertTrue(SLICE_BRANCH_RE.test(getCurrentBranch(wt.path)), "slice branch regex matches namespaced branch"); + assert.deepStrictEqual(getCurrentBranch(wt.path), "gsd/alpha/M001/S01", "worktree-namespaced slice branch"); + assert.ok(SLICE_BRANCH_RE.test(getCurrentBranch(wt.path)), "slice branch regex matches namespaced branch"); // ── Do work on slice branch, then merge to worktree branch ───────────────── @@ -119,23 +119,23 @@ async function main(): Promise { // Checkout worktree base branch and merge slice branch run("git checkout worktree/alpha", wt.path); - assertEq(getCurrentBranch(wt.path), "worktree/alpha", "back on worktree branch"); + assert.deepStrictEqual(getCurrentBranch(wt.path), "worktree/alpha", "back on worktree branch"); run(`git merge --no-ff ${sliceBranch} -m "feat(M001/S01): First"`, wt.path); run(`git branch -d ${sliceBranch}`, wt.path); - assertEq(getCurrentBranch(wt.path), "worktree/alpha", "still on worktree branch after merge"); - assertTrue(readFileSync(join(wt.path, "feature.txt"), "utf-8").includes("new feature"), "merge brought feature to worktree branch"); + assert.deepStrictEqual(getCurrentBranch(wt.path), "worktree/alpha", "still on worktree branch after merge"); + assert.ok(readFileSync(join(wt.path, "feature.txt"), "utf-8").includes("new feature"), "merge brought feature to worktree branch"); // Verify slice branch is gone const branches = run("git branch", base); - assertTrue(!branches.includes("gsd/alpha/M001/S01"), "slice branch cleaned up"); + assert.ok(!branches.includes("gsd/alpha/M001/S01"), "slice branch cleaned up"); // ── Second slice in same worktree ────────────────────────────────────────── console.log("\n=== Second slice in worktree ==="); const sliceBranch2 = getSliceBranchName("M001", "S02", "alpha"); run(`git checkout -b ${sliceBranch2}`, wt.path); - assertEq(getCurrentBranch(wt.path), "gsd/alpha/M001/S02", "on S02 namespaced branch"); + assert.deepStrictEqual(getCurrentBranch(wt.path), "gsd/alpha/M001/S02", "on S02 namespaced branch"); writeFileSync(join(wt.path, "feature2.txt"), "second feature\n", "utf-8"); run("git add .", wt.path); @@ -144,28 +144,28 @@ async function main(): Promise { run("git checkout worktree/alpha", wt.path); run(`git merge --no-ff ${sliceBranch2} -m "feat(M001/S02): Second"`, wt.path); run(`git branch -d ${sliceBranch2}`, wt.path); - assertEq(getCurrentBranch(wt.path), "worktree/alpha", "back on worktree branch"); + assert.deepStrictEqual(getCurrentBranch(wt.path), "worktree/alpha", "back on worktree branch"); // ── Parallel worktrees don't conflict ────────────────────────────────────── console.log("\n=== Parallel worktrees ==="); const wt2 = createWorktree(base, "beta"); - assertEq(getMainBranch(wt2.path), "worktree/beta", "second worktree has its own base branch"); + assert.deepStrictEqual(getMainBranch(wt2.path), "worktree/beta", "second worktree has its own base branch"); // Both worktrees can create S01 branches without conflict const betaBranch = getSliceBranchName("M001", "S01", "beta"); run(`git checkout -b ${betaBranch}`, wt2.path); - assertEq(getCurrentBranch(wt2.path), "gsd/beta/M001/S01", "beta has its own namespaced branch"); + assert.deepStrictEqual(getCurrentBranch(wt2.path), "gsd/beta/M001/S01", "beta has its own namespaced branch"); // Alpha worktree can re-create S01 too (it was already merged+deleted earlier) const alphaReBranch = getSliceBranchName("M001", "S01", "alpha"); run(`git checkout -b ${alphaReBranch}`, wt.path); - assertEq(getCurrentBranch(wt.path), "gsd/alpha/M001/S01", "alpha re-created S01"); + assert.deepStrictEqual(getCurrentBranch(wt.path), "gsd/alpha/M001/S01", "alpha re-created S01"); // Both exist simultaneously const allBranches = run("git branch", base); - assertTrue(allBranches.includes("gsd/alpha/M001/S01"), "alpha S01 branch exists"); - assertTrue(allBranches.includes("gsd/beta/M001/S01"), "beta S01 branch exists"); + assert.ok(allBranches.includes("gsd/alpha/M001/S01"), "alpha S01 branch exists"); + assert.ok(allBranches.includes("gsd/beta/M001/S01"), "beta S01 branch exists"); // ── State derivation in worktree ─────────────────────────────────────────── @@ -173,8 +173,8 @@ async function main(): Promise { // Switch alpha back to its base so deriveState sees milestone files run("git checkout worktree/alpha", wt.path); const state = await deriveState(wt.path); - assertTrue(state.activeMilestone !== null, "worktree has active milestone"); - assertEq(state.activeMilestone?.id, "M001", "correct milestone"); + assert.ok(state.activeMilestone !== null, "worktree has active milestone"); + assert.deepStrictEqual(state.activeMilestone?.id, "M001", "correct milestone"); // ── autoCommitCurrentBranch in worktree ──────────────────────────────────── @@ -183,8 +183,8 @@ async function main(): Promise { run(`git checkout ${betaBranch}`, wt2.path); writeFileSync(join(wt2.path, "dirty.txt"), "uncommitted\n", "utf-8"); const commitMsg = autoCommitCurrentBranch(wt2.path, "execute-task", "M001/S01/T01"); - assertTrue(commitMsg !== null, "auto-commit works in worktree"); - assertEq(run("git status --short", wt2.path), "", "worktree clean after auto-commit"); + assert.ok(commitMsg !== null, "auto-commit works in worktree"); + assert.deepStrictEqual(run("git status --short", wt2.path), "", "worktree clean after auto-commit"); // ── Cleanup ──────────────────────────────────────────────────────────────── @@ -194,14 +194,7 @@ async function main(): Promise { run("git checkout worktree/beta", wt2.path); removeWorktree(base, "alpha", { deleteBranch: true }); removeWorktree(base, "beta", { deleteBranch: true }); - assertEq(listWorktrees(base).length, 0, "all worktrees removed"); + assert.deepStrictEqual(listWorktrees(base).length, 0, "all worktrees removed"); rmSync(base, { recursive: true, force: true }); - - report(); -} - -main().catch((error) => { - console.error(error); - process.exit(1); }); diff --git a/src/resources/extensions/gsd/tests/worktree-symlink-removal.test.ts b/src/resources/extensions/gsd/tests/worktree-symlink-removal.test.ts index f92f719e0..b63d5dd7b 100644 --- a/src/resources/extensions/gsd/tests/worktree-symlink-removal.test.ts +++ b/src/resources/extensions/gsd/tests/worktree-symlink-removal.test.ts @@ -20,9 +20,9 @@ import { listWorktrees, worktreePath, } from "../worktree-manager.ts"; -import { createTestContext } from './test-helpers.ts'; +import { describe, test } from 'node:test'; +import assert from 'node:assert/strict'; -const { assertEq, assertTrue, report } = createTestContext(); function run(command: string, cwd: string): string { return execSync(command, { cwd, stdio: ["ignore", "pipe", "pipe"], encoding: "utf-8" }).trim(); @@ -46,8 +46,8 @@ mkdirSync(join(externalState, "worktrees"), { recursive: true }); symlinkSync(externalState, join(base, ".gsd")); // Verify the symlink is in place -assertTrue(existsSync(join(base, ".gsd")), ".gsd symlink exists"); -assertTrue( +assert.ok(existsSync(join(base, ".gsd")), ".gsd symlink exists"); +assert.ok( realpathSync(join(base, ".gsd")) === externalState, ".gsd resolves to external state dir", ); @@ -57,28 +57,28 @@ writeFileSync(join(base, "README.md"), "# Test\n", "utf-8"); run("git add .", base); run('git commit -m "init"', base); -async function main(): Promise { +describe('worktree-symlink-removal', async () => { console.log("\n=== #1852: removeWorktree with symlinked .gsd/ ==="); // Create a worktree — git will resolve the symlink and register // the worktree at the external path const info = createWorktree(base, "M002", { branch: "milestone/M002" }); - assertTrue(info.exists, "worktree created"); + assert.ok(info.exists, "worktree created"); // Verify worktree was created at the resolved (external) path const realWtPath = realpathSync(info.path); - assertTrue( + assert.ok( realWtPath.startsWith(externalState), `worktree real path (${realWtPath}) is under external state dir`, ); // Verify git registered the worktree const gitList = run("git worktree list", base); - assertTrue(gitList.includes("M002"), "git worktree list shows M002"); + assert.ok(gitList.includes("M002"), "git worktree list shows M002"); // The computed path via worktreePath uses the symlink path const computedPath = worktreePath(base, "M002"); - assertTrue(existsSync(computedPath), "computed path exists (via symlink)"); + assert.ok(existsSync(computedPath), "computed path exists (via symlink)"); // Simulate what syncStateToProjectRoot does: replace the .gsd symlink with // a real directory containing stale worktree data. This causes worktreePath() @@ -93,8 +93,8 @@ async function main(): Promise { // Now worktreePath(base, "M002") points to the LOCAL stale dir, not the // external path where git actually registered the worktree. const stalePath = worktreePath(base, "M002"); - assertTrue(existsSync(stalePath), "stale local worktree dir exists"); - assertTrue( + assert.ok(existsSync(stalePath), "stale local worktree dir exists"); + assert.ok( stalePath !== realWtPath, `computed path (${stalePath}) differs from git-registered path (${realWtPath})`, ); @@ -105,36 +105,29 @@ async function main(): Promise { // After removal, the worktree should be gone from git's list const gitListAfter = run("git worktree list", base); - assertTrue( + assert.ok( !gitListAfter.includes("M002"), "worktree removed from git worktree list after removeWorktree", ); // The branch should be deleted const branches = run("git branch", base); - assertTrue( + assert.ok( !branches.includes("milestone/M002"), "milestone/M002 branch deleted after removeWorktree", ); // The worktree directory should be gone - assertTrue( + assert.ok( !existsSync(realWtPath), "worktree directory removed from disk", ); // List should be empty const listed = listWorktrees(base); - assertEq(listed.length, 0, "no worktrees listed after removal"); + assert.deepStrictEqual(listed.length, 0, "no worktrees listed after removal"); // Cleanup rmSync(base, { recursive: true, force: true }); rmSync(externalState, { recursive: true, force: true }); - - report(); -} - -main().catch((error) => { - console.error(error); - process.exit(1); }); diff --git a/src/resources/extensions/gsd/tests/worktree-sync-milestones.test.ts b/src/resources/extensions/gsd/tests/worktree-sync-milestones.test.ts index 9c5552a2c..0df83dfd2 100644 --- a/src/resources/extensions/gsd/tests/worktree-sync-milestones.test.ts +++ b/src/resources/extensions/gsd/tests/worktree-sync-milestones.test.ts @@ -29,9 +29,9 @@ import { tmpdir } from 'node:os'; import { syncProjectRootToWorktree } from '../auto-worktree-sync.ts'; import { syncGsdStateToWorktree, syncWorktreeStateBack } from '../auto-worktree.ts'; -import { createTestContext } from './test-helpers.ts'; +import { describe, test } from 'node:test'; +import assert from 'node:assert/strict'; -const { assertTrue, report } = createTestContext(); function createBase(name: string): string { const base = mkdtempSync(join(tmpdir(), `gsd-wt-sync-${name}-`)); @@ -43,7 +43,7 @@ function cleanup(base: string): void { rmSync(base, { recursive: true, force: true }); } -async function main(): Promise { +describe('worktree-sync-milestones', async () => { // ─── 1. Milestone directory synced from main to worktree ────────────── console.log('\n=== 1. milestone directory synced from main to worktree ==='); @@ -58,13 +58,13 @@ async function main(): Promise { writeFileSync(join(m001Dir, 'M001-ROADMAP.md'), '# Roadmap'); // Worktree has no M001 - assertTrue(!existsSync(join(wtBase, '.gsd', 'milestones', 'M001')), 'M001 missing before sync'); + assert.ok(!existsSync(join(wtBase, '.gsd', 'milestones', 'M001')), 'M001 missing before sync'); syncProjectRootToWorktree(mainBase, wtBase, 'M001'); - assertTrue(existsSync(join(wtBase, '.gsd', 'milestones', 'M001')), '#1311: M001 synced to worktree'); - assertTrue(existsSync(join(wtBase, '.gsd', 'milestones', 'M001', 'M001-CONTEXT.md')), 'M001 CONTEXT synced'); - assertTrue(existsSync(join(wtBase, '.gsd', 'milestones', 'M001', 'M001-ROADMAP.md')), 'M001 ROADMAP synced'); + assert.ok(existsSync(join(wtBase, '.gsd', 'milestones', 'M001')), '#1311: M001 synced to worktree'); + assert.ok(existsSync(join(wtBase, '.gsd', 'milestones', 'M001', 'M001-CONTEXT.md')), 'M001 CONTEXT synced'); + assert.ok(existsSync(join(wtBase, '.gsd', 'milestones', 'M001', 'M001-ROADMAP.md')), 'M001 ROADMAP synced'); } finally { cleanup(mainBase); cleanup(wtBase); @@ -92,8 +92,8 @@ async function main(): Promise { syncProjectRootToWorktree(mainBase, wtBase, 'M001'); - assertTrue(existsSync(join(wtBase, '.gsd', 'milestones', 'M001', 'slices', 'S02')), '#1311: S02 synced'); - assertTrue(existsSync(join(wtBase, '.gsd', 'milestones', 'M001', 'slices', 'S02', 'S02-PLAN.md')), 'S02 PLAN synced'); + assert.ok(existsSync(join(wtBase, '.gsd', 'milestones', 'M001', 'slices', 'S02')), '#1311: S02 synced'); + assert.ok(existsSync(join(wtBase, '.gsd', 'milestones', 'M001', 'slices', 'S02', 'S02-PLAN.md')), 'S02 PLAN synced'); } finally { cleanup(mainBase); cleanup(wtBase); @@ -113,11 +113,11 @@ async function main(): Promise { // Worktree has a stale gsd.db writeFileSync(join(wtBase, '.gsd', 'gsd.db'), 'stale data'); - assertTrue(existsSync(join(wtBase, '.gsd', 'gsd.db')), 'gsd.db exists before sync'); + assert.ok(existsSync(join(wtBase, '.gsd', 'gsd.db')), 'gsd.db exists before sync'); syncProjectRootToWorktree(mainBase, wtBase, 'M001'); - assertTrue(!existsSync(join(wtBase, '.gsd', 'gsd.db')), '#853: gsd.db deleted after sync'); + assert.ok(!existsSync(join(wtBase, '.gsd', 'gsd.db')), '#853: gsd.db deleted after sync'); } finally { cleanup(mainBase); cleanup(wtBase); @@ -131,7 +131,7 @@ async function main(): Promise { try { // Should not throw syncProjectRootToWorktree(base, base, 'M001'); - assertTrue(true, 'no crash when paths are equal'); + assert.ok(true, 'no crash when paths are equal'); } finally { cleanup(base); } @@ -144,7 +144,7 @@ async function main(): Promise { const wtBase = createBase('wt'); try { syncProjectRootToWorktree(mainBase, wtBase, null); - assertTrue(true, 'no crash when milestoneId is null'); + assert.ok(true, 'no crash when milestoneId is null'); } finally { cleanup(mainBase); cleanup(wtBase); @@ -155,7 +155,7 @@ async function main(): Promise { console.log('\n=== 6. non-existent directories → no-op ==='); { syncProjectRootToWorktree('/tmp/does-not-exist-main', '/tmp/does-not-exist-wt', 'M001'); - assertTrue(true, 'no crash on missing directories'); + assert.ok(true, 'no crash on missing directories'); } // ─── 7. milestones/ directory created in worktree when missing ──────── @@ -174,15 +174,15 @@ async function main(): Promise { writeFileSync(join(m001Dir, 'M001-CONTEXT.md'), '# M001 Context'); writeFileSync(join(m001Dir, 'M001-ROADMAP.md'), '# M001 Roadmap'); - assertTrue(!existsSync(join(wtBase, '.gsd', 'milestones')), 'milestones/ missing before sync'); + assert.ok(!existsSync(join(wtBase, '.gsd', 'milestones')), 'milestones/ missing before sync'); const result = syncGsdStateToWorktree(mainBase, wtBase); - assertTrue(existsSync(join(wtBase, '.gsd', 'milestones')), 'milestones/ created in worktree'); - assertTrue(existsSync(join(wtBase, '.gsd', 'milestones', 'M001')), 'M001 synced to worktree'); - assertTrue(existsSync(join(wtBase, '.gsd', 'milestones', 'M001', 'M001-CONTEXT.md')), 'M001 CONTEXT synced'); - assertTrue(existsSync(join(wtBase, '.gsd', 'milestones', 'M001', 'M001-ROADMAP.md')), 'M001 ROADMAP synced'); - assertTrue(result.synced.length > 0, 'sync reported files'); + assert.ok(existsSync(join(wtBase, '.gsd', 'milestones')), 'milestones/ created in worktree'); + assert.ok(existsSync(join(wtBase, '.gsd', 'milestones', 'M001')), 'M001 synced to worktree'); + assert.ok(existsSync(join(wtBase, '.gsd', 'milestones', 'M001', 'M001-CONTEXT.md')), 'M001 CONTEXT synced'); + assert.ok(existsSync(join(wtBase, '.gsd', 'milestones', 'M001', 'M001-ROADMAP.md')), 'M001 ROADMAP synced'); + assert.ok(result.synced.length > 0, 'sync reported files'); } finally { cleanup(mainBase); rmSync(wtBase, { recursive: true, force: true }); @@ -212,19 +212,19 @@ async function main(): Promise { const mainSliceDir = join(mainBase, '.gsd', 'milestones', 'M001', 'slices', 'S01'); const mainTasksDir = join(mainSliceDir, 'tasks'); - assertTrue( + assert.ok( existsSync(join(mainSliceDir, 'S01-SUMMARY.md')), '#1678: slice SUMMARY synced to project root', ); - assertTrue( + assert.ok( existsSync(join(mainTasksDir, 'T01-SUMMARY.md')), '#1678: task T01-SUMMARY synced to project root', ); - assertTrue( + assert.ok( existsSync(join(mainTasksDir, 'T02-SUMMARY.md')), '#1678: task T02-SUMMARY synced to project root', ); - assertTrue( + assert.ok( synced.some((p) => p.includes('tasks/T01-SUMMARY.md')), '#1678: task summary appears in synced list', ); @@ -257,27 +257,27 @@ async function main(): Promise { // Root-level files should be overwritten with worktree versions const reqContent = readFileSync(join(mainBase, '.gsd', 'REQUIREMENTS.md'), 'utf-8'); - assertTrue( + assert.ok( reqContent.includes('R002'), 'REQUIREMENTS.md updated with worktree content', ); const projContent = readFileSync(join(mainBase, '.gsd', 'PROJECT.md'), 'utf-8'); - assertTrue( + assert.ok( projContent.includes('M002'), 'PROJECT.md updated with worktree content', ); - assertTrue( + assert.ok( existsSync(join(mainBase, '.gsd', 'KNOWLEDGE.md')), 'KNOWLEDGE.md synced from worktree', ); - assertTrue( + assert.ok( synced.includes('REQUIREMENTS.md'), 'REQUIREMENTS.md appears in synced list', ); - assertTrue( + assert.ok( synced.includes('PROJECT.md'), 'PROJECT.md appears in synced list', ); @@ -308,11 +308,11 @@ async function main(): Promise { writeFileSync(join(wtM002Dir, 'M002-abc123-ROADMAP.md'), '# M002 Roadmap'); // Main has neither - assertTrue( + assert.ok( !existsSync(join(mainBase, '.gsd', 'milestones', 'M001')), 'M001 missing in main before sync', ); - assertTrue( + assert.ok( !existsSync(join(mainBase, '.gsd', 'milestones', 'M002-abc123')), 'M002 missing in main before sync', ); @@ -321,22 +321,22 @@ async function main(): Promise { const { synced } = syncWorktreeStateBack(mainBase, wtBase, 'M001'); // M001 should be synced (current milestone — always synced) - assertTrue( + assert.ok( existsSync(join(mainBase, '.gsd', 'milestones', 'M001', 'M001-SUMMARY.md')), 'M001 SUMMARY synced to main', ); // M002 should ALSO be synced (next milestone — the fix) - assertTrue( + assert.ok( existsSync(join(mainBase, '.gsd', 'milestones', 'M002-abc123', 'M002-abc123-CONTEXT.md')), 'M002 CONTEXT synced to main (next-milestone fix)', ); - assertTrue( + assert.ok( existsSync(join(mainBase, '.gsd', 'milestones', 'M002-abc123', 'M002-abc123-ROADMAP.md')), 'M002 ROADMAP synced to main (next-milestone fix)', ); - assertTrue( + assert.ok( synced.some((p) => p.includes('M002-abc123')), 'M002 appears in synced list', ); @@ -387,34 +387,34 @@ async function main(): Promise { const { synced } = syncWorktreeStateBack(mainBase, wtBase, 'M006-589wvh'); // Verify M006 artifacts synced - assertTrue( + assert.ok( existsSync(join(mainBase, '.gsd', 'milestones', 'M006-589wvh', 'M006-589wvh-SUMMARY.md')), 'M006 SUMMARY synced', ); - assertTrue( + assert.ok( existsSync(join(mainBase, '.gsd', 'milestones', 'M006-589wvh', 'slices', 'S01', 'S01-SUMMARY.md')), 'M006 S01 SUMMARY synced', ); // Verify M007 artifacts synced (the critical fix) - assertTrue( + assert.ok( existsSync(join(mainBase, '.gsd', 'milestones', 'M007-wortc8', 'M007-wortc8-CONTEXT.md')), 'M007 CONTEXT synced to main (next-milestone)', ); - assertTrue( + assert.ok( existsSync(join(mainBase, '.gsd', 'milestones', 'M007-wortc8', 'M007-wortc8-ROADMAP.md')), 'M007 ROADMAP synced to main (next-milestone)', ); // Verify root-level files updated const reqContent = readFileSync(join(mainBase, '.gsd', 'REQUIREMENTS.md'), 'utf-8'); - assertTrue( + assert.ok( reqContent.includes('R090'), 'REQUIREMENTS.md has R090 from worktree', ); const projContent = readFileSync(join(mainBase, '.gsd', 'PROJECT.md'), 'utf-8'); - assertTrue( + assert.ok( projContent.includes('M007'), 'PROJECT.md has M007 from worktree', ); @@ -441,11 +441,11 @@ async function main(): Promise { // Main's REQUIREMENTS should be untouched (worktree had nothing to sync) const content = readFileSync(join(mainBase, '.gsd', 'REQUIREMENTS.md'), 'utf-8'); - assertTrue( + assert.ok( content === '# Original', 'REQUIREMENTS.md unchanged when worktree has no copy', ); - assertTrue( + assert.ok( !synced.includes('REQUIREMENTS.md'), 'REQUIREMENTS.md not in synced list', ); @@ -473,11 +473,11 @@ async function main(): Promise { ); // Main has neither - assertTrue( + assert.ok( !existsSync(join(mainBase, '.gsd', 'QUEUE.md')), 'QUEUE.md missing in main before sync', ); - assertTrue( + assert.ok( !existsSync(join(mainBase, '.gsd', 'completed-units.json')), 'completed-units.json missing in main before sync', ); @@ -485,31 +485,31 @@ async function main(): Promise { const { synced } = syncWorktreeStateBack(mainBase, wtBase, 'M001'); // QUEUE.md should be synced - assertTrue( + assert.ok( existsSync(join(mainBase, '.gsd', 'QUEUE.md')), '#1787: QUEUE.md synced from worktree to main', ); const queueContent = readFileSync(join(mainBase, '.gsd', 'QUEUE.md'), 'utf-8'); - assertTrue( + assert.ok( queueContent.includes('M002 next'), '#1787: QUEUE.md has correct content', ); - assertTrue( + assert.ok( synced.includes('QUEUE.md'), '#1787: QUEUE.md appears in synced list', ); // completed-units.json should be synced - assertTrue( + assert.ok( existsSync(join(mainBase, '.gsd', 'completed-units.json')), '#1787: completed-units.json synced from worktree to main', ); const cuContent = readFileSync(join(mainBase, '.gsd', 'completed-units.json'), 'utf-8'); - assertTrue( + assert.ok( cuContent.includes('M001-S01-T01'), '#1787: completed-units.json has correct content', ); - assertTrue( + assert.ok( synced.includes('completed-units.json'), '#1787: completed-units.json appears in synced list', ); @@ -535,20 +535,20 @@ async function main(): Promise { mkdirSync(suffixDir, { recursive: true }); writeFileSync(join(suffixDir, 'M001-abc123-CONTEXT.md'), '# M001 Context'); - assertTrue(!existsSync(join(wtBase, '.gsd', 'milestones', 'sprint-alpha')), 'sprint-alpha missing before sync'); - assertTrue(!existsSync(join(wtBase, '.gsd', 'milestones', 'M001-abc123')), 'M001-abc123 missing before sync'); + assert.ok(!existsSync(join(wtBase, '.gsd', 'milestones', 'sprint-alpha')), 'sprint-alpha missing before sync'); + assert.ok(!existsSync(join(wtBase, '.gsd', 'milestones', 'M001-abc123')), 'M001-abc123 missing before sync'); const result = syncGsdStateToWorktree(mainBase, wtBase); - assertTrue( + assert.ok( existsSync(join(wtBase, '.gsd', 'milestones', 'sprint-alpha', 'CONTEXT.md')), '#1547: non-standard milestone dir "sprint-alpha" synced to worktree', ); - assertTrue( + assert.ok( existsSync(join(wtBase, '.gsd', 'milestones', 'M001-abc123', 'M001-abc123-CONTEXT.md')), '#1547: suffixed milestone dir "M001-abc123" synced to worktree', ); - assertTrue(result.synced.length > 0, 'sync reported files'); + assert.ok(result.synced.length > 0, 'sync reported files'); } finally { cleanup(mainBase); cleanup(wtBase); @@ -570,18 +570,18 @@ async function main(): Promise { mkdirSync(wtCustomDir, { recursive: true }); writeFileSync(join(wtCustomDir, 'SUMMARY.md'), '# Sprint Beta Summary'); - assertTrue( + assert.ok( !existsSync(join(mainBase, '.gsd', 'milestones', 'sprint-beta')), 'sprint-beta missing in main before sync', ); const { synced } = syncWorktreeStateBack(mainBase, wtBase, 'M001'); - assertTrue( + assert.ok( existsSync(join(mainBase, '.gsd', 'milestones', 'sprint-beta', 'SUMMARY.md')), '#1547: non-standard milestone dir "sprint-beta" synced back to main', ); - assertTrue( + assert.ok( synced.some((p) => p.includes('sprint-beta')), '#1547: sprint-beta appears in synced list', ); @@ -590,11 +590,4 @@ async function main(): Promise { rmSync(wtBase, { recursive: true, force: true }); } } - - report(); -} - -main().catch((error) => { - console.error(error); - process.exit(1); }); diff --git a/src/resources/extensions/gsd/tests/worktree.test.ts b/src/resources/extensions/gsd/tests/worktree.test.ts index f1829de04..71dd32be7 100644 --- a/src/resources/extensions/gsd/tests/worktree.test.ts +++ b/src/resources/extensions/gsd/tests/worktree.test.ts @@ -17,9 +17,9 @@ import { } from "../worktree.ts"; import { readIntegrationBranch } from "../git-service.ts"; import { _resetHasChangesCache } from "../native-git-bridge.ts"; -import { createTestContext } from './test-helpers.ts'; +import { describe, test } from 'node:test'; +import assert from 'node:assert/strict'; -const { assertEq, assertTrue, report } = createTestContext(); /** * Normalize a path for reliable comparison on Windows CI runners. @@ -47,56 +47,56 @@ writeFileSync(join(base, ".gsd", "milestones", "M001", "slices", "S01", "S01-PLA run("git add .", base); run('git commit -m "chore: init"', base); -async function main(): Promise { +describe('worktree', async () => { console.log("\n=== autoCommitCurrentBranch ==="); // Clean — should return null const cleanResult = autoCommitCurrentBranch(base, "execute-task", "M001/S01/T01"); - assertEq(cleanResult, null, "returns null for clean repo"); + assert.deepStrictEqual(cleanResult, null, "returns null for clean repo"); // Make dirty — reset the nativeHasChanges cache so the fresh dirt is detected _resetHasChangesCache(); writeFileSync(join(base, "dirty.txt"), "uncommitted\n", "utf-8"); const dirtyResult = autoCommitCurrentBranch(base, "execute-task", "M001/S01/T01"); - assertTrue(dirtyResult !== null, "returns commit message for dirty repo"); - assertTrue(dirtyResult!.includes("M001/S01/T01"), "commit message includes unit id"); - assertEq(run("git status --short", base), "", "repo is clean after auto-commit"); + assert.ok(dirtyResult !== null, "returns commit message for dirty repo"); + assert.ok(dirtyResult!.includes("M001/S01/T01"), "commit message includes unit id"); + assert.deepStrictEqual(run("git status --short", base), "", "repo is clean after auto-commit"); console.log("\n=== getSliceBranchName ==="); - assertEq(getSliceBranchName("M001", "S01"), "gsd/M001/S01", "branch name format correct"); - assertEq(getSliceBranchName("M001", "S01", null), "gsd/M001/S01", "null worktree = plain branch"); - assertEq(getSliceBranchName("M001", "S01", "my-wt"), "gsd/my-wt/M001/S01", "worktree-namespaced branch"); + assert.deepStrictEqual(getSliceBranchName("M001", "S01"), "gsd/M001/S01", "branch name format correct"); + assert.deepStrictEqual(getSliceBranchName("M001", "S01", null), "gsd/M001/S01", "null worktree = plain branch"); + assert.deepStrictEqual(getSliceBranchName("M001", "S01", "my-wt"), "gsd/my-wt/M001/S01", "worktree-namespaced branch"); console.log("\n=== parseSliceBranch ==="); const plain = parseSliceBranch("gsd/M001/S01"); - assertTrue(plain !== null, "parses plain branch"); - assertEq(plain!.worktreeName, null, "plain branch has no worktree name"); - assertEq(plain!.milestoneId, "M001", "plain branch milestone"); - assertEq(plain!.sliceId, "S01", "plain branch slice"); + assert.ok(plain !== null, "parses plain branch"); + assert.deepStrictEqual(plain!.worktreeName, null, "plain branch has no worktree name"); + assert.deepStrictEqual(plain!.milestoneId, "M001", "plain branch milestone"); + assert.deepStrictEqual(plain!.sliceId, "S01", "plain branch slice"); const namespaced = parseSliceBranch("gsd/feature-auth/M001/S01"); - assertTrue(namespaced !== null, "parses worktree-namespaced branch"); - assertEq(namespaced!.worktreeName, "feature-auth", "worktree name extracted"); - assertEq(namespaced!.milestoneId, "M001", "namespaced branch milestone"); - assertEq(namespaced!.sliceId, "S01", "namespaced branch slice"); + assert.ok(namespaced !== null, "parses worktree-namespaced branch"); + assert.deepStrictEqual(namespaced!.worktreeName, "feature-auth", "worktree name extracted"); + assert.deepStrictEqual(namespaced!.milestoneId, "M001", "namespaced branch milestone"); + assert.deepStrictEqual(namespaced!.sliceId, "S01", "namespaced branch slice"); const invalid = parseSliceBranch("main"); - assertEq(invalid, null, "non-slice branch returns null"); + assert.deepStrictEqual(invalid, null, "non-slice branch returns null"); const worktreeBranch = parseSliceBranch("worktree/foo"); - assertEq(worktreeBranch, null, "worktree/ prefix is not a slice branch"); + assert.deepStrictEqual(worktreeBranch, null, "worktree/ prefix is not a slice branch"); console.log("\n=== SLICE_BRANCH_RE ==="); - assertTrue(SLICE_BRANCH_RE.test("gsd/M001/S01"), "regex matches plain branch"); - assertTrue(SLICE_BRANCH_RE.test("gsd/my-wt/M001/S01"), "regex matches worktree branch"); - assertTrue(!SLICE_BRANCH_RE.test("main"), "regex rejects main"); - assertTrue(!SLICE_BRANCH_RE.test("gsd/"), "regex rejects bare gsd/"); - assertTrue(!SLICE_BRANCH_RE.test("worktree/foo"), "regex rejects worktree/foo"); + assert.ok(SLICE_BRANCH_RE.test("gsd/M001/S01"), "regex matches plain branch"); + assert.ok(SLICE_BRANCH_RE.test("gsd/my-wt/M001/S01"), "regex matches worktree branch"); + assert.ok(!SLICE_BRANCH_RE.test("main"), "regex rejects main"); + assert.ok(!SLICE_BRANCH_RE.test("gsd/"), "regex rejects bare gsd/"); + assert.ok(!SLICE_BRANCH_RE.test("worktree/foo"), "regex rejects worktree/foo"); console.log("\n=== detectWorktreeName ==="); - assertEq(detectWorktreeName("/projects/myapp"), null, "no worktree in plain path"); - assertEq(detectWorktreeName("/projects/myapp/.gsd/worktrees/feature-auth"), "feature-auth", "detects worktree name"); - assertEq(detectWorktreeName("/projects/myapp/.gsd/worktrees/my-wt/subdir"), "my-wt", "detects worktree with subdir"); + assert.deepStrictEqual(detectWorktreeName("/projects/myapp"), null, "no worktree in plain path"); + assert.deepStrictEqual(detectWorktreeName("/projects/myapp/.gsd/worktrees/feature-auth"), "feature-auth", "detects worktree name"); + assert.deepStrictEqual(detectWorktreeName("/projects/myapp/.gsd/worktrees/my-wt/subdir"), "my-wt", "detects worktree with subdir"); // ═══════════════════════════════════════════════════════════════════════ // Integration branch — facade-level tests @@ -115,16 +115,16 @@ async function main(): Promise { run("git add -A && git commit -m init", repo); run("git checkout -b f-123-thing", repo); - assertEq(getCurrentBranch(repo), "f-123-thing", "on feature branch"); + assert.deepStrictEqual(getCurrentBranch(repo), "f-123-thing", "on feature branch"); const commitsBefore = run("git rev-list --count HEAD", repo); captureIntegrationBranch(repo, "M001"); - assertEq(readIntegrationBranch(repo, "M001"), "f-123-thing", + assert.deepStrictEqual(readIntegrationBranch(repo, "M001"), "f-123-thing", "captureIntegrationBranch records the current branch"); // Metadata is stored in external state, not committed to git. const commitsAfter = run("git rev-list --count HEAD", repo); - assertEq(commitsAfter, commitsBefore, "captureIntegrationBranch does not create a git commit"); + assert.deepStrictEqual(commitsAfter, commitsBefore, "captureIntegrationBranch does not create a git commit"); rmSync(repo, { recursive: true, force: true }); } @@ -144,7 +144,7 @@ async function main(): Promise { run("git checkout -b gsd/M001/S01", repo); captureIntegrationBranch(repo, "M001"); - assertEq(readIntegrationBranch(repo, "M001"), null, + assert.deepStrictEqual(readIntegrationBranch(repo, "M001"), null, "capture from slice branch is a no-op"); rmSync(repo, { recursive: true, force: true }); @@ -167,12 +167,12 @@ async function main(): Promise { // Without milestone set, getMainBranch returns "main" setActiveMilestoneId(repo, null); - assertEq(getMainBranch(repo), "main", + assert.deepStrictEqual(getMainBranch(repo), "main", "getMainBranch returns main without milestone set"); // With milestone set, getMainBranch returns feature branch setActiveMilestoneId(repo, "M001"); - assertEq(getMainBranch(repo), "my-feature", + assert.deepStrictEqual(getMainBranch(repo), "my-feature", "getMainBranch returns integration branch with milestone set"); rmSync(repo, { recursive: true, force: true }); @@ -180,22 +180,22 @@ async function main(): Promise { // ── detectWorktreeName: symlink-resolved paths ─────────────────────────── console.log("\n=== detectWorktreeName (symlink-resolved paths) ==="); - assertEq( + assert.deepStrictEqual( detectWorktreeName("/Users/fran/.gsd/projects/89e1c9ad49bf/worktrees/M001"), "M001", "detects milestone in symlink-resolved path", ); - assertEq( + assert.deepStrictEqual( detectWorktreeName("/Users/fran/.gsd/projects/abc123/worktrees/M002/subdir"), "M002", "detects milestone with trailing subdir in symlink-resolved path", ); - assertEq( + assert.deepStrictEqual( detectWorktreeName("/Users/fran/.gsd/projects/abc123"), null, "returns null for project root without worktrees segment", ); - assertEq( + assert.deepStrictEqual( detectWorktreeName("/foo/.gsd/worktrees/M001"), "M001", "still detects direct layout path", @@ -211,7 +211,7 @@ async function main(): Promise { // With GSD_PROJECT_ROOT env var set (layer 1 — coordinator passes it) process.env.GSD_PROJECT_ROOT = "/real/project"; - assertEq( + assert.deepStrictEqual( resolveProjectRoot("/Users/fran/.gsd/projects/89e1c9ad49bf/worktrees/M001"), "/real/project", "uses GSD_PROJECT_ROOT when set", @@ -219,7 +219,7 @@ async function main(): Promise { delete process.env.GSD_PROJECT_ROOT; // Without GSD_PROJECT_ROOT, direct layout still works (no ~/.gsd collision) - assertEq( + assert.deepStrictEqual( resolveProjectRoot("/some/repo"), "/some/repo", "ignores GSD_PROJECT_ROOT override for non-worktree paths", @@ -227,19 +227,19 @@ async function main(): Promise { delete process.env.GSD_PROJECT_ROOT; // Without GSD_PROJECT_ROOT, direct layout still works (no ~/.gsd collision) - assertEq( + assert.deepStrictEqual( resolveProjectRoot("/foo/.gsd/worktrees/M001"), "/foo", "still resolves direct layout path", ); - assertEq( + assert.deepStrictEqual( resolveProjectRoot("/some/repo"), "/some/repo", "returns unchanged for non-worktree path", ); // Without GSD_PROJECT_ROOT, direct layout with nested subdirs - assertEq( + assert.deepStrictEqual( resolveProjectRoot("/data/.gsd/worktrees/M003/nested"), "/data", "resolves correctly with nested subdirs after worktree name (direct layout)", @@ -264,7 +264,7 @@ async function main(): Promise { mkdirSync(deep, { recursive: true }); process.env.GSD_HOME = join(fakeHome, ".gsd"); - assertEq( + assert.deepStrictEqual( normalizePath(resolveProjectRoot(realpathSync(deep))), normalizePath(project), "resolves to real project root from deep symlink-resolved worktree path", @@ -276,10 +276,4 @@ async function main(): Promise { } rmSync(base, { recursive: true, force: true }); - report(); -} - -main().catch((error) => { - console.error(error); - process.exit(1); });