From 4498dcea32f4ea5f850071844e762f7a959d68e1 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Tue, 24 Mar 2026 23:33:01 -0400 Subject: [PATCH] refactor(test): migrate gsd/tests i-n from custom harness to node:test (#2399) --- .../gsd/tests/idle-recovery.test.ts | 183 +++++----- .../gsd/tests/integration-edge.test.ts | 87 +++-- .../gsd/tests/integration-lifecycle.test.ts | 95 +++-- .../integration-mixed-milestones.test.ts | 166 ++++----- .../gsd/tests/markdown-renderer.test.ts | 344 ++++++++---------- .../extensions/gsd/tests/md-importer.test.ts | 224 +++++------- .../gsd/tests/memory-extractor.test.ts | 99 +++-- .../extensions/gsd/tests/memory-store.test.ts | 173 ++++----- .../gsd/tests/migrate-command.test.ts | 123 +++---- .../gsd/tests/migrate-hierarchy.test.ts | 176 +++++---- .../gsd/tests/migrate-parser.test.ts | 331 ++++++++--------- .../gsd/tests/migrate-transformer.test.ts | 266 +++++++------- .../tests/migrate-validator-parsers.test.ts | 234 ++++++------ .../tests/migrate-writer-integration.test.ts | 183 +++++----- .../gsd/tests/migrate-writer.test.ts | 277 ++++++-------- .../gsd/tests/must-have-parser.test.ts | 173 ++++----- .../gsd/tests/none-mode-gates.test.ts | 75 ++-- 17 files changed, 1468 insertions(+), 1741 deletions(-) diff --git a/src/resources/extensions/gsd/tests/idle-recovery.test.ts b/src/resources/extensions/gsd/tests/idle-recovery.test.ts index 0f500f199..f13b3a32e 100644 --- a/src/resources/extensions/gsd/tests/idle-recovery.test.ts +++ b/src/resources/extensions/gsd/tests/idle-recovery.test.ts @@ -8,9 +8,9 @@ import { verifyExpectedArtifact, buildLoopRemediationSteps, } from "../auto.ts"; -import { createTestContext } from './test-helpers.ts'; +import { describe, test, beforeEach, afterEach } from 'node:test'; +import assert from 'node:assert/strict'; -const { assertEq, assertTrue, report } = createTestContext(); function createFixtureBase(): string { const base = mkdtempSync(join(tmpdir(), "gsd-idle-recovery-test-")); mkdirSync(join(base, ".gsd", "milestones", "M001", "slices", "S01", "tasks"), { recursive: true }); @@ -23,99 +23,91 @@ function cleanup(base: string): void { // ═══ resolveExpectedArtifactPath ═════════════════════════════════════════════ -{ - console.log("\n=== resolveExpectedArtifactPath: research-milestone ==="); +test('resolveExpectedArtifactPath: research-milestone', () => { const base = createFixtureBase(); try { const result = resolveExpectedArtifactPath("research-milestone", "M001", base); - assertTrue(result !== null, "should resolve a path"); - assertTrue(result!.endsWith("M001-RESEARCH.md"), `path should end with M001-RESEARCH.md, got ${result}`); + assert.ok(result !== null, "should resolve a path"); + assert.ok(result!.endsWith("M001-RESEARCH.md"), `path should end with M001-RESEARCH.md, got ${result}`); } finally { cleanup(base); } -} +}); -{ - console.log("\n=== resolveExpectedArtifactPath: plan-milestone ==="); +test('resolveExpectedArtifactPath: plan-milestone', () => { const base = createFixtureBase(); try { const result = resolveExpectedArtifactPath("plan-milestone", "M001", base); - assertTrue(result !== null, "should resolve a path"); - assertTrue(result!.endsWith("M001-ROADMAP.md"), `path should end with M001-ROADMAP.md, got ${result}`); + assert.ok(result !== null, "should resolve a path"); + assert.ok(result!.endsWith("M001-ROADMAP.md"), `path should end with M001-ROADMAP.md, got ${result}`); } finally { cleanup(base); } -} +}); -{ - console.log("\n=== resolveExpectedArtifactPath: research-slice ==="); +test('resolveExpectedArtifactPath: research-slice', () => { const base = createFixtureBase(); try { const result = resolveExpectedArtifactPath("research-slice", "M001/S01", base); - assertTrue(result !== null, "should resolve a path"); - assertTrue(result!.endsWith("S01-RESEARCH.md"), `path should end with S01-RESEARCH.md, got ${result}`); + assert.ok(result !== null, "should resolve a path"); + assert.ok(result!.endsWith("S01-RESEARCH.md"), `path should end with S01-RESEARCH.md, got ${result}`); } finally { cleanup(base); } -} +}); -{ - console.log("\n=== resolveExpectedArtifactPath: plan-slice ==="); +test('resolveExpectedArtifactPath: plan-slice', () => { const base = createFixtureBase(); try { const result = resolveExpectedArtifactPath("plan-slice", "M001/S01", base); - assertTrue(result !== null, "should resolve a path"); - assertTrue(result!.endsWith("S01-PLAN.md"), `path should end with S01-PLAN.md, got ${result}`); + assert.ok(result !== null, "should resolve a path"); + assert.ok(result!.endsWith("S01-PLAN.md"), `path should end with S01-PLAN.md, got ${result}`); } finally { cleanup(base); } -} +}); -{ - console.log("\n=== resolveExpectedArtifactPath: complete-milestone ==="); +test('resolveExpectedArtifactPath: complete-milestone', () => { const base = createFixtureBase(); try { const result = resolveExpectedArtifactPath("complete-milestone", "M001", base); - assertTrue(result !== null, "should resolve a path"); - assertTrue(result!.endsWith("M001-SUMMARY.md"), `path should end with M001-SUMMARY.md, got ${result}`); + assert.ok(result !== null, "should resolve a path"); + assert.ok(result!.endsWith("M001-SUMMARY.md"), `path should end with M001-SUMMARY.md, got ${result}`); } finally { cleanup(base); } -} +}); -{ - console.log("\n=== resolveExpectedArtifactPath: unknown unit type → null ==="); +test('resolveExpectedArtifactPath: unknown unit type → null', () => { const base = createFixtureBase(); try { const result = resolveExpectedArtifactPath("unknown-type", "M001/S01", base); - assertEq(result, null, "unknown type returns null"); + assert.deepStrictEqual(result, null, "unknown type returns null"); } finally { cleanup(base); } -} +}); // ═══ writeBlockerPlaceholder ═════════════════════════════════════════════════ -{ - console.log("\n=== writeBlockerPlaceholder: writes file for research-slice ==="); +test('writeBlockerPlaceholder: writes file for research-slice', () => { const base = createFixtureBase(); try { const result = writeBlockerPlaceholder("research-slice", "M001/S01", base, "idle recovery exhausted 2 attempts"); - assertTrue(result !== null, "should return relative path"); + assert.ok(result !== null, "should return relative path"); const absPath = resolveExpectedArtifactPath("research-slice", "M001/S01", base)!; - assertTrue(existsSync(absPath), "file should exist on disk"); + assert.ok(existsSync(absPath), "file should exist on disk"); const content = readFileSync(absPath, "utf-8"); - assertTrue(content.includes("BLOCKER"), "should contain BLOCKER heading"); - assertTrue(content.includes("idle recovery exhausted 2 attempts"), "should contain the reason"); - assertTrue(content.includes("research-slice"), "should mention the unit type"); - assertTrue(content.includes("M001/S01"), "should mention the unit ID"); + assert.ok(content.includes("BLOCKER"), "should contain BLOCKER heading"); + assert.ok(content.includes("idle recovery exhausted 2 attempts"), "should contain the reason"); + assert.ok(content.includes("research-slice"), "should mention the unit type"); + assert.ok(content.includes("M001/S01"), "should mention the unit ID"); } finally { cleanup(base); } -} +}); -{ - console.log("\n=== writeBlockerPlaceholder: creates directory if missing ==="); +test('writeBlockerPlaceholder: creates directory if missing', () => { const base = mkdtempSync(join(tmpdir(), "gsd-idle-recovery-test-")); try { // Only create milestone dir, not slice dir @@ -123,38 +115,36 @@ function cleanup(base: string): void { // resolveSlicePath needs the slice dir to exist to resolve, so this should return null const result = writeBlockerPlaceholder("research-slice", "M001/S01", base, "test reason"); // Since the slice dir doesn't exist, resolveExpectedArtifactPath returns null - assertEq(result, null, "returns null when directory structure doesn't exist"); + assert.deepStrictEqual(result, null, "returns null when directory structure doesn't exist"); } finally { cleanup(base); } -} +}); -{ - console.log("\n=== writeBlockerPlaceholder: writes file for research-milestone ==="); +test('writeBlockerPlaceholder: writes file for research-milestone', () => { const base = createFixtureBase(); try { const result = writeBlockerPlaceholder("research-milestone", "M001", base, "hard timeout"); - assertTrue(result !== null, "should return relative path"); + assert.ok(result !== null, "should return relative path"); const absPath = resolveExpectedArtifactPath("research-milestone", "M001", base)!; - assertTrue(existsSync(absPath), "file should exist on disk"); + assert.ok(existsSync(absPath), "file should exist on disk"); const content = readFileSync(absPath, "utf-8"); - assertTrue(content.includes("BLOCKER"), "should contain BLOCKER heading"); - assertTrue(content.includes("hard timeout"), "should contain the reason"); + assert.ok(content.includes("BLOCKER"), "should contain BLOCKER heading"); + assert.ok(content.includes("hard timeout"), "should contain the reason"); } finally { cleanup(base); } -} +}); -{ - console.log("\n=== writeBlockerPlaceholder: unknown type → null ==="); +test('writeBlockerPlaceholder: unknown type → null', () => { const base = createFixtureBase(); try { const result = writeBlockerPlaceholder("unknown-type", "M001/S01", base, "test"); - assertEq(result, null, "unknown type returns null"); + assert.deepStrictEqual(result, null, "unknown type returns null"); } finally { cleanup(base); } -} +}); // ═══ verifyExpectedArtifact: complete-slice roadmap check ════════════════════ // Regression for #indefinite-hang: complete-slice must verify roadmap [x] or @@ -177,8 +167,7 @@ const ROADMAP_COMPLETE = `# M001: Test Milestone > After this: something works `; -{ - console.log("\n=== verifyExpectedArtifact: complete-slice — all artifacts present + roadmap marked [x] returns true ==="); +test('verifyExpectedArtifact: complete-slice — all artifacts present + roadmap marked [x] returns true', () => { const base = createFixtureBase(); try { const sliceDir = join(base, ".gsd", "milestones", "M001", "slices", "S01"); @@ -186,14 +175,13 @@ const ROADMAP_COMPLETE = `# M001: Test Milestone writeFileSync(join(sliceDir, "S01-UAT.md"), "# UAT\n", "utf-8"); writeFileSync(join(base, ".gsd", "milestones", "M001", "M001-ROADMAP.md"), ROADMAP_COMPLETE, "utf-8"); const result = verifyExpectedArtifact("complete-slice", "M001/S01", base); - assertTrue(result === true, "SUMMARY + UAT + roadmap [x] should verify as true"); + assert.ok(result === true, "SUMMARY + UAT + roadmap [x] should verify as true"); } finally { cleanup(base); } -} +}); -{ - console.log("\n=== verifyExpectedArtifact: complete-slice — SUMMARY + UAT present but roadmap NOT marked [x] returns false ==="); +test('verifyExpectedArtifact: complete-slice — SUMMARY + UAT present but roadmap NOT marked [x] returns false', () => { const base = createFixtureBase(); try { const sliceDir = join(base, ".gsd", "milestones", "M001", "slices", "S01"); @@ -201,14 +189,13 @@ const ROADMAP_COMPLETE = `# M001: Test Milestone writeFileSync(join(sliceDir, "S01-UAT.md"), "# UAT\n", "utf-8"); writeFileSync(join(base, ".gsd", "milestones", "M001", "M001-ROADMAP.md"), ROADMAP_INCOMPLETE, "utf-8"); const result = verifyExpectedArtifact("complete-slice", "M001/S01", base); - assertTrue(result === false, "roadmap not marked [x] should return false (crash recovery scenario)"); + assert.ok(result === false, "roadmap not marked [x] should return false (crash recovery scenario)"); } finally { cleanup(base); } -} +}); -{ - console.log("\n=== verifyExpectedArtifact: complete-slice — SUMMARY present but UAT missing returns false ==="); +test('verifyExpectedArtifact: complete-slice — SUMMARY present but UAT missing returns false', () => { const base = createFixtureBase(); try { const sliceDir = join(base, ".gsd", "milestones", "M001", "slices", "S01"); @@ -216,14 +203,13 @@ const ROADMAP_COMPLETE = `# M001: Test Milestone // no UAT file writeFileSync(join(base, ".gsd", "milestones", "M001", "M001-ROADMAP.md"), ROADMAP_COMPLETE, "utf-8"); const result = verifyExpectedArtifact("complete-slice", "M001/S01", base); - assertTrue(result === false, "missing UAT should return false"); + assert.ok(result === false, "missing UAT should return false"); } finally { cleanup(base); } -} +}); -{ - console.log("\n=== verifyExpectedArtifact: complete-slice — no roadmap file present is lenient (returns true) ==="); +test('verifyExpectedArtifact: complete-slice — no roadmap file present is lenient (returns true)', () => { const base = createFixtureBase(); try { const sliceDir = join(base, ".gsd", "milestones", "M001", "slices", "S01"); @@ -231,87 +217,80 @@ const ROADMAP_COMPLETE = `# M001: Test Milestone writeFileSync(join(sliceDir, "S01-UAT.md"), "# UAT\n", "utf-8"); // no roadmap file const result = verifyExpectedArtifact("complete-slice", "M001/S01", base); - assertTrue(result === true, "missing roadmap file should be lenient and return true"); + assert.ok(result === true, "missing roadmap file should be lenient and return true"); } finally { cleanup(base); } -} +}); // ═══ buildLoopRemediationSteps ═══════════════════════════════════════════════ -{ - console.log("\n=== buildLoopRemediationSteps: execute-task returns concrete steps ==="); +test('buildLoopRemediationSteps: execute-task returns concrete steps', () => { const base = mkdtempSync(join(tmpdir(), "gsd-loop-remediation-test-")); try { mkdirSync(join(base, ".gsd", "milestones", "M002", "slices", "S03", "tasks"), { recursive: true }); const result = buildLoopRemediationSteps("execute-task", "M002/S03/T01", base); - assertTrue(result !== null, "should return remediation steps"); - assertTrue(result!.includes("gsd undo-task"), "steps include undo-task command"); - assertTrue(result!.includes("T01"), "steps mention the task ID"); - assertTrue(result!.includes("gsd undo-task"), "steps include gsd undo-task command"); + assert.ok(result !== null, "should return remediation steps"); + assert.ok(result!.includes("gsd undo-task"), "steps include undo-task command"); + assert.ok(result!.includes("T01"), "steps mention the task ID"); + assert.ok(result!.includes("gsd undo-task"), "steps include gsd undo-task command"); } finally { rmSync(base, { recursive: true, force: true }); } -} +}); -{ - console.log("\n=== buildLoopRemediationSteps: plan-slice returns concrete steps ==="); +test('buildLoopRemediationSteps: plan-slice returns concrete steps', () => { const base = mkdtempSync(join(tmpdir(), "gsd-loop-remediation-test-")); try { mkdirSync(join(base, ".gsd", "milestones", "M001", "slices", "S01"), { recursive: true }); const result = buildLoopRemediationSteps("plan-slice", "M001/S01", base); - assertTrue(result !== null, "should return remediation steps for plan-slice"); - assertTrue(result!.includes("S01-PLAN.md"), "steps mention the slice plan file"); - assertTrue(result!.includes("gsd recover"), "steps include gsd recover command"); + assert.ok(result !== null, "should return remediation steps for plan-slice"); + assert.ok(result!.includes("S01-PLAN.md"), "steps mention the slice plan file"); + assert.ok(result!.includes("gsd recover"), "steps include gsd recover command"); } finally { rmSync(base, { recursive: true, force: true }); } -} +}); -{ - console.log("\n=== buildLoopRemediationSteps: research-slice returns concrete steps ==="); +test('buildLoopRemediationSteps: research-slice returns concrete steps', () => { const base = mkdtempSync(join(tmpdir(), "gsd-loop-remediation-test-")); try { mkdirSync(join(base, ".gsd", "milestones", "M001", "slices", "S01"), { recursive: true }); const result = buildLoopRemediationSteps("research-slice", "M001/S01", base); - assertTrue(result !== null, "should return remediation steps for research-slice"); - assertTrue(result!.includes("S01-RESEARCH.md"), "steps mention the slice research file"); - assertTrue(result!.includes("gsd recover"), "steps include gsd recover command"); + assert.ok(result !== null, "should return remediation steps for research-slice"); + assert.ok(result!.includes("S01-RESEARCH.md"), "steps mention the slice research file"); + assert.ok(result!.includes("gsd recover"), "steps include gsd recover command"); } finally { rmSync(base, { recursive: true, force: true }); } -} +}); -{ - console.log("\n=== buildLoopRemediationSteps: unknown type returns null ==="); +test('buildLoopRemediationSteps: unknown type returns null', () => { const base = mkdtempSync(join(tmpdir(), "gsd-loop-remediation-test-")); try { const result = buildLoopRemediationSteps("unknown-type", "M001/S01", base); - assertEq(result, null, "unknown type returns null"); + assert.deepStrictEqual(result, null, "unknown type returns null"); } finally { rmSync(base, { recursive: true, force: true }); } -} +}); // ═══ verifyExpectedArtifact: hook unit types ═════════════════════════════════ -console.log("\n=== verifyExpectedArtifact: hook types always return true ==="); - -{ +test('verifyExpectedArtifact: hook types always return true', () => { const base = createFixtureBase(); try { // Hook units don't have standard artifacts — they should always pass const result1 = verifyExpectedArtifact("hook/code-review", "M001/S01/T01", base); - assertTrue(result1, "hook/code-review should always return true"); + assert.ok(result1, "hook/code-review should always return true"); const result2 = verifyExpectedArtifact("hook/simplify", "M001/S01/T02", base); - assertTrue(result2, "hook/simplify should always return true"); + assert.ok(result2, "hook/simplify should always return true"); const result3 = verifyExpectedArtifact("hook/custom-hook", "M001/S01", base); - assertTrue(result3, "hook/custom-hook at slice level should return true"); + assert.ok(result3, "hook/custom-hook at slice level should return true"); } finally { rmSync(base, { recursive: true, force: true }); } -} +}); -report(); diff --git a/src/resources/extensions/gsd/tests/integration-edge.test.ts b/src/resources/extensions/gsd/tests/integration-edge.test.ts index befa0779f..d3a1ecf24 100644 --- a/src/resources/extensions/gsd/tests/integration-edge.test.ts +++ b/src/resources/extensions/gsd/tests/integration-edge.test.ts @@ -19,9 +19,8 @@ import { formatDecisionsForPrompt, formatRequirementsForPrompt, } from '../context-store.ts'; -import { createTestContext } from './test-helpers.ts'; - -const { assertEq, assertTrue, report } = createTestContext(); +import { describe, test, beforeEach, afterEach } from 'node:test'; +import assert from 'node:assert/strict'; // ─── Fixture Helper ──────────────────────────────────────────────────────── @@ -48,8 +47,7 @@ function generateDecisionsMarkdown(count: number): string { // Edge Case 1: Empty Project // ═══════════════════════════════════════════════════════════════════════════ -console.log('\n=== integration-edge: empty project ==='); -{ +test('integration-edge: empty project', () => { const base = mkdtempSync(join(tmpdir(), 'gsd-int-edge-empty-')); const gsdDir = join(base, '.gsd'); mkdirSync(gsdDir, { recursive: true }); @@ -59,55 +57,54 @@ console.log('\n=== integration-edge: empty project ==='); try { // Open DB first so migrateFromMarkdown doesn't auto-create at default path openDatabase(dbPath); - assertTrue(isDbAvailable(), 'empty: DB available after open'); + assert.ok(isDbAvailable(), 'empty: DB available after open'); // Migrate with no markdown files on disk const result = migrateFromMarkdown(base); - assertEq(result.decisions, 0, 'empty: 0 decisions imported'); - assertEq(result.requirements, 0, 'empty: 0 requirements imported'); - assertEq(result.artifacts, 0, 'empty: 0 artifacts imported'); + assert.deepStrictEqual(result.decisions, 0, 'empty: 0 decisions imported'); + assert.deepStrictEqual(result.requirements, 0, 'empty: 0 requirements imported'); + assert.deepStrictEqual(result.artifacts, 0, 'empty: 0 artifacts imported'); // Query decisions → empty array const decisions = queryDecisions(); - assertEq(decisions.length, 0, 'empty: queryDecisions returns empty array'); + assert.deepStrictEqual(decisions.length, 0, 'empty: queryDecisions returns empty array'); // Query requirements → empty array const requirements = queryRequirements(); - assertEq(requirements.length, 0, 'empty: queryRequirements returns empty array'); + assert.deepStrictEqual(requirements.length, 0, 'empty: queryRequirements returns empty array'); // Query with scope filters → still empty, no crash const scopedDecisions = queryDecisions({ milestoneId: 'M001' }); - assertEq(scopedDecisions.length, 0, 'empty: scoped queryDecisions returns empty'); + assert.deepStrictEqual(scopedDecisions.length, 0, 'empty: scoped queryDecisions returns empty'); const scopedRequirements = queryRequirements({ sliceId: 'S01' }); - assertEq(scopedRequirements.length, 0, 'empty: scoped queryRequirements returns empty'); + assert.deepStrictEqual(scopedRequirements.length, 0, 'empty: scoped queryRequirements returns empty'); // Format empty results → empty strings const formattedD = formatDecisionsForPrompt([]); const formattedR = formatRequirementsForPrompt([]); - assertEq(formattedD, '', 'empty: formatDecisionsForPrompt returns empty string'); - assertEq(formattedR, '', 'empty: formatRequirementsForPrompt returns empty string'); + assert.deepStrictEqual(formattedD, '', 'empty: formatDecisionsForPrompt returns empty string'); + assert.deepStrictEqual(formattedR, '', 'empty: formatRequirementsForPrompt returns empty string'); // Format with actual empty query results const formattedD2 = formatDecisionsForPrompt(decisions); const formattedR2 = formatRequirementsForPrompt(requirements); - assertEq(formattedD2, '', 'empty: format of empty query decisions is empty string'); - assertEq(formattedR2, '', 'empty: format of empty query requirements is empty string'); + assert.deepStrictEqual(formattedD2, '', 'empty: format of empty query decisions is empty string'); + assert.deepStrictEqual(formattedR2, '', 'empty: format of empty query requirements is empty string'); closeDatabase(); } finally { closeDatabase(); rmSync(base, { recursive: true, force: true }); } -} +}); // ═══════════════════════════════════════════════════════════════════════════ // Edge Case 2: Partial Migration (decisions only, no requirements) // ═══════════════════════════════════════════════════════════════════════════ -console.log('\n=== integration-edge: partial migration ==='); -{ +test('integration-edge: partial migration', () => { const base = mkdtempSync(join(tmpdir(), 'gsd-int-edge-partial-')); const gsdDir = join(base, '.gsd'); mkdirSync(gsdDir, { recursive: true }); @@ -120,49 +117,48 @@ console.log('\n=== integration-edge: partial migration ==='); try { openDatabase(dbPath); - assertTrue(isDbAvailable(), 'partial: DB available after open'); + assert.ok(isDbAvailable(), 'partial: DB available after open'); const result = migrateFromMarkdown(base); // Decisions imported, requirements skipped gracefully - assertTrue(result.decisions === 6, `partial: imported ${result.decisions} decisions, expected 6`); - assertEq(result.requirements, 0, 'partial: 0 requirements imported (no file)'); + assert.ok(result.decisions === 6, `partial: imported ${result.decisions} decisions, expected 6`); + assert.deepStrictEqual(result.requirements, 0, 'partial: 0 requirements imported (no file)'); // Decisions queryable const decisions = queryDecisions(); - assertTrue(decisions.length === 6, `partial: queryDecisions returns 6 (got ${decisions.length})`); + assert.ok(decisions.length === 6, `partial: queryDecisions returns 6 (got ${decisions.length})`); const m001Decisions = queryDecisions({ milestoneId: 'M001' }); - assertTrue(m001Decisions.length > 0, 'partial: M001 decisions non-empty'); - assertTrue(m001Decisions.length < decisions.length, 'partial: M001 scope filters correctly'); + assert.ok(m001Decisions.length > 0, 'partial: M001 decisions non-empty'); + assert.ok(m001Decisions.length < decisions.length, 'partial: M001 scope filters correctly'); // Requirements return empty — no crash const requirements = queryRequirements(); - assertEq(requirements.length, 0, 'partial: queryRequirements returns empty'); + assert.deepStrictEqual(requirements.length, 0, 'partial: queryRequirements returns empty'); const scopedReqs = queryRequirements({ sliceId: 'S01' }); - assertEq(scopedReqs.length, 0, 'partial: scoped queryRequirements returns empty'); + assert.deepStrictEqual(scopedReqs.length, 0, 'partial: scoped queryRequirements returns empty'); // Format works on partial data const formattedD = formatDecisionsForPrompt(m001Decisions); - assertTrue(formattedD.length > 0, 'partial: formatted decisions non-empty'); + assert.ok(formattedD.length > 0, 'partial: formatted decisions non-empty'); const formattedR = formatRequirementsForPrompt(requirements); - assertEq(formattedR, '', 'partial: formatted empty requirements is empty string'); + assert.deepStrictEqual(formattedR, '', 'partial: formatted empty requirements is empty string'); closeDatabase(); } finally { closeDatabase(); rmSync(base, { recursive: true, force: true }); } -} +}); // ═══════════════════════════════════════════════════════════════════════════ // Edge Case 3: Fallback Mode (_resetProvider) // ═══════════════════════════════════════════════════════════════════════════ -console.log('\n=== integration-edge: fallback mode ==='); -{ +test('integration-edge: fallback mode', () => { const base = mkdtempSync(join(tmpdir(), 'gsd-int-edge-fallback-')); const gsdDir = join(base, '.gsd'); mkdirSync(gsdDir, { recursive: true }); @@ -175,54 +171,53 @@ console.log('\n=== integration-edge: fallback mode ==='); try { // Step 1: Open DB normally and verify it works openDatabase(dbPath); - assertTrue(isDbAvailable(), 'fallback: DB available after open'); + assert.ok(isDbAvailable(), 'fallback: DB available after open'); migrateFromMarkdown(base); const before = queryDecisions(); - assertTrue(before.length === 4, `fallback: 4 decisions before reset (got ${before.length})`); + assert.ok(before.length === 4, `fallback: 4 decisions before reset (got ${before.length})`); // Step 2: Close and reset provider → DB unavailable closeDatabase(); _resetProvider(); - assertTrue(!isDbAvailable(), 'fallback: DB unavailable after _resetProvider'); + assert.ok(!isDbAvailable(), 'fallback: DB unavailable after _resetProvider'); // Step 3: Queries degrade gracefully (return empty, don't throw) const degradedDecisions = queryDecisions(); - assertEq(degradedDecisions.length, 0, 'fallback: queryDecisions returns empty when unavailable'); + assert.deepStrictEqual(degradedDecisions.length, 0, 'fallback: queryDecisions returns empty when unavailable'); const degradedRequirements = queryRequirements(); - assertEq(degradedRequirements.length, 0, 'fallback: queryRequirements returns empty when unavailable'); + assert.deepStrictEqual(degradedRequirements.length, 0, 'fallback: queryRequirements returns empty when unavailable'); const degradedScopedD = queryDecisions({ milestoneId: 'M001' }); - assertEq(degradedScopedD.length, 0, 'fallback: scoped queryDecisions returns empty when unavailable'); + assert.deepStrictEqual(degradedScopedD.length, 0, 'fallback: scoped queryDecisions returns empty when unavailable'); const degradedScopedR = queryRequirements({ sliceId: 'S01' }); - assertEq(degradedScopedR.length, 0, 'fallback: scoped queryRequirements returns empty when unavailable'); + assert.deepStrictEqual(degradedScopedR.length, 0, 'fallback: scoped queryRequirements returns empty when unavailable'); // Format functions work on empty arrays (no crash) const formattedD = formatDecisionsForPrompt(degradedDecisions); - assertEq(formattedD, '', 'fallback: format degraded decisions is empty'); + assert.deepStrictEqual(formattedD, '', 'fallback: format degraded decisions is empty'); const formattedR = formatRequirementsForPrompt(degradedRequirements); - assertEq(formattedR, '', 'fallback: format degraded requirements is empty'); + assert.deepStrictEqual(formattedR, '', 'fallback: format degraded requirements is empty'); // Step 4: Re-open DB → restores availability openDatabase(dbPath); - assertTrue(isDbAvailable(), 'fallback: DB available after re-open'); + assert.ok(isDbAvailable(), 'fallback: DB available after re-open'); // Data should be there from the file-backed DB (persisted by first open) // But rows may need re-import since the DB was freshly opened from the file migrateFromMarkdown(base); const restored = queryDecisions(); - assertTrue(restored.length === 4, `fallback: 4 decisions after re-open (got ${restored.length})`); + assert.ok(restored.length === 4, `fallback: 4 decisions after re-open (got ${restored.length})`); closeDatabase(); } finally { closeDatabase(); rmSync(base, { recursive: true, force: true }); } -} +}); // ─── Report ──────────────────────────────────────────────────────────────── -report(); diff --git a/src/resources/extensions/gsd/tests/integration-lifecycle.test.ts b/src/resources/extensions/gsd/tests/integration-lifecycle.test.ts index 3cb94b765..2cfa31ea8 100644 --- a/src/resources/extensions/gsd/tests/integration-lifecycle.test.ts +++ b/src/resources/extensions/gsd/tests/integration-lifecycle.test.ts @@ -21,9 +21,8 @@ import { formatRequirementsForPrompt, } from '../context-store.ts'; import { saveDecisionToDb, generateDecisionsMd } from '../db-writer.ts'; -import { createTestContext } from './test-helpers.ts'; - -const { assertEq, assertTrue, assertMatch, report } = createTestContext(); +import { describe, test, beforeEach, afterEach } from 'node:test'; +import assert from 'node:assert/strict'; // ─── Fixture Generators (duplicated from token-savings.test.ts — file-scoped) ── @@ -119,10 +118,7 @@ const ROADMAP_CONTENT = `# M001: Test Milestone\n\n**Vision:** Integration test // Full Lifecycle Integration Test // ═══════════════════════════════════════════════════════════════════════════ -async function main(): Promise { - - console.log('\n=== integration-lifecycle: full pipeline ==='); - { +test('integration-lifecycle: full pipeline', async () => { // ── Step 1: Set up temp dir with realistic .gsd/ structure ────────── const base = mkdtempSync(join(tmpdir(), 'gsd-int-lifecycle-')); const gsdDir = join(base, '.gsd'); @@ -142,37 +138,37 @@ async function main(): Promise { try { // ── Step 2: Open file-backed DB + migrateFromMarkdown ────────────── openDatabase(dbPath); - assertTrue(isDbAvailable(), 'lifecycle: DB is available after open'); + assert.ok(isDbAvailable(), 'lifecycle: DB is available after open'); const result = migrateFromMarkdown(base); - assertTrue(result.decisions === DECISIONS_COUNT, `lifecycle: imported ${result.decisions} decisions, expected ${DECISIONS_COUNT}`); - assertTrue(result.requirements === REQUIREMENTS_COUNT, `lifecycle: imported ${result.requirements} requirements, expected ${REQUIREMENTS_COUNT}`); - assertTrue(result.artifacts >= 1, `lifecycle: imported at least 1 artifact (got ${result.artifacts})`); + assert.ok(result.decisions === DECISIONS_COUNT, `lifecycle: imported ${result.decisions} decisions, expected ${DECISIONS_COUNT}`); + assert.ok(result.requirements === REQUIREMENTS_COUNT, `lifecycle: imported ${result.requirements} requirements, expected ${REQUIREMENTS_COUNT}`); + assert.ok(result.artifacts >= 1, `lifecycle: imported at least 1 artifact (got ${result.artifacts})`); // Verify file-backed DB uses WAL const adapter = _getAdapter()!; const mode = adapter.prepare('PRAGMA journal_mode').get(); - assertEq(mode?.['journal_mode'], 'wal', 'lifecycle: file-backed DB uses WAL mode'); + assert.deepStrictEqual(mode?.['journal_mode'], 'wal', 'lifecycle: file-backed DB uses WAL mode'); // ── Step 3: Scoped queries — decisions by milestone ──────────────── const allDecisions = queryDecisions(); const m001Decisions = queryDecisions({ milestoneId: 'M001' }); const m002Decisions = queryDecisions({ milestoneId: 'M002' }); - assertTrue(allDecisions.length === DECISIONS_COUNT, `lifecycle: all decisions count = ${DECISIONS_COUNT} (got ${allDecisions.length})`); - assertTrue(m001Decisions.length > 0, 'lifecycle: M001 decisions non-empty'); - assertTrue(m002Decisions.length > 0, 'lifecycle: M002 decisions non-empty'); - assertTrue(m001Decisions.length < allDecisions.length, 'lifecycle: M001 filtered count < total count'); - assertTrue(m002Decisions.length < allDecisions.length, 'lifecycle: M002 filtered count < total count'); - assertEq(m001Decisions.length + m002Decisions.length, allDecisions.length, 'lifecycle: M001 + M002 = total decisions'); + assert.ok(allDecisions.length === DECISIONS_COUNT, `lifecycle: all decisions count = ${DECISIONS_COUNT} (got ${allDecisions.length})`); + assert.ok(m001Decisions.length > 0, 'lifecycle: M001 decisions non-empty'); + assert.ok(m002Decisions.length > 0, 'lifecycle: M002 decisions non-empty'); + assert.ok(m001Decisions.length < allDecisions.length, 'lifecycle: M001 filtered count < total count'); + assert.ok(m002Decisions.length < allDecisions.length, 'lifecycle: M002 filtered count < total count'); + assert.deepStrictEqual(m001Decisions.length + m002Decisions.length, allDecisions.length, 'lifecycle: M001 + M002 = total decisions'); // Verify scoping correctness for (const d of m001Decisions) { - assertTrue(d.when_context.includes('M001'), `lifecycle: M001 decision ${d.id} has M001 in when_context`); + assert.ok(d.when_context.includes('M001'), `lifecycle: M001 decision ${d.id} has M001 in when_context`); } for (const d of m002Decisions) { - assertTrue(d.when_context.includes('M002'), `lifecycle: M002 decision ${d.id} has M002 in when_context`); + assert.ok(d.when_context.includes('M002'), `lifecycle: M002 decision ${d.id} has M002 in when_context`); } // ── Step 4: Scoped queries — requirements by slice ───────────────── @@ -180,19 +176,19 @@ async function main(): Promise { const s01Requirements = queryRequirements({ sliceId: 'S01' }); const s04Requirements = queryRequirements({ sliceId: 'S04' }); - assertTrue(allRequirements.length === REQUIREMENTS_COUNT, `lifecycle: all requirements count = ${REQUIREMENTS_COUNT} (got ${allRequirements.length})`); - assertTrue(s01Requirements.length > 0, 'lifecycle: S01 requirements non-empty'); - assertTrue(s04Requirements.length > 0, 'lifecycle: S04 requirements non-empty'); - assertTrue(s01Requirements.length < allRequirements.length, 'lifecycle: S01 filtered count < total count'); + assert.ok(allRequirements.length === REQUIREMENTS_COUNT, `lifecycle: all requirements count = ${REQUIREMENTS_COUNT} (got ${allRequirements.length})`); + assert.ok(s01Requirements.length > 0, 'lifecycle: S01 requirements non-empty'); + assert.ok(s04Requirements.length > 0, 'lifecycle: S04 requirements non-empty'); + assert.ok(s01Requirements.length < allRequirements.length, 'lifecycle: S01 filtered count < total count'); // ── Step 5: Format + token savings validation ────────────────────── const formattedDecisions = formatDecisionsForPrompt(m001Decisions); const formattedRequirements = formatRequirementsForPrompt(s01Requirements); - assertTrue(formattedDecisions.length > 0, 'lifecycle: formatted M001 decisions non-empty'); - assertTrue(formattedRequirements.length > 0, 'lifecycle: formatted S01 requirements non-empty'); - assertMatch(formattedDecisions, /\| D/, 'lifecycle: formatted decisions contains decision rows'); - assertMatch(formattedRequirements, /### R\d+/, 'lifecycle: formatted requirements has headings'); + assert.ok(formattedDecisions.length > 0, 'lifecycle: formatted M001 decisions non-empty'); + assert.ok(formattedRequirements.length > 0, 'lifecycle: formatted S01 requirements non-empty'); + assert.match(formattedDecisions, /\| D/, 'lifecycle: formatted decisions contains decision rows'); + assert.match(formattedRequirements, /### R\d+/, 'lifecycle: formatted requirements has headings'); // Token savings: scoped output vs full file content const fullDecisionsContent = readFileSync(join(gsdDir, 'DECISIONS.md'), 'utf-8'); @@ -203,24 +199,24 @@ async function main(): Promise { console.log(` Token savings: ${savingsPercent.toFixed(1)}% (scoped: ${dbScopedTotal}, full: ${fullTotal})`); - assertTrue(dbScopedTotal > 0, 'lifecycle: scoped content non-empty'); - assertTrue(dbScopedTotal < fullTotal, 'lifecycle: scoped content smaller than full content'); - assertTrue(savingsPercent >= 30, `lifecycle: savings ≥30% (actual: ${savingsPercent.toFixed(1)}%)`); + assert.ok(dbScopedTotal > 0, 'lifecycle: scoped content non-empty'); + assert.ok(dbScopedTotal < fullTotal, 'lifecycle: scoped content smaller than full content'); + assert.ok(savingsPercent >= 30, `lifecycle: savings ≥30% (actual: ${savingsPercent.toFixed(1)}%)`); // ── Step 6: Simulate content change → re-import ──────────────────── const newDecisionRow = `| D${DECISIONS_COUNT + 1} | M001/S01 | testing | new decision added after initial import | choice X | rationale Y | yes |`; appendFileSync(join(gsdDir, 'DECISIONS.md'), '\n' + newDecisionRow + '\n'); const result2 = migrateFromMarkdown(base); - assertTrue(result2.decisions === DECISIONS_COUNT + 1, `lifecycle: re-import got ${result2.decisions} decisions, expected ${DECISIONS_COUNT + 1}`); + assert.ok(result2.decisions === DECISIONS_COUNT + 1, `lifecycle: re-import got ${result2.decisions} decisions, expected ${DECISIONS_COUNT + 1}`); const afterReimport = queryDecisions(); - assertTrue(afterReimport.length === DECISIONS_COUNT + 1, `lifecycle: DB has ${DECISIONS_COUNT + 1} decisions after re-import (got ${afterReimport.length})`); + assert.ok(afterReimport.length === DECISIONS_COUNT + 1, `lifecycle: DB has ${DECISIONS_COUNT + 1} decisions after re-import (got ${afterReimport.length})`); // Verify the new decision is queryable const newM001 = queryDecisions({ milestoneId: 'M001' }); const foundNew = newM001.some(d => d.id === `D${DECISIONS_COUNT + 1}`); - assertTrue(foundNew, `lifecycle: newly imported D${DECISIONS_COUNT + 1} found in M001 scope`); + assert.ok(foundNew, `lifecycle: newly imported D${DECISIONS_COUNT + 1} found in M001 scope`); // ── Step 7: saveDecisionToDb write-back + round-trip ─────────────── const saved = await saveDecisionToDb( @@ -234,44 +230,37 @@ async function main(): Promise { base, ); - assertTrue(typeof saved.id === 'string', 'lifecycle: saveDecisionToDb returned an id'); - assertMatch(saved.id, /^D\d+$/, 'lifecycle: saved ID matches D### pattern'); + assert.ok(typeof saved.id === 'string', 'lifecycle: saveDecisionToDb returned an id'); + assert.match(saved.id, /^D\d+$/, 'lifecycle: saved ID matches D### pattern'); // Query back from DB const allAfterSave = queryDecisions(); const savedDecision = allAfterSave.find(d => d.id === saved.id); - assertTrue(savedDecision !== null && savedDecision !== undefined, `lifecycle: saved decision ${saved.id} found in DB`); - assertEq(savedDecision?.decision, 'integration test write-back decision', 'lifecycle: saved decision text matches'); - assertEq(savedDecision?.choice, 'option Z', 'lifecycle: saved choice matches'); + assert.ok(savedDecision !== null && savedDecision !== undefined, `lifecycle: saved decision ${saved.id} found in DB`); + assert.deepStrictEqual(savedDecision?.decision, 'integration test write-back decision', 'lifecycle: saved decision text matches'); + assert.deepStrictEqual(savedDecision?.choice, 'option Z', 'lifecycle: saved choice matches'); // Verify DECISIONS.md was regenerated with the new decision const regeneratedMd = readFileSync(join(gsdDir, 'DECISIONS.md'), 'utf-8'); - assertTrue(regeneratedMd.includes(saved.id), `lifecycle: regenerated DECISIONS.md contains ${saved.id}`); - assertTrue(regeneratedMd.includes('integration test write-back decision'), 'lifecycle: regenerated md contains write-back text'); + assert.ok(regeneratedMd.includes(saved.id), `lifecycle: regenerated DECISIONS.md contains ${saved.id}`); + assert.ok(regeneratedMd.includes('integration test write-back decision'), 'lifecycle: regenerated md contains write-back text'); // Round-trip: parse regenerated markdown back → verify field fidelity const reparsed = parseDecisionsTable(regeneratedMd); const reparsedSaved = reparsed.find(d => d.id === saved.id); - assertTrue(reparsedSaved !== undefined, `lifecycle: reparsed markdown contains ${saved.id}`); - assertEq(reparsedSaved?.choice, 'option Z', 'lifecycle: round-trip choice preserved'); - assertEq(reparsedSaved?.rationale, 'proves round-trip fidelity', 'lifecycle: round-trip rationale preserved'); + assert.ok(reparsedSaved !== undefined, `lifecycle: reparsed markdown contains ${saved.id}`); + assert.deepStrictEqual(reparsedSaved?.choice, 'option Z', 'lifecycle: round-trip choice preserved'); + assert.deepStrictEqual(reparsedSaved?.rationale, 'proves round-trip fidelity', 'lifecycle: round-trip rationale preserved'); // ── Step 8: DB consistency — total count sanity ───────────────────── const finalCount = queryDecisions().length; // Original 14 + 1 re-import + 1 saveDecisionToDb = 16 - assertTrue(finalCount === DECISIONS_COUNT + 2, `lifecycle: final DB count = ${DECISIONS_COUNT + 2} (got ${finalCount})`); + assert.ok(finalCount === DECISIONS_COUNT + 2, `lifecycle: final DB count = ${DECISIONS_COUNT + 2} (got ${finalCount})`); closeDatabase(); } finally { closeDatabase(); rmSync(base, { recursive: true, force: true }); } - } - - report(); -} - -main().catch((error) => { - console.error(error); - process.exit(1); }); + diff --git a/src/resources/extensions/gsd/tests/integration-mixed-milestones.test.ts b/src/resources/extensions/gsd/tests/integration-mixed-milestones.test.ts index b5e2e8de1..94d2d76b6 100644 --- a/src/resources/extensions/gsd/tests/integration-mixed-milestones.test.ts +++ b/src/resources/extensions/gsd/tests/integration-mixed-milestones.test.ts @@ -20,11 +20,11 @@ import { parseSliceBranch, } from '../worktree.ts'; import { clearPathCache } from '../paths.ts'; -import { createTestContext } from './test-helpers.ts'; +import { describe, test, beforeEach, afterEach } from 'node:test'; +import assert from 'node:assert/strict'; // ─── Assertion Helpers ──────────────────────────────────────────────────── -const { assertEq, assertTrue, assertMatch, report } = createTestContext(); // ─── Fixture Helpers ────────────────────────────────────────────────────── function createFixtureBase(): string { @@ -79,11 +79,9 @@ function createGitRepo(): string { // Test Groups // ═══════════════════════════════════════════════════════════════════════════ -async function main(): Promise { - // ─── Group 1: deriveState with new-format-only milestones ───────────── - console.log('\n=== Group 1: deriveState with new-format-only milestones ==='); - { + +test('Group 1: deriveState with new-format-only milestones', async () => { const base = createFixtureBase(); try { // Create M001-abc123 with roadmap + 2 slices (S01 complete, S02 in-progress) @@ -125,32 +123,32 @@ async function main(): Promise { const state = await deriveState(base); // Phase should be executing (active milestone with incomplete slice + plan + tasks) - assertEq(state.phase, 'executing', 'G1: phase is executing'); - assertTrue(state.activeMilestone !== null, 'G1: activeMilestone is not null'); - assertEq(state.activeMilestone?.id, 'M001-abc123', 'G1: activeMilestone id is M001-abc123'); - assertEq(state.activeMilestone?.title, 'Test Feature', 'G1: title stripped to Test Feature'); + assert.deepStrictEqual(state.phase, 'executing', 'G1: phase is executing'); + assert.ok(state.activeMilestone !== null, 'G1: activeMilestone is not null'); + assert.deepStrictEqual(state.activeMilestone?.id, 'M001-abc123', 'G1: activeMilestone id is M001-abc123'); + assert.deepStrictEqual(state.activeMilestone?.title, 'Test Feature', 'G1: title stripped to Test Feature'); // Registry - assertEq(state.registry.length, 1, 'G1: registry has 1 entry'); - assertEq(state.registry[0]?.id, 'M001-abc123', 'G1: registry entry id'); - assertEq(state.registry[0]?.status, 'active', 'G1: registry entry status is active'); - assertEq(state.registry[0]?.title, 'Test Feature', 'G1: registry title stripped'); + assert.deepStrictEqual(state.registry.length, 1, 'G1: registry has 1 entry'); + assert.deepStrictEqual(state.registry[0]?.id, 'M001-abc123', 'G1: registry entry id'); + assert.deepStrictEqual(state.registry[0]?.status, 'active', 'G1: registry entry status is active'); + assert.deepStrictEqual(state.registry[0]?.title, 'Test Feature', 'G1: registry title stripped'); // Active slice - assertTrue(state.activeSlice !== null, 'G1: activeSlice is not null'); - assertEq(state.activeSlice?.id, 'S02', 'G1: activeSlice is S02'); + assert.ok(state.activeSlice !== null, 'G1: activeSlice is not null'); + assert.deepStrictEqual(state.activeSlice?.id, 'S02', 'G1: activeSlice is S02'); // Progress - assertEq(state.progress?.milestones?.done, 0, 'G1: milestones done = 0'); - assertEq(state.progress?.milestones?.total, 1, 'G1: milestones total = 1'); + assert.deepStrictEqual(state.progress?.milestones?.done, 0, 'G1: milestones done = 0'); + assert.deepStrictEqual(state.progress?.milestones?.total, 1, 'G1: milestones total = 1'); } finally { cleanup(base); } - } +}); // ─── Group 2: deriveState with mixed-format milestones ──────────────── - console.log('\n=== Group 2: deriveState with mixed old+new format milestones ==='); - { + +test('Group 2: deriveState with mixed old+new format milestones', async () => { const base = createFixtureBase(); try { // M001 — complete milestone (all slices done + summary) @@ -217,40 +215,40 @@ Everything worked. const state = await deriveState(base); // Registry — should have 2 entries sorted by seq number - assertEq(state.registry.length, 2, 'G2: registry has 2 entries'); - assertEq(state.registry[0]?.id, 'M001', 'G2: registry[0] is M001 (sorted first)'); - assertEq(state.registry[1]?.id, 'M002-abc123', 'G2: registry[1] is M002-abc123 (sorted second)'); + assert.deepStrictEqual(state.registry.length, 2, 'G2: registry has 2 entries'); + assert.deepStrictEqual(state.registry[0]?.id, 'M001', 'G2: registry[0] is M001 (sorted first)'); + assert.deepStrictEqual(state.registry[1]?.id, 'M002-abc123', 'G2: registry[1] is M002-abc123 (sorted second)'); // M001 is complete - assertEq(state.registry[0]?.status, 'complete', 'G2: M001 status is complete'); - assertEq(state.registry[0]?.title, 'Legacy Feature', 'G2: M001 title stripped'); + assert.deepStrictEqual(state.registry[0]?.status, 'complete', 'G2: M001 status is complete'); + assert.deepStrictEqual(state.registry[0]?.title, 'Legacy Feature', 'G2: M001 title stripped'); // M002-abc123 is active - assertEq(state.registry[1]?.status, 'active', 'G2: M002-abc123 status is active'); - assertEq(state.registry[1]?.title, 'New Feature', 'G2: M002-abc123 title stripped'); + assert.deepStrictEqual(state.registry[1]?.status, 'active', 'G2: M002-abc123 status is active'); + assert.deepStrictEqual(state.registry[1]?.title, 'New Feature', 'G2: M002-abc123 title stripped'); // Active milestone - assertTrue(state.activeMilestone !== null, 'G2: activeMilestone is not null'); - assertEq(state.activeMilestone?.id, 'M002-abc123', 'G2: activeMilestone is M002-abc123'); - assertEq(state.activeMilestone?.title, 'New Feature', 'G2: activeMilestone title stripped'); + assert.ok(state.activeMilestone !== null, 'G2: activeMilestone is not null'); + assert.deepStrictEqual(state.activeMilestone?.id, 'M002-abc123', 'G2: activeMilestone is M002-abc123'); + assert.deepStrictEqual(state.activeMilestone?.title, 'New Feature', 'G2: activeMilestone title stripped'); // Phase - assertEq(state.phase, 'executing', 'G2: phase is executing'); + assert.deepStrictEqual(state.phase, 'executing', 'G2: phase is executing'); // Active slice - assertEq(state.activeSlice?.id, 'S02', 'G2: activeSlice is S02'); + assert.deepStrictEqual(state.activeSlice?.id, 'S02', 'G2: activeSlice is S02'); // Progress - assertEq(state.progress?.milestones?.done, 1, 'G2: milestones done = 1'); - assertEq(state.progress?.milestones?.total, 2, 'G2: milestones total = 2'); + assert.deepStrictEqual(state.progress?.milestones?.done, 1, 'G2: milestones done = 1'); + assert.deepStrictEqual(state.progress?.milestones?.total, 2, 'G2: milestones total = 2'); } finally { cleanup(base); } - } +}); // ─── Group 3: indexWorkspace with mixed-format milestones ───────────── - console.log('\n=== Group 3: indexWorkspace with mixed-format milestones ==='); - { + +test('Group 3: indexWorkspace with mixed-format milestones', async () => { const base = createFixtureBase(); try { // Same fixture as Group 2: M001 (complete) + M002-abc123 (active) @@ -304,39 +302,39 @@ Everything worked. const index = await indexWorkspace(base); // Both milestones indexed - assertEq(index.milestones.length, 2, 'G3: 2 milestones in index'); - assertEq(index.milestones[0]?.id, 'M001', 'G3: index[0] is M001'); - assertEq(index.milestones[1]?.id, 'M002-abc123', 'G3: index[1] is M002-abc123'); + assert.deepStrictEqual(index.milestones.length, 2, 'G3: 2 milestones in index'); + assert.deepStrictEqual(index.milestones[0]?.id, 'M001', 'G3: index[0] is M001'); + assert.deepStrictEqual(index.milestones[1]?.id, 'M002-abc123', 'G3: index[1] is M002-abc123'); // Titles stripped from both formats - assertEq(index.milestones[0]?.title, 'Legacy Feature', 'G3: M001 title stripped'); - assertEq(index.milestones[1]?.title, 'New Feature', 'G3: M002-abc123 title stripped'); + assert.deepStrictEqual(index.milestones[0]?.title, 'Legacy Feature', 'G3: M001 title stripped'); + assert.deepStrictEqual(index.milestones[1]?.title, 'New Feature', 'G3: M002-abc123 title stripped'); // Active state - assertEq(index.active.milestoneId, 'M002-abc123', 'G3: active milestone is M002-abc123'); - assertEq(index.active.sliceId, 'S01', 'G3: active slice is S01'); + assert.deepStrictEqual(index.active.milestoneId, 'M002-abc123', 'G3: active milestone is M002-abc123'); + assert.deepStrictEqual(index.active.sliceId, 'S01', 'G3: active slice is S01'); // Scopes include new-format paths - assertTrue( + assert.ok( index.scopes.some(s => s.scope === 'M002-abc123'), 'G3: scope includes M002-abc123 milestone', ); - assertTrue( + assert.ok( index.scopes.some(s => s.scope === 'M002-abc123/S01'), 'G3: scope includes M002-abc123/S01 slice', ); - assertTrue( + assert.ok( index.scopes.some(s => s.scope === 'M002-abc123/S01/T01'), 'G3: scope includes M002-abc123/S01/T01 task', ); } finally { cleanup(base); } - } +}); // ─── Group 4: inlinePriorMilestoneSummary with mixed formats ────────── - console.log('\n=== Group 4: inlinePriorMilestoneSummary with mixed formats ==='); - { + +test('Group 4: inlinePriorMilestoneSummary with mixed formats', async () => { const base = createFixtureBase(); try { // M001 — completed with summary @@ -358,21 +356,21 @@ Built the legacy feature successfully. const result = await inlinePriorMilestoneSummary('M002-abc123', base); // Result should be non-null (M001 is before M002-abc123) - assertTrue(result !== null, 'G4: result is non-null'); - assertTrue(typeof result === 'string', 'G4: result is a string'); + assert.ok(result !== null, 'G4: result is non-null'); + assert.ok(typeof result === 'string', 'G4: result is a string'); // Should contain the M001 summary content - assertTrue(result!.includes('Prior Milestone Summary'), 'G4: contains Prior Milestone Summary header'); - assertTrue(result!.includes('Built the legacy feature successfully'), 'G4: contains M001 summary content'); - assertTrue(result!.includes('Used old format for milestone IDs'), 'G4: contains M001 key decisions'); + assert.ok(result!.includes('Prior Milestone Summary'), 'G4: contains Prior Milestone Summary header'); + assert.ok(result!.includes('Built the legacy feature successfully'), 'G4: contains M001 summary content'); + assert.ok(result!.includes('Used old format for milestone IDs'), 'G4: contains M001 key decisions'); } finally { cleanup(base); } - } +}); // ─── Group 5: dispatch-guard with new-format milestones ────────────── - console.log('\n=== Group 5: dispatch-guard with new-format milestones ==='); - { + +test('Group 5: dispatch-guard with new-format milestones', () => { const base = createGitRepo(); try { // M001-abc123: all slices complete @@ -403,28 +401,28 @@ Built the legacy feature successfully. run('git commit -m init', base); // No blocker: M001-abc123 is complete, dispatching M002-abc123/S01 - assertEq( + assert.deepStrictEqual( getPriorSliceCompletionBlocker(base, 'main', 'plan-slice', 'M002-abc123/S01'), null, 'G5: no blocker for M002-abc123/S01 when M001-abc123 all complete', ); // No blocker for first slice of first milestone - assertEq( + assert.deepStrictEqual( getPriorSliceCompletionBlocker(base, 'main', 'execute-task', 'M001-abc123/S01/T01'), null, 'G5: no blocker for M001-abc123/S01/T01 (first milestone first slice)', ); // Blocker: trying to dispatch M002-abc123/S02 when S01 is incomplete - assertMatch( + assert.match( getPriorSliceCompletionBlocker(base, 'main', 'execute-task', 'M002-abc123/S02/T01') ?? '', /M002-abc123\/S01 is not complete/, 'G5: blocks M002-abc123/S02 when S01 incomplete', ); // Non-slice dispatch type should not be blocked - assertEq( + assert.deepStrictEqual( getPriorSliceCompletionBlocker(base, 'main', 'plan-milestone', 'M002-abc123'), null, 'G5: non-slice dispatch type not blocked', @@ -447,7 +445,7 @@ Built the legacy feature successfully. // M001 (seq=1) < M001-abc123 (seq=1) — but M001 has incomplete S02 // Since M001 seq=1 and M002-abc123 seq=2, blocker should reference M001/S02 - assertMatch( + assert.match( getPriorSliceCompletionBlocker(base, 'main', 'plan-slice', 'M002-abc123/S01') ?? '', /earlier slice M001\/S02 is not complete/, 'G5: mixed-format blocker references M001/S02', @@ -468,7 +466,7 @@ Built the legacy feature successfully. run('git commit -m complete-m001', base); clearPathCache(); - assertEq( + assert.deepStrictEqual( getPriorSliceCompletionBlocker(base, 'main', 'plan-slice', 'M002-abc123/S01'), null, 'G5: no blocker after M001 completed (mixed format)', @@ -476,7 +474,7 @@ Built the legacy feature successfully. // M001-abc123 still has all complete, M002-abc123/S01 still incomplete // Check that S02 of M002-abc123 is still blocked by its own S01 - assertMatch( + assert.match( getPriorSliceCompletionBlocker(base, 'main', 'execute-task', 'M002-abc123/S02/T01') ?? '', /M002-abc123\/S01 is not complete/, 'G5: intra-milestone blocker still works in mixed-format context', @@ -508,7 +506,7 @@ Built the legacy feature successfully. run('git commit -m add-m003', base); clearPathCache(); - assertMatch( + assert.match( getPriorSliceCompletionBlocker(base, 'main', 'execute-task', 'M003-xyz789/S02/T01') ?? '', /earlier slice M003-xyz789\/S01 is not complete/, 'G5: positional path produces "earlier slice" message with new-format milestone ID', @@ -516,13 +514,13 @@ Built the legacy feature successfully. } finally { cleanup(base); } - } +}); // ─── Group 6: Branch name helpers with new-format IDs ─────────────── - console.log('\n=== Group 6: Branch name helpers with new-format IDs ==='); - { + +test('Group 6: Branch name helpers with new-format IDs', () => { // Test getSliceBranchName with new-format ID - assertEq( + assert.deepStrictEqual( getSliceBranchName('M001-abc123', 'S01'), 'gsd/M001-abc123/S01', 'G6: getSliceBranchName returns gsd/M001-abc123/S01', @@ -530,26 +528,12 @@ Built the legacy feature successfully. // Test parseSliceBranch with new-format branch name const parsed = parseSliceBranch('gsd/M001-abc123/S01'); - assertTrue(parsed !== null, 'G6: parseSliceBranch returns non-null for new-format'); - assertEq(parsed?.milestoneId, 'M001-abc123', 'G6: parsed milestoneId is M001-abc123'); - assertEq(parsed?.sliceId, 'S01', 'G6: parsed sliceId is S01'); - assertEq(parsed?.worktreeName, null, 'G6: parsed worktreeName is null (no worktree)'); - } + assert.ok(parsed !== null, 'G6: parseSliceBranch returns non-null for new-format'); + assert.deepStrictEqual(parsed?.milestoneId, 'M001-abc123', 'G6: parsed milestoneId is M001-abc123'); + assert.deepStrictEqual(parsed?.sliceId, 'S01', 'G6: parsed sliceId is S01'); + assert.deepStrictEqual(parsed?.worktreeName, null, 'G6: parsed worktreeName is null (no worktree)'); +}); // ─── Summary ────────────────────────────────────────────────────────── - 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('integration-mixed-milestones: all groups pass', async () => { - await main(); - }); -} else { - main().catch((error) => { - console.error(error); - process.exit(1); - }); -} +// When run via vitest, wrap in test(); when run via tsx, call directly. \ No newline at end of file diff --git a/src/resources/extensions/gsd/tests/markdown-renderer.test.ts b/src/resources/extensions/gsd/tests/markdown-renderer.test.ts index 35551f06d..83f47c49a 100644 --- a/src/resources/extensions/gsd/tests/markdown-renderer.test.ts +++ b/src/resources/extensions/gsd/tests/markdown-renderer.test.ts @@ -1,4 +1,3 @@ -import { createTestContext } from './test-helpers.ts'; import * as path from 'node:path'; import * as os from 'node:os'; import * as fs from 'node:fs'; @@ -38,8 +37,8 @@ import { } from '../files.ts'; import { clearPathCache, _clearGsdRootCache } from '../paths.ts'; import { invalidateStateCache } from '../state.ts'; - -const { assertEq, assertTrue, assertMatch, report } = createTestContext(); +import { describe, test, beforeEach, afterEach } from 'node:test'; +import assert from 'node:assert/strict'; // ═══════════════════════════════════════════════════════════════════════════ // Helpers @@ -174,29 +173,27 @@ function makeTaskSummaryContent(taskId: string): string { // DB Accessor Tests // ═══════════════════════════════════════════════════════════════════════════ -console.log('\n── markdown-renderer: DB accessor basics ──'); - -{ +test('── markdown-renderer: DB accessor basics ──', () => { openDatabase(':memory:'); // getAllMilestones — empty const empty = getAllMilestones(); - assertEq(empty.length, 0, 'getAllMilestones returns empty when no milestones'); + assert.deepStrictEqual(empty.length, 0, 'getAllMilestones returns empty when no milestones'); // Insert and retrieve insertMilestone({ id: 'M001', title: 'Test MS', status: 'active' }); insertMilestone({ id: 'M002', title: 'Second MS', status: 'active' }); const all = getAllMilestones(); - assertEq(all.length, 2, 'getAllMilestones returns 2 milestones'); - assertEq(all[0].id, 'M001', 'first milestone is M001'); - assertEq(all[1].id, 'M002', 'second milestone is M002'); - assertEq(all[0].title, 'Test MS', 'milestone title correct'); - assertEq(all[0].status, 'active', 'milestone status correct'); + assert.deepStrictEqual(all.length, 2, 'getAllMilestones returns 2 milestones'); + assert.deepStrictEqual(all[0].id, 'M001', 'first milestone is M001'); + assert.deepStrictEqual(all[1].id, 'M002', 'second milestone is M002'); + assert.deepStrictEqual(all[0].title, 'Test MS', 'milestone title correct'); + assert.deepStrictEqual(all[0].status, 'active', 'milestone status correct'); // getMilestoneSlices — empty const noSlices = getMilestoneSlices('M001'); - assertEq(noSlices.length, 0, 'getMilestoneSlices returns empty when no slices'); + assert.deepStrictEqual(noSlices.length, 0, 'getMilestoneSlices returns empty when no slices'); // Insert slices and retrieve insertSlice({ id: 'S01', milestoneId: 'M001', title: 'Slice 1', status: 'complete' }); @@ -204,26 +201,24 @@ console.log('\n── markdown-renderer: DB accessor basics ──'); insertSlice({ id: 'S01', milestoneId: 'M002', title: 'M2 Slice', status: 'pending' }); const m1Slices = getMilestoneSlices('M001'); - assertEq(m1Slices.length, 2, 'M001 has 2 slices'); - assertEq(m1Slices[0].id, 'S01', 'first slice is S01'); - assertEq(m1Slices[0].status, 'complete', 'S01 status is complete'); - assertEq(m1Slices[1].id, 'S02', 'second slice is S02'); - assertEq(m1Slices[1].status, 'pending', 'S02 status is pending'); + assert.deepStrictEqual(m1Slices.length, 2, 'M001 has 2 slices'); + assert.deepStrictEqual(m1Slices[0].id, 'S01', 'first slice is S01'); + assert.deepStrictEqual(m1Slices[0].status, 'complete', 'S01 status is complete'); + assert.deepStrictEqual(m1Slices[1].id, 'S02', 'second slice is S02'); + assert.deepStrictEqual(m1Slices[1].status, 'pending', 'S02 status is pending'); const m2Slices = getMilestoneSlices('M002'); - assertEq(m2Slices.length, 1, 'M002 has 1 slice'); + assert.deepStrictEqual(m2Slices.length, 1, 'M002 has 1 slice'); closeDatabase(); -} +}); -console.log('\n── markdown-renderer: getArtifact accessor ──'); - -{ +test('── markdown-renderer: getArtifact accessor ──', () => { openDatabase(':memory:'); // Not found const missing = getArtifact('nonexistent/path'); - assertEq(missing, null, 'getArtifact returns null for missing path'); + assert.deepStrictEqual(missing, null, 'getArtifact returns null for missing path'); // Insert and retrieve insertArtifact({ @@ -236,21 +231,19 @@ console.log('\n── markdown-renderer: getArtifact accessor ──'); }); const found = getArtifact('milestones/M001/M001-ROADMAP.md'); - assertTrue(found !== null, 'getArtifact returns non-null for existing path'); - assertEq(found!.artifact_type, 'ROADMAP', 'artifact type correct'); - assertEq(found!.milestone_id, 'M001', 'milestone_id correct'); - assertEq(found!.full_content, '# Roadmap content', 'content correct'); + assert.ok(found !== null, 'getArtifact returns non-null for existing path'); + assert.deepStrictEqual(found!.artifact_type, 'ROADMAP', 'artifact type correct'); + assert.deepStrictEqual(found!.milestone_id, 'M001', 'milestone_id correct'); + assert.deepStrictEqual(found!.full_content, '# Roadmap content', 'content correct'); closeDatabase(); -} +}); // ═══════════════════════════════════════════════════════════════════════════ // Roadmap Checkbox Round-Trip // ═══════════════════════════════════════════════════════════════════════════ -console.log('\n── markdown-renderer: renderRoadmapCheckboxes round-trip ──'); - -{ +test('── markdown-renderer: renderRoadmapCheckboxes round-trip ──', async () => { const tmpDir = makeTmpDir(); const dbPath = path.join(tmpDir, '.gsd', 'gsd.db'); openDatabase(dbPath); @@ -275,36 +268,34 @@ console.log('\n── markdown-renderer: renderRoadmapCheckboxes round-trip ─ // Render — should set S01 [x] and leave S02 [ ] const ok = await renderRoadmapCheckboxes(tmpDir, 'M001'); - assertTrue(ok, 'renderRoadmapCheckboxes returns true'); + assert.ok(ok, 'renderRoadmapCheckboxes returns true'); // Read rendered file and parse const rendered = fs.readFileSync(roadmapPath, 'utf-8'); clearAllCaches(); const parsed = parseRoadmap(rendered); - assertEq(parsed.slices.length, 2, 'roadmap has 2 slices after render'); + assert.deepStrictEqual(parsed.slices.length, 2, 'roadmap has 2 slices after render'); const s01 = parsed.slices.find(s => s.id === 'S01'); const s02 = parsed.slices.find(s => s.id === 'S02'); - assertTrue(!!s01, 'S01 found in parsed roadmap'); - assertTrue(!!s02, 'S02 found in parsed roadmap'); - assertTrue(s01!.done, 'S01 is checked (done) after render'); - assertTrue(!s02!.done, 'S02 is unchecked (pending) after render'); + assert.ok(!!s01, 'S01 found in parsed roadmap'); + assert.ok(!!s02, 'S02 found in parsed roadmap'); + assert.ok(s01!.done, 'S01 is checked (done) after render'); + assert.ok(!s02!.done, 'S02 is unchecked (pending) after render'); // Verify artifact stored in DB const artifact = getArtifact('milestones/M001/M001-ROADMAP.md'); - assertTrue(artifact !== null, 'roadmap artifact stored in DB after render'); - assertTrue(artifact!.full_content.includes('[x] **S01:'), 'DB artifact has S01 checked'); - assertTrue(artifact!.full_content.includes('[ ] **S02:'), 'DB artifact has S02 unchecked'); + assert.ok(artifact !== null, 'roadmap artifact stored in DB after render'); + assert.ok(artifact!.full_content.includes('[x] **S01:'), 'DB artifact has S01 checked'); + assert.ok(artifact!.full_content.includes('[ ] **S02:'), 'DB artifact has S02 unchecked'); } finally { closeDatabase(); cleanupDir(tmpDir); } -} +}); -console.log('\n── markdown-renderer: renderRoadmapCheckboxes bidirectional ──'); - -{ +test('── markdown-renderer: renderRoadmapCheckboxes bidirectional ──', async () => { const tmpDir = makeTmpDir(); const dbPath = path.join(tmpDir, '.gsd', 'gsd.db'); openDatabase(dbPath); @@ -328,7 +319,7 @@ console.log('\n── markdown-renderer: renderRoadmapCheckboxes bidirectional clearAllCaches(); const ok = await renderRoadmapCheckboxes(tmpDir, 'M001'); - assertTrue(ok, 'bidirectional render returns true'); + assert.ok(ok, 'bidirectional render returns true'); const rendered = fs.readFileSync(roadmapPath, 'utf-8'); clearAllCaches(); @@ -336,21 +327,19 @@ console.log('\n── markdown-renderer: renderRoadmapCheckboxes bidirectional const s01 = parsed.slices.find(s => s.id === 'S01'); const s02 = parsed.slices.find(s => s.id === 'S02'); - assertTrue(!s01!.done, 'S01 unchecked (DB says pending, was checked on disk)'); - assertTrue(s02!.done, 'S02 checked (DB says complete, was unchecked on disk)'); + assert.ok(!s01!.done, 'S01 unchecked (DB says pending, was checked on disk)'); + assert.ok(s02!.done, 'S02 checked (DB says complete, was unchecked on disk)'); } finally { closeDatabase(); cleanupDir(tmpDir); } -} +}); // ═══════════════════════════════════════════════════════════════════════════ // Plan Checkbox Round-Trip // ═══════════════════════════════════════════════════════════════════════════ -console.log('\n── markdown-renderer: renderPlanCheckboxes round-trip ──'); - -{ +test('── markdown-renderer: renderPlanCheckboxes round-trip ──', async () => { const tmpDir = makeTmpDir(); const dbPath = path.join(tmpDir, '.gsd', 'gsd.db'); openDatabase(dbPath); @@ -376,29 +365,27 @@ console.log('\n── markdown-renderer: renderPlanCheckboxes round-trip ──' clearAllCaches(); const ok = await renderPlanCheckboxes(tmpDir, 'M001', 'S01'); - assertTrue(ok, 'renderPlanCheckboxes returns true'); + assert.ok(ok, 'renderPlanCheckboxes returns true'); const rendered = fs.readFileSync(planPath, 'utf-8'); clearAllCaches(); const parsed = parsePlan(rendered); - assertEq(parsed.tasks.length, 3, 'plan has 3 tasks after render'); + assert.deepStrictEqual(parsed.tasks.length, 3, 'plan has 3 tasks after render'); const t01 = parsed.tasks.find(t => t.id === 'T01'); const t02 = parsed.tasks.find(t => t.id === 'T02'); const t03 = parsed.tasks.find(t => t.id === 'T03'); - assertTrue(t01!.done, 'T01 checked (done in DB)'); - assertTrue(t02!.done, 'T02 checked (done in DB)'); - assertTrue(!t03!.done, 'T03 unchecked (pending in DB)'); + assert.ok(t01!.done, 'T01 checked (done in DB)'); + assert.ok(t02!.done, 'T02 checked (done in DB)'); + assert.ok(!t03!.done, 'T03 unchecked (pending in DB)'); } finally { closeDatabase(); cleanupDir(tmpDir); } -} +}); -console.log('\n── markdown-renderer: renderPlanCheckboxes bidirectional ──'); - -{ +test('── markdown-renderer: renderPlanCheckboxes bidirectional ──', async () => { const tmpDir = makeTmpDir(); const dbPath = path.join(tmpDir, '.gsd', 'gsd.db'); openDatabase(dbPath); @@ -422,7 +409,7 @@ console.log('\n── markdown-renderer: renderPlanCheckboxes bidirectional ─ clearAllCaches(); const ok = await renderPlanCheckboxes(tmpDir, 'M001', 'S01'); - assertTrue(ok, 'bidirectional plan render returns true'); + assert.ok(ok, 'bidirectional plan render returns true'); const rendered = fs.readFileSync(planPath, 'utf-8'); clearAllCaches(); @@ -430,17 +417,15 @@ console.log('\n── markdown-renderer: renderPlanCheckboxes bidirectional ─ const t01 = parsed.tasks.find(t => t.id === 'T01'); const t02 = parsed.tasks.find(t => t.id === 'T02'); - assertTrue(!t01!.done, 'T01 unchecked (DB says pending, was checked)'); - assertTrue(t02!.done, 'T02 checked (DB says done, was unchecked)'); + assert.ok(!t01!.done, 'T01 unchecked (DB says pending, was checked)'); + assert.ok(t02!.done, 'T02 checked (DB says done, was unchecked)'); } finally { closeDatabase(); cleanupDir(tmpDir); } -} +}); -console.log('\n── markdown-renderer: renderPlanFromDb creates parse-compatible slice plan + task plan files ──'); - -{ +test('── markdown-renderer: renderPlanFromDb creates parse-compatible slice plan + task plan files ──', async () => { const tmpDir = makeTmpDir(); const dbPath = path.join(tmpDir, '.gsd', 'gsd.db'); openDatabase(dbPath); @@ -498,50 +483,48 @@ console.log('\n── markdown-renderer: renderPlanFromDb creates parse-compatib }); const rendered = await renderPlanFromDb(tmpDir, 'M001', 'S02'); - assertTrue(fs.existsSync(rendered.planPath), 'slice plan written to disk'); - assertEq(rendered.taskPlanPaths.length, 2, 'task plan paths returned for each task'); - assertTrue(rendered.taskPlanPaths.every((p) => fs.existsSync(p)), 'all task plan files written to disk'); + assert.ok(fs.existsSync(rendered.planPath), 'slice plan written to disk'); + assert.strictEqual(rendered.taskPlanPaths.length, 2, 'task plan paths returned for each task'); + assert.ok(rendered.taskPlanPaths.every((p) => fs.existsSync(p)), 'all task plan files written to disk'); const planContent = fs.readFileSync(rendered.planPath, 'utf-8'); clearAllCaches(); const parsedPlan = parsePlan(planContent); - assertEq(parsedPlan.id, 'S02', 'rendered slice plan parses with correct slice id'); - assertEq(parsedPlan.goal, 'Render slice plans from DB state.', 'rendered slice plan preserves goal'); - assertEq(parsedPlan.demo, 'Rendered plans exist on disk.', 'rendered slice plan preserves demo'); - assertEq(parsedPlan.mustHaves.length, 2, 'rendered slice plan exposes must-haves'); - assertEq(parsedPlan.tasks.length, 2, 'rendered slice plan exposes all tasks'); - assertEq(parsedPlan.tasks[0].id, 'T01', 'first task parses correctly'); - assertTrue(parsedPlan.tasks[0].description.includes('DB-backed slice plan renderer'), 'task description preserved in slice plan'); - assertEq(parsedPlan.tasks[0].files?.[0], 'src/resources/extensions/gsd/markdown-renderer.ts', 'files list preserved in slice plan'); - assertEq(parsedPlan.tasks[0].verify, 'node --test markdown-renderer.test.ts', 'verify line preserved in slice plan'); + assert.strictEqual(parsedPlan.id, 'S02', 'rendered slice plan parses with correct slice id'); + assert.strictEqual(parsedPlan.goal, 'Render slice plans from DB state.', 'rendered slice plan preserves goal'); + assert.strictEqual(parsedPlan.demo, 'Rendered plans exist on disk.', 'rendered slice plan preserves demo'); + assert.strictEqual(parsedPlan.mustHaves.length, 2, 'rendered slice plan exposes must-haves'); + assert.strictEqual(parsedPlan.tasks.length, 2, 'rendered slice plan exposes all tasks'); + assert.strictEqual(parsedPlan.tasks[0].id, 'T01', 'first task parses correctly'); + assert.ok(parsedPlan.tasks[0].description.includes('DB-backed slice plan renderer'), 'task description preserved in slice plan'); + assert.strictEqual(parsedPlan.tasks[0].files?.[0], 'src/resources/extensions/gsd/markdown-renderer.ts', 'files list preserved in slice plan'); + assert.strictEqual(parsedPlan.tasks[0].verify, 'node --test markdown-renderer.test.ts', 'verify line preserved in slice plan'); const planArtifact = getArtifact('milestones/M001/slices/S02/S02-PLAN.md'); - assertTrue(planArtifact !== null, 'slice plan artifact stored in DB'); - assertTrue(planArtifact!.full_content.includes('## Tasks'), 'stored plan artifact contains task section'); + assert.ok(planArtifact !== null, 'slice plan artifact stored in DB'); + assert.ok(planArtifact!.full_content.includes('## Tasks'), 'stored plan artifact contains task section'); const taskPlanPath = path.join(tmpDir, '.gsd', 'milestones', 'M001', 'slices', 'S02', 'tasks', 'T01-PLAN.md'); const taskPlanContent = fs.readFileSync(taskPlanPath, 'utf-8'); const taskPlanFile = parseTaskPlanFile(taskPlanContent); - assertEq(taskPlanFile.frontmatter.estimated_steps, 1, 'task plan frontmatter exposes estimated_steps'); - assertEq(taskPlanFile.frontmatter.estimated_files, 1, 'task plan frontmatter exposes estimated_files'); - assertEq(taskPlanFile.frontmatter.skills_used.length, 0, 'task plan frontmatter uses conservative empty skills list'); - assertMatch(taskPlanContent, /^# T01: Render slice plan/m, 'task plan renders task heading'); - assertMatch(taskPlanContent, /^## Inputs$/m, 'task plan renders Inputs section'); - assertMatch(taskPlanContent, /^## Expected Output$/m, 'task plan renders Expected Output section'); - assertMatch(taskPlanContent, /^## Verification$/m, 'task plan renders Verification section'); + assert.strictEqual(taskPlanFile.frontmatter.estimated_steps, 1, 'task plan frontmatter exposes estimated_steps'); + assert.strictEqual(taskPlanFile.frontmatter.estimated_files, 1, 'task plan frontmatter exposes estimated_files'); + assert.strictEqual(taskPlanFile.frontmatter.skills_used.length, 0, 'task plan frontmatter uses conservative empty skills list'); + assert.match(taskPlanContent, /^# T01: Render slice plan/m, 'task plan renders task heading'); + assert.match(taskPlanContent, /^## Inputs$/m, 'task plan renders Inputs section'); + assert.match(taskPlanContent, /^## Expected Output$/m, 'task plan renders Expected Output section'); + assert.match(taskPlanContent, /^## Verification$/m, 'task plan renders Verification section'); const taskArtifact = getArtifact('milestones/M001/slices/S02/tasks/T01-PLAN.md'); - assertTrue(taskArtifact !== null, 'task plan artifact stored in DB'); - assertTrue(taskArtifact!.full_content.includes('skills_used: []'), 'stored task plan artifact preserves conservative skills_used'); + assert.ok(taskArtifact !== null, 'task plan artifact stored in DB'); + assert.ok(taskArtifact!.full_content.includes('skills_used: []'), 'stored task plan artifact preserves conservative skills_used'); } finally { closeDatabase(); cleanupDir(tmpDir); } -} +}); -console.log('\n── markdown-renderer: renderTaskPlanFromDb throws for missing task ──'); - -{ +test('── markdown-renderer: renderTaskPlanFromDb throws for missing task ──', async () => { const tmpDir = makeTmpDir(); const dbPath = path.join(tmpDir, '.gsd', 'gsd.db'); openDatabase(dbPath); @@ -557,23 +540,21 @@ console.log('\n── markdown-renderer: renderTaskPlanFromDb throws for missing await renderTaskPlanFromDb(tmpDir, 'M001', 'S02', 'T99'); } catch (error) { threw = true; - assertMatch(String((error as Error).message), /task M001\/S02\/T99 not found/, 'renderTaskPlanFromDb should fail clearly when task row is missing'); + assert.match(String((error as Error).message), /task M001\/S02\/T99 not found/, 'renderTaskPlanFromDb should fail clearly when task row is missing'); } - assertTrue(threw, 'renderTaskPlanFromDb throws when the task row is missing'); + assert.ok(threw, 'renderTaskPlanFromDb throws when the task row is missing'); } finally { closeDatabase(); cleanupDir(tmpDir); } -} +}); // ═══════════════════════════════════════════════════════════════════════════ // Task Summary Rendering // ═══════════════════════════════════════════════════════════════════════════ -console.log('\n── markdown-renderer: renderTaskSummary round-trip ──'); - -{ +test('── markdown-renderer: renderTaskSummary round-trip ──', async () => { const tmpDir = makeTmpDir(); const dbPath = path.join(tmpDir, '.gsd', 'gsd.db'); openDatabase(dbPath); @@ -596,33 +577,31 @@ console.log('\n── markdown-renderer: renderTaskSummary round-trip ──'); }); const ok = await renderTaskSummary(tmpDir, 'M001', 'S01', 'T01'); - assertTrue(ok, 'renderTaskSummary returns true'); + assert.ok(ok, 'renderTaskSummary returns true'); // Verify file exists on disk const summaryPath = path.join( tmpDir, '.gsd', 'milestones', 'M001', 'slices', 'S01', 'tasks', 'T01-SUMMARY.md', ); - assertTrue(fs.existsSync(summaryPath), 'T01-SUMMARY.md written to disk'); + assert.ok(fs.existsSync(summaryPath), 'T01-SUMMARY.md written to disk'); // Parse and verify const rendered = fs.readFileSync(summaryPath, 'utf-8'); clearAllCaches(); const parsed = parseSummary(rendered); - assertEq(parsed.frontmatter.id, 'T01', 'parsed summary has correct id'); - assertEq(parsed.frontmatter.parent, 'S01', 'parsed summary has correct parent'); - assertEq(parsed.frontmatter.milestone, 'M001', 'parsed summary has correct milestone'); - assertEq(parsed.frontmatter.duration, '45m', 'parsed summary has correct duration'); - assertTrue(parsed.title.includes('T01'), 'parsed summary title contains task ID'); - assertTrue(parsed.whatHappened.includes('Built the test feature'), 'whatHappened content preserved'); + assert.deepStrictEqual(parsed.frontmatter.id, 'T01', 'parsed summary has correct id'); + assert.deepStrictEqual(parsed.frontmatter.parent, 'S01', 'parsed summary has correct parent'); + assert.deepStrictEqual(parsed.frontmatter.milestone, 'M001', 'parsed summary has correct milestone'); + assert.deepStrictEqual(parsed.frontmatter.duration, '45m', 'parsed summary has correct duration'); + assert.ok(parsed.title.includes('T01'), 'parsed summary title contains task ID'); + assert.ok(parsed.whatHappened.includes('Built the test feature'), 'whatHappened content preserved'); } finally { closeDatabase(); cleanupDir(tmpDir); } -} +}); -console.log('\n── markdown-renderer: renderTaskSummary skips empty ──'); - -{ +test('── markdown-renderer: renderTaskSummary skips empty ──', async () => { const tmpDir = makeTmpDir(); const dbPath = path.join(tmpDir, '.gsd', 'gsd.db'); openDatabase(dbPath); @@ -643,20 +622,18 @@ console.log('\n── markdown-renderer: renderTaskSummary skips empty ──'); }); const ok = await renderTaskSummary(tmpDir, 'M001', 'S01', 'T01'); - assertTrue(!ok, 'renderTaskSummary returns false for empty summary'); + assert.ok(!ok, 'renderTaskSummary returns false for empty summary'); } finally { closeDatabase(); cleanupDir(tmpDir); } -} +}); // ═══════════════════════════════════════════════════════════════════════════ // Slice Summary Rendering // ═══════════════════════════════════════════════════════════════════════════ -console.log('\n── markdown-renderer: renderSliceSummary round-trip ──'); - -{ +test('── markdown-renderer: renderSliceSummary round-trip ──', async () => { const tmpDir = makeTmpDir(); const dbPath = path.join(tmpDir, '.gsd', 'gsd.db'); openDatabase(dbPath); @@ -680,38 +657,36 @@ console.log('\n── markdown-renderer: renderSliceSummary round-trip ──'); }); const ok = await renderSliceSummary(tmpDir, 'M001', 'S01'); - assertTrue(ok, 'renderSliceSummary returns true'); + assert.ok(ok, 'renderSliceSummary returns true'); // Verify SUMMARY file const summaryPath = path.join( tmpDir, '.gsd', 'milestones', 'M001', 'slices', 'S01', 'S01-SUMMARY.md', ); - assertTrue(fs.existsSync(summaryPath), 'S01-SUMMARY.md written to disk'); + assert.ok(fs.existsSync(summaryPath), 'S01-SUMMARY.md written to disk'); const summaryContent = fs.readFileSync(summaryPath, 'utf-8'); - assertTrue(summaryContent.includes('Test Slice Summary'), 'summary content correct'); + assert.ok(summaryContent.includes('Test Slice Summary'), 'summary content correct'); // Verify UAT file const uatPath = path.join( tmpDir, '.gsd', 'milestones', 'M001', 'slices', 'S01', 'S01-UAT.md', ); - assertTrue(fs.existsSync(uatPath), 'S01-UAT.md written to disk'); + assert.ok(fs.existsSync(uatPath), 'S01-UAT.md written to disk'); const uatContent = fs.readFileSync(uatPath, 'utf-8'); - assertTrue(uatContent.includes('artifact-driven'), 'UAT content correct'); + assert.ok(uatContent.includes('artifact-driven'), 'UAT content correct'); } finally { closeDatabase(); cleanupDir(tmpDir); } -} +}); // ═══════════════════════════════════════════════════════════════════════════ // renderAllFromDb // ═══════════════════════════════════════════════════════════════════════════ -console.log('\n── markdown-renderer: renderAllFromDb produces all files ──'); - -{ +test('── markdown-renderer: renderAllFromDb produces all files ──', async () => { const tmpDir = makeTmpDir(); const dbPath = path.join(tmpDir, '.gsd', 'gsd.db'); openDatabase(dbPath); @@ -779,8 +754,8 @@ console.log('\n── markdown-renderer: renderAllFromDb produces all files ─ const result = await renderAllFromDb(tmpDir); - assertTrue(result.rendered > 0, 'renderAllFromDb rendered some files'); - assertEq(result.errors.length, 0, 'renderAllFromDb had no errors'); + assert.ok(result.rendered > 0, 'renderAllFromDb rendered some files'); + assert.deepStrictEqual(result.errors.length, 0, 'renderAllFromDb had no errors'); // Verify M001 roadmap has S01 checked const m1Roadmap = fs.readFileSync( @@ -789,7 +764,7 @@ console.log('\n── markdown-renderer: renderAllFromDb produces all files ─ clearAllCaches(); const parsed1 = parseRoadmap(m1Roadmap); const s01 = parsed1.slices.find(s => s.id === 'S01'); - assertTrue(s01!.done, 'M001 S01 checked after renderAll'); + assert.ok(s01!.done, 'M001 S01 checked after renderAll'); // Verify M001/S01 plan has T01 checked const m1s1Plan = fs.readFileSync( @@ -797,26 +772,24 @@ console.log('\n── markdown-renderer: renderAllFromDb produces all files ─ ); clearAllCaches(); const parsedPlan = parsePlan(m1s1Plan); - assertTrue(parsedPlan.tasks[0].done, 'M001/S01 T01 checked after renderAll'); + assert.ok(parsedPlan.tasks[0].done, 'M001/S01 T01 checked after renderAll'); // Verify task summary written const taskSummaryPath = path.join( tmpDir, '.gsd', 'milestones', 'M001', 'slices', 'S01', 'tasks', 'T01-SUMMARY.md', ); - assertTrue(fs.existsSync(taskSummaryPath), 'T01 summary written by renderAll'); + assert.ok(fs.existsSync(taskSummaryPath), 'T01 summary written by renderAll'); } finally { closeDatabase(); cleanupDir(tmpDir); } -} +}); // ═══════════════════════════════════════════════════════════════════════════ // Graceful Degradation (Disk Fallback) // ═══════════════════════════════════════════════════════════════════════════ -console.log('\n── markdown-renderer: graceful fallback reads from disk when artifact not in DB ──'); - -{ +test('── markdown-renderer: graceful fallback reads from disk when artifact not in DB ──', async () => { const tmpDir = makeTmpDir(); const dbPath = path.join(tmpDir, '.gsd', 'gsd.db'); openDatabase(dbPath); @@ -838,29 +811,27 @@ console.log('\n── markdown-renderer: graceful fallback reads from disk when // Verify no artifact in DB const before = getArtifact('milestones/M001/M001-ROADMAP.md'); - assertEq(before, null, 'artifact not in DB before render'); + assert.deepStrictEqual(before, null, 'artifact not in DB before render'); // Render — should read from disk, store in DB const ok = await renderRoadmapCheckboxes(tmpDir, 'M001'); - assertTrue(ok, 'render succeeds with disk fallback'); + assert.ok(ok, 'render succeeds with disk fallback'); // Verify artifact now in DB (stored after reading from disk) const after = getArtifact('milestones/M001/M001-ROADMAP.md'); - assertTrue(after !== null, 'artifact stored in DB after disk fallback render'); - assertTrue(after!.full_content.includes('[x] **S01:'), 'DB artifact reflects rendered state'); + assert.ok(after !== null, 'artifact stored in DB after disk fallback render'); + assert.ok(after!.full_content.includes('[x] **S01:'), 'DB artifact reflects rendered state'); } finally { closeDatabase(); cleanupDir(tmpDir); } -} +}); // ═══════════════════════════════════════════════════════════════════════════ // stderr warnings (graceful degradation diagnostics) // ═══════════════════════════════════════════════════════════════════════════ -console.log('\n── markdown-renderer: stderr warning on missing content ──'); - -{ +test('── markdown-renderer: stderr warning on missing content ──', async () => { openDatabase(':memory:'); // No milestone/slices in DB, no files on disk — should return false and emit stderr @@ -868,18 +839,16 @@ console.log('\n── markdown-renderer: stderr warning on missing content ─ // No slices inserted — should warn about no slices const ok = await renderRoadmapCheckboxes('/nonexistent/path', 'M001'); - assertTrue(!ok, 'returns false when no slices in DB'); + assert.ok(!ok, 'returns false when no slices in DB'); closeDatabase(); -} +}); // ═══════════════════════════════════════════════════════════════════════════ // Stale Detection — Plan Checkbox Mismatch // ═══════════════════════════════════════════════════════════════════════════ -console.log('\n── markdown-renderer: detectStaleRenders finds plan checkbox mismatch ──'); - -{ +test('── markdown-renderer: detectStaleRenders finds plan checkbox mismatch ──', () => { const tmpDir = makeTmpDir(); const dbPath = path.join(tmpDir, '.gsd', 'gsd.db'); openDatabase(dbPath); @@ -910,27 +879,25 @@ console.log('\n── markdown-renderer: detectStaleRenders finds plan checkbox // The stale detection should find T02 as stale. const stale = detectStaleRenders(tmpDir); - assertTrue(stale.length > 0, 'detectStaleRenders should find stale entries'); + assert.ok(stale.length > 0, 'detectStaleRenders should find stale entries'); const t02Stale = stale.find(s => s.reason.includes('T02')); - assertTrue(!!t02Stale, 'should detect T02 as stale (done in DB, unchecked in plan)'); - assertTrue(t02Stale!.reason.includes('done in DB but unchecked'), 'reason should explain the mismatch'); + assert.ok(!!t02Stale, 'should detect T02 as stale (done in DB, unchecked in plan)'); + assert.ok(t02Stale!.reason.includes('done in DB but unchecked'), 'reason should explain the mismatch'); // T01 should NOT be stale — it's checked and done const t01Stale = stale.find(s => s.reason.includes('T01')); - assertEq(t01Stale, undefined, 'T01 should not be stale (done and checked)'); + assert.deepStrictEqual(t01Stale, undefined, 'T01 should not be stale (done and checked)'); } finally { closeDatabase(); cleanupDir(tmpDir); } -} +}); // ═══════════════════════════════════════════════════════════════════════════ // Stale Repair — Plan Checkbox // ═══════════════════════════════════════════════════════════════════════════ -console.log('\n── markdown-renderer: repairStaleRenders fixes plan and second detect returns empty ──'); - -{ +test('── markdown-renderer: repairStaleRenders fixes plan and second detect returns empty ──', async () => { const tmpDir = makeTmpDir(); const dbPath = path.join(tmpDir, '.gsd', 'gsd.db'); openDatabase(dbPath); @@ -956,34 +923,32 @@ console.log('\n── markdown-renderer: repairStaleRenders fixes plan and secon // Verify stale before repair const staleBefore = detectStaleRenders(tmpDir); - assertTrue(staleBefore.length > 0, 'should have stale entries before repair'); + assert.ok(staleBefore.length > 0, 'should have stale entries before repair'); // Repair const repaired = await repairStaleRenders(tmpDir); - assertTrue(repaired > 0, 'repairStaleRenders should repair at least 1 file'); + assert.ok(repaired > 0, 'repairStaleRenders should repair at least 1 file'); // After repair, detect again — should be empty clearAllCaches(); const staleAfter = detectStaleRenders(tmpDir); - assertEq(staleAfter.length, 0, 'detectStaleRenders should return empty after repair'); + assert.deepStrictEqual(staleAfter.length, 0, 'detectStaleRenders should return empty after repair'); // Verify the plan file was actually updated const repairedContent = fs.readFileSync(planPath, 'utf-8'); - assertTrue(repairedContent.includes('[x] **T01:'), 'T01 should be checked after repair'); - assertTrue(repairedContent.includes('[x] **T02:'), 'T02 should be checked after repair'); + assert.ok(repairedContent.includes('[x] **T01:'), 'T01 should be checked after repair'); + assert.ok(repairedContent.includes('[x] **T02:'), 'T02 should be checked after repair'); } finally { closeDatabase(); cleanupDir(tmpDir); } -} +}); // ═══════════════════════════════════════════════════════════════════════════ // Stale Detection — Roadmap Checkbox Mismatch // ═══════════════════════════════════════════════════════════════════════════ -console.log('\n── markdown-renderer: detectStaleRenders finds roadmap checkbox mismatch ──'); - -{ +test('── markdown-renderer: detectStaleRenders finds roadmap checkbox mismatch ──', () => { const tmpDir = makeTmpDir(); const dbPath = path.join(tmpDir, '.gsd', 'gsd.db'); openDatabase(dbPath); @@ -1007,23 +972,21 @@ console.log('\n── markdown-renderer: detectStaleRenders finds roadmap checkb const stale = detectStaleRenders(tmpDir); const s01Stale = stale.find(s => s.reason.includes('S01')); - assertTrue(!!s01Stale, 'should detect S01 as stale (complete in DB, unchecked in roadmap)'); + assert.ok(!!s01Stale, 'should detect S01 as stale (complete in DB, unchecked in roadmap)'); const s02Stale = stale.find(s => s.reason.includes('S02')); - assertEq(s02Stale, undefined, 'S02 should not be stale (pending and unchecked — matches)'); + assert.deepStrictEqual(s02Stale, undefined, 'S02 should not be stale (pending and unchecked — matches)'); } finally { closeDatabase(); cleanupDir(tmpDir); } -} +}); // ═══════════════════════════════════════════════════════════════════════════ // Stale Detection — Missing Task Summary // ═══════════════════════════════════════════════════════════════════════════ -console.log('\n── markdown-renderer: detectStaleRenders finds missing task summary ──'); - -{ +test('── markdown-renderer: detectStaleRenders finds missing task summary ──', () => { const tmpDir = makeTmpDir(); const dbPath = path.join(tmpDir, '.gsd', 'gsd.db'); openDatabase(dbPath); @@ -1058,21 +1021,19 @@ console.log('\n── markdown-renderer: detectStaleRenders finds missing task s const stale = detectStaleRenders(tmpDir); const summaryStale = stale.find(s => s.reason.includes('SUMMARY.md missing')); - assertTrue(!!summaryStale, 'should detect missing T01-SUMMARY.md'); - assertTrue(summaryStale!.reason.includes('T01'), 'reason should mention T01'); + assert.ok(!!summaryStale, 'should detect missing T01-SUMMARY.md'); + assert.ok(summaryStale!.reason.includes('T01'), 'reason should mention T01'); } finally { closeDatabase(); cleanupDir(tmpDir); } -} +}); // ═══════════════════════════════════════════════════════════════════════════ // Stale Repair — Missing Task Summary // ═══════════════════════════════════════════════════════════════════════════ -console.log('\n── markdown-renderer: repairStaleRenders writes missing task summary ──'); - -{ +test('── markdown-renderer: repairStaleRenders writes missing task summary ──', async () => { const tmpDir = makeTmpDir(); const dbPath = path.join(tmpDir, '.gsd', 'gsd.db'); openDatabase(dbPath); @@ -1104,32 +1065,30 @@ console.log('\n── markdown-renderer: repairStaleRenders writes missing task // Repair const repaired = await repairStaleRenders(tmpDir); - assertTrue(repaired > 0, 'should repair missing summary'); + assert.ok(repaired > 0, 'should repair missing summary'); // Verify file written const summaryPath = path.join( tmpDir, '.gsd', 'milestones', 'M001', 'slices', 'S01', 'tasks', 'T01-SUMMARY.md', ); - assertTrue(fs.existsSync(summaryPath), 'T01-SUMMARY.md should exist after repair'); + assert.ok(fs.existsSync(summaryPath), 'T01-SUMMARY.md should exist after repair'); // Second detect should be empty clearAllCaches(); const staleAfter = detectStaleRenders(tmpDir); const summaryStale = staleAfter.find(s => s.reason.includes('SUMMARY.md missing') && s.reason.includes('T01')); - assertEq(summaryStale, undefined, 'missing summary should be fixed after repair'); + assert.deepStrictEqual(summaryStale, undefined, 'missing summary should be fixed after repair'); } finally { closeDatabase(); cleanupDir(tmpDir); } -} +}); // ═══════════════════════════════════════════════════════════════════════════ // Stale Repair — Idempotency // ═══════════════════════════════════════════════════════════════════════════ -console.log('\n── markdown-renderer: repairStaleRenders idempotency — fully synced returns 0 ──'); - -{ +test('── markdown-renderer: repairStaleRenders idempotency — fully synced returns 0 ──', async () => { const tmpDir = makeTmpDir(); const dbPath = path.join(tmpDir, '.gsd', 'gsd.db'); openDatabase(dbPath); @@ -1152,20 +1111,18 @@ console.log('\n── markdown-renderer: repairStaleRenders idempotency — full // No stale entries when everything is in sync (no summary to check since no fullSummaryMd) const repaired = await repairStaleRenders(tmpDir); - assertEq(repaired, 0, 'repairStaleRenders should return 0 on fully synced project'); + assert.deepStrictEqual(repaired, 0, 'repairStaleRenders should return 0 on fully synced project'); } finally { closeDatabase(); cleanupDir(tmpDir); } -} +}); // ═══════════════════════════════════════════════════════════════════════════ // Stale Detection — Missing Slice Summary + UAT // ═══════════════════════════════════════════════════════════════════════════ -console.log('\n── markdown-renderer: detectStaleRenders finds missing slice summary and UAT ──'); - -{ +test('── markdown-renderer: detectStaleRenders finds missing slice summary and UAT ──', () => { const tmpDir = makeTmpDir(); const dbPath = path.join(tmpDir, '.gsd', 'gsd.db'); openDatabase(dbPath); @@ -1192,14 +1149,13 @@ console.log('\n── markdown-renderer: detectStaleRenders finds missing slice const summaryStale = stale.find(s => s.reason.includes('SUMMARY.md missing') && s.reason.includes('S01')); const uatStale = stale.find(s => s.reason.includes('UAT.md missing') && s.reason.includes('S01')); - assertTrue(!!summaryStale, 'should detect missing S01-SUMMARY.md'); - assertTrue(!!uatStale, 'should detect missing S01-UAT.md'); + assert.ok(!!summaryStale, 'should detect missing S01-SUMMARY.md'); + assert.ok(!!uatStale, 'should detect missing S01-UAT.md'); } finally { closeDatabase(); cleanupDir(tmpDir); } -} +}); // ═══════════════════════════════════════════════════════════════════════════ -report(); diff --git a/src/resources/extensions/gsd/tests/md-importer.test.ts b/src/resources/extensions/gsd/tests/md-importer.test.ts index b4830e893..de4a721b8 100644 --- a/src/resources/extensions/gsd/tests/md-importer.test.ts +++ b/src/resources/extensions/gsd/tests/md-importer.test.ts @@ -1,4 +1,3 @@ -import { createTestContext } from './test-helpers.ts'; import * as fs from 'node:fs'; import * as path from 'node:path'; import * as os from 'node:os'; @@ -17,8 +16,8 @@ import { parseRequirementsSections, migrateFromMarkdown, } from '../md-importer.ts'; - -const { assertEq, assertTrue, report } = createTestContext(); +import { describe, test, beforeEach, afterEach } from 'node:test'; +import assert from 'node:assert/strict'; // ═══════════════════════════════════════════════════════════════════════════ // Fixtures @@ -135,43 +134,37 @@ function cleanupDir(dir: string): void { // md-importer: parseDecisionsTable // ═══════════════════════════════════════════════════════════════════════════ -console.log('\n=== md-importer: parseDecisionsTable ==='); - -{ +test('md-importer: parseDecisionsTable', () => { const decisions = parseDecisionsTable(DECISIONS_MD); - assertEq(decisions.length, 4, 'should parse 4 decisions'); - assertEq(decisions[0].id, 'D001', 'first decision should be D001'); - assertEq(decisions[0].decision, 'SQLite library', 'D001 decision text'); - assertEq(decisions[0].choice, 'better-sqlite3', 'D001 choice'); - assertEq(decisions[0].scope, 'library', 'D001 scope'); - assertEq(decisions[0].revisable, 'No', 'D001 revisable'); -} + assert.deepStrictEqual(decisions.length, 4, 'should parse 4 decisions'); + assert.deepStrictEqual(decisions[0].id, 'D001', 'first decision should be D001'); + assert.deepStrictEqual(decisions[0].decision, 'SQLite library', 'D001 decision text'); + assert.deepStrictEqual(decisions[0].choice, 'better-sqlite3', 'D001 choice'); + assert.deepStrictEqual(decisions[0].scope, 'library', 'D001 scope'); + assert.deepStrictEqual(decisions[0].revisable, 'No', 'D001 revisable'); +}); -console.log('=== md-importer: supersession detection ==='); - -{ +test('md-importer: supersession detection', () => { const decisions = parseDecisionsTable(DECISIONS_MD); // D010 amends D001 → D001.superseded_by = D010 const d001 = decisions.find(d => d.id === 'D001'); - assertEq(d001?.superseded_by, 'D010', 'D001 should be superseded by D010'); + assert.deepStrictEqual(d001?.superseded_by, 'D010', 'D001 should be superseded by D010'); // D020 amends D010 → D010.superseded_by = D020 const d010 = decisions.find(d => d.id === 'D010'); - assertEq(d010?.superseded_by, 'D020', 'D010 should be superseded by D020'); + assert.deepStrictEqual(d010?.superseded_by, 'D020', 'D010 should be superseded by D020'); // D002 is not amended const d002 = decisions.find(d => d.id === 'D002'); - assertEq(d002?.superseded_by, null, 'D002 should not be superseded'); + assert.deepStrictEqual(d002?.superseded_by, null, 'D002 should not be superseded'); // D020 is the latest in chain, not superseded const d020 = decisions.find(d => d.id === 'D020'); - assertEq(d020?.superseded_by, null, 'D020 should not be superseded'); -} + assert.deepStrictEqual(d020?.superseded_by, null, 'D020 should not be superseded'); +}); -console.log('=== md-importer: malformed/empty rows skipped ==='); - -{ +test('md-importer: malformed/empty rows skipped', () => { const malformedInput = `# Decisions | # | When | Scope | Decision | Choice | Rationale | Revisable? | @@ -182,24 +175,20 @@ console.log('=== md-importer: malformed/empty rows skipped ==='); | D003 | M001 | arch | Config | JSON | Simple | Yes | `; const decisions = parseDecisionsTable(malformedInput); - assertEq(decisions.length, 2, 'should skip rows without D-prefix IDs'); - assertEq(decisions[0].id, 'D001', 'first valid row'); - assertEq(decisions[1].id, 'D003', 'second valid row (skipping malformed)'); -} + assert.deepStrictEqual(decisions.length, 2, 'should skip rows without D-prefix IDs'); + assert.deepStrictEqual(decisions[0].id, 'D001', 'first valid row'); + assert.deepStrictEqual(decisions[1].id, 'D003', 'second valid row (skipping malformed)'); +}); -console.log('=== md-importer: made_by backward compatibility (old 7-column format) ==='); - -{ +test('md-importer: made_by backward compatibility (old 7-column format)', () => { const decisions = parseDecisionsTable(DECISIONS_MD); // Old format has no Made By column — should default to 'agent' for (const d of decisions) { - assertEq(d.made_by, 'agent', `${d.id} made_by defaults to agent for legacy format`); + assert.deepStrictEqual(d.made_by, 'agent', `${d.id} made_by defaults to agent for legacy format`); } -} +}); -console.log('=== md-importer: made_by column parsing (new 8-column format) ==='); - -{ +test('md-importer: made_by column parsing (new 8-column format)', () => { const newFormatMd = `# Decisions Register | # | When | Scope | Decision | Choice | Rationale | Revisable? | Made By | @@ -210,62 +199,58 @@ console.log('=== md-importer: made_by column parsing (new 8-column format) ===') | D004 | M002 | impl | Cache strategy | LRU | Predictable | No | bogus | `; const decisions = parseDecisionsTable(newFormatMd); - assertEq(decisions.length, 4, 'should parse 4 decisions with new format'); - assertEq(decisions[0].made_by, 'human', 'D001 made_by = human'); - assertEq(decisions[1].made_by, 'agent', 'D002 made_by = agent'); - assertEq(decisions[2].made_by, 'collaborative', 'D003 made_by = collaborative'); - assertEq(decisions[3].made_by, 'agent', 'D004 invalid made_by defaults to agent'); -} + assert.deepStrictEqual(decisions.length, 4, 'should parse 4 decisions with new format'); + assert.deepStrictEqual(decisions[0].made_by, 'human', 'D001 made_by = human'); + assert.deepStrictEqual(decisions[1].made_by, 'agent', 'D002 made_by = agent'); + assert.deepStrictEqual(decisions[2].made_by, 'collaborative', 'D003 made_by = collaborative'); + assert.deepStrictEqual(decisions[3].made_by, 'agent', 'D004 invalid made_by defaults to agent'); +}); // ═══════════════════════════════════════════════════════════════════════════ // md-importer: parseRequirementsSections // ═══════════════════════════════════════════════════════════════════════════ -console.log('=== md-importer: parseRequirementsSections ==='); - -{ +test('md-importer: parseRequirementsSections', () => { const reqs = parseRequirementsSections(REQUIREMENTS_MD); - assertEq(reqs.length, 5, 'should parse 5 unique requirements'); + assert.deepStrictEqual(reqs.length, 5, 'should parse 5 unique requirements'); const r001 = reqs.find(r => r.id === 'R001'); - assertTrue(!!r001, 'R001 should exist'); - assertEq(r001?.class, 'core-capability', 'R001 class'); - assertEq(r001?.status, 'active', 'R001 status'); - assertEq(r001?.description, 'A SQLite database with typed wrappers', 'R001 description'); - assertEq(r001?.why, 'Foundation for storage', 'R001 why'); - assertEq(r001?.source, 'user', 'R001 source'); - assertEq(r001?.primary_owner, 'M001/S01', 'R001 primary_owner'); - assertEq(r001?.supporting_slices, 'none', 'R001 supporting_slices'); - assertEq(r001?.validation, 'unmapped', 'R001 validation'); - assertEq(r001?.notes, 'WAL mode enabled', 'R001 notes'); - assertTrue(r001?.full_content?.includes('### R001') ?? false, 'R001 full_content should have heading'); + assert.ok(!!r001, 'R001 should exist'); + assert.deepStrictEqual(r001?.class, 'core-capability', 'R001 class'); + assert.deepStrictEqual(r001?.status, 'active', 'R001 status'); + assert.deepStrictEqual(r001?.description, 'A SQLite database with typed wrappers', 'R001 description'); + assert.deepStrictEqual(r001?.why, 'Foundation for storage', 'R001 why'); + assert.deepStrictEqual(r001?.source, 'user', 'R001 source'); + assert.deepStrictEqual(r001?.primary_owner, 'M001/S01', 'R001 primary_owner'); + assert.deepStrictEqual(r001?.supporting_slices, 'none', 'R001 supporting_slices'); + assert.deepStrictEqual(r001?.validation, 'unmapped', 'R001 validation'); + assert.deepStrictEqual(r001?.notes, 'WAL mode enabled', 'R001 notes'); + assert.ok(r001?.full_content?.includes('### R001') ?? false, 'R001 full_content should have heading'); // Validated section — R017 (abbreviated format with "Validated by" / "Proof" bullets) const r017 = reqs.find(r => r.id === 'R017'); - assertTrue(!!r017, 'R017 should exist'); - assertEq(r017?.status, 'validated', 'R017 status from validated section'); - assertEq(r017?.validation, 'M001/S01', 'R017 validation (from "Validated by" bullet)'); - assertEq(r017?.notes, '50 decisions queried in 0.62ms', 'R017 notes (from "Proof" bullet)'); + assert.ok(!!r017, 'R017 should exist'); + assert.deepStrictEqual(r017?.status, 'validated', 'R017 status from validated section'); + assert.deepStrictEqual(r017?.validation, 'M001/S01', 'R017 validation (from "Validated by" bullet)'); + assert.deepStrictEqual(r017?.notes, '50 decisions queried in 0.62ms', 'R017 notes (from "Proof" bullet)'); // Deferred requirement const r030 = reqs.find(r => r.id === 'R030'); - assertEq(r030?.status, 'deferred', 'R030 status should be deferred'); - assertEq(r030?.class, 'differentiator', 'R030 class'); - assertEq(r030?.description, 'Rust crate for embeddings', 'R030 description'); + assert.deepStrictEqual(r030?.status, 'deferred', 'R030 status should be deferred'); + assert.deepStrictEqual(r030?.class, 'differentiator', 'R030 class'); + assert.deepStrictEqual(r030?.description, 'Rust crate for embeddings', 'R030 description'); // Out of scope const r040 = reqs.find(r => r.id === 'R040'); - assertEq(r040?.status, 'out-of-scope', 'R040 status should be out-of-scope'); - assertEq(r040?.class, 'anti-feature', 'R040 class'); -} + assert.deepStrictEqual(r040?.status, 'out-of-scope', 'R040 status should be out-of-scope'); + assert.deepStrictEqual(r040?.class, 'anti-feature', 'R040 class'); +}); // ═══════════════════════════════════════════════════════════════════════════ // md-importer: migrateFromMarkdown orchestrator // ═══════════════════════════════════════════════════════════════════════════ -console.log('=== md-importer: migrateFromMarkdown orchestrator ==='); - -{ +test('md-importer: migrateFromMarkdown orchestrator', () => { const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-import-test-')); createFixtureTree(tmpDir); @@ -273,53 +258,51 @@ console.log('=== md-importer: migrateFromMarkdown orchestrator ==='); openDatabase(':memory:'); const result = migrateFromMarkdown(tmpDir); - assertEq(result.decisions, 4, 'should import 4 decisions'); - assertEq(result.requirements, 5, 'should import 5 requirements'); - assertTrue(result.artifacts > 0, 'should import some artifacts'); + assert.deepStrictEqual(result.decisions, 4, 'should import 4 decisions'); + assert.deepStrictEqual(result.requirements, 5, 'should import 5 requirements'); + assert.ok(result.artifacts > 0, 'should import some artifacts'); // Verify decisions queryable const d001 = getDecisionById('D001'); - assertTrue(!!d001, 'D001 should be queryable'); - assertEq(d001?.superseded_by, 'D010', 'D001 superseded_by should be D010'); + assert.ok(!!d001, 'D001 should be queryable'); + assert.deepStrictEqual(d001?.superseded_by, 'D010', 'D001 superseded_by should be D010'); // Verify requirements queryable const r001 = getRequirementById('R001'); - assertTrue(!!r001, 'R001 should be queryable'); - assertEq(r001?.status, 'active', 'R001 status from DB'); + assert.ok(!!r001, 'R001 should be queryable'); + assert.deepStrictEqual(r001?.status, 'active', 'R001 status from DB'); // Verify active views const activeD = getActiveDecisions(); - assertEq(activeD.length, 2, 'should have 2 active decisions (D002, D020)'); + assert.deepStrictEqual(activeD.length, 2, 'should have 2 active decisions (D002, D020)'); // Verify artifacts table const adapter = _getAdapter(); const artifacts = adapter?.prepare('SELECT count(*) as c FROM artifacts').get(); - assertTrue((artifacts?.c as number) > 0, 'artifacts table should have rows'); + assert.ok((artifacts?.c as number) > 0, 'artifacts table should have rows'); // Verify hierarchy correctness const roadmap = adapter?.prepare('SELECT * FROM artifacts WHERE artifact_type = :type').get({ ':type': 'ROADMAP' }); - assertTrue(!!roadmap, 'ROADMAP artifact should exist'); - assertEq(roadmap?.milestone_id, 'M001', 'ROADMAP should be in M001'); + assert.ok(!!roadmap, 'ROADMAP artifact should exist'); + assert.deepStrictEqual(roadmap?.milestone_id, 'M001', 'ROADMAP should be in M001'); const taskPlan = adapter?.prepare('SELECT * FROM artifacts WHERE task_id = :taskId AND artifact_type = :type').get({ ':taskId': 'T01', ':type': 'PLAN', }); - assertTrue(!!taskPlan, 'T01-PLAN artifact should exist'); + assert.ok(!!taskPlan, 'T01-PLAN artifact should exist'); closeDatabase(); } finally { cleanupDir(tmpDir); } -} +}); // ═══════════════════════════════════════════════════════════════════════════ // md-importer: idempotent re-import // ═══════════════════════════════════════════════════════════════════════════ -console.log('=== md-importer: idempotent re-import ==='); - -{ +test('md-importer: idempotent re-import', () => { const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-idemp-test-')); createFixtureTree(tmpDir); @@ -328,9 +311,9 @@ console.log('=== md-importer: idempotent re-import ==='); const r1 = migrateFromMarkdown(tmpDir); const r2 = migrateFromMarkdown(tmpDir); - assertEq(r1.decisions, r2.decisions, 'double import should produce same decision count'); - assertEq(r1.requirements, r2.requirements, 'double import should produce same requirement count'); - assertEq(r1.artifacts, r2.artifacts, 'double import should produce same artifact count'); + assert.deepStrictEqual(r1.decisions, r2.decisions, 'double import should produce same decision count'); + assert.deepStrictEqual(r1.requirements, r2.requirements, 'double import should produce same requirement count'); + assert.deepStrictEqual(r1.artifacts, r2.artifacts, 'double import should produce same artifact count'); // Verify no duplicates const adapter = _getAdapter(); @@ -338,23 +321,21 @@ console.log('=== md-importer: idempotent re-import ==='); const rc = adapter?.prepare('SELECT count(*) as c FROM requirements').get()?.c as number; const ac = adapter?.prepare('SELECT count(*) as c FROM artifacts').get()?.c as number; - assertEq(dc, r1.decisions, 'DB decision count matches import count'); - assertEq(rc, r1.requirements, 'DB requirement count matches import count'); - assertEq(ac, r1.artifacts, 'DB artifact count matches import count'); + assert.deepStrictEqual(dc, r1.decisions, 'DB decision count matches import count'); + assert.deepStrictEqual(rc, r1.requirements, 'DB requirement count matches import count'); + assert.deepStrictEqual(ac, r1.artifacts, 'DB artifact count matches import count'); closeDatabase(); } finally { cleanupDir(tmpDir); } -} +}); // ═══════════════════════════════════════════════════════════════════════════ // md-importer: missing file graceful handling // ═══════════════════════════════════════════════════════════════════════════ -console.log('=== md-importer: missing file handling ==='); - -{ +test('md-importer: missing file handling', () => { const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-empty-test-')); // Create empty .gsd/ with no files fs.mkdirSync(path.join(tmpDir, '.gsd'), { recursive: true }); @@ -363,43 +344,39 @@ console.log('=== md-importer: missing file handling ==='); openDatabase(':memory:'); const result = migrateFromMarkdown(tmpDir); - assertEq(result.decisions, 0, 'missing DECISIONS.md → 0 decisions'); - assertEq(result.requirements, 0, 'missing REQUIREMENTS.md → 0 requirements'); - assertEq(result.artifacts, 0, 'empty tree → 0 artifacts'); + assert.deepStrictEqual(result.decisions, 0, 'missing DECISIONS.md → 0 decisions'); + assert.deepStrictEqual(result.requirements, 0, 'missing REQUIREMENTS.md → 0 requirements'); + assert.deepStrictEqual(result.artifacts, 0, 'empty tree → 0 artifacts'); closeDatabase(); } finally { cleanupDir(tmpDir); } -} +}); // ═══════════════════════════════════════════════════════════════════════════ // md-importer: schema v1→v2 migration on existing DBs // ═══════════════════════════════════════════════════════════════════════════ -console.log('=== md-importer: schema v1→v2 migration ==='); - -{ +test('md-importer: schema v1→v2 migration', () => { // This test verifies that opening a fresh DB auto-migrates to current schema version openDatabase(':memory:'); const adapter = _getAdapter(); const version = adapter?.prepare('SELECT MAX(version) as v FROM schema_version').get(); - assertEq(version?.v, 10, 'new DB should be at schema version 10'); + assert.deepStrictEqual(version?.v, 10, 'new DB should be at schema version 10'); // Artifacts table should exist const tableCheck = adapter?.prepare("SELECT count(*) as c FROM sqlite_master WHERE type='table' AND name='artifacts'").get(); - assertEq(tableCheck?.c, 1, 'artifacts table should exist'); + assert.deepStrictEqual(tableCheck?.c, 1, 'artifacts table should exist'); closeDatabase(); -} +}); // ═══════════════════════════════════════════════════════════════════════════ // md-importer: round-trip fidelity // ═══════════════════════════════════════════════════════════════════════════ -console.log('=== md-importer: round-trip fidelity ==='); - -{ +test('md-importer: round-trip fidelity', () => { const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-roundtrip-test-')); createFixtureTree(tmpDir); @@ -409,32 +386,31 @@ console.log('=== md-importer: round-trip fidelity ==='); // Round-trip: verify imported field values match source const d002 = getDecisionById('D002'); - assertEq(d002?.when_context, 'M001', 'D002 when_context round-trip'); - assertEq(d002?.scope, 'arch', 'D002 scope round-trip'); - assertEq(d002?.decision, 'DB location', 'D002 decision round-trip'); - assertEq(d002?.choice, '.gsd/gsd.db', 'D002 choice round-trip'); - assertEq(d002?.rationale, 'Derived state', 'D002 rationale round-trip'); + assert.deepStrictEqual(d002?.when_context, 'M001', 'D002 when_context round-trip'); + assert.deepStrictEqual(d002?.scope, 'arch', 'D002 scope round-trip'); + assert.deepStrictEqual(d002?.decision, 'DB location', 'D002 decision round-trip'); + assert.deepStrictEqual(d002?.choice, '.gsd/gsd.db', 'D002 choice round-trip'); + assert.deepStrictEqual(d002?.rationale, 'Derived state', 'D002 rationale round-trip'); const r002 = getRequirementById('R002'); - assertEq(r002?.class, 'failure-visibility', 'R002 class round-trip'); - assertEq(r002?.description, 'Falls back to markdown if SQLite unavailable', 'R002 description round-trip'); - assertEq(r002?.why, 'Must not break on exotic platforms', 'R002 why round-trip'); - assertEq(r002?.primary_owner, 'M001/S01', 'R002 primary_owner round-trip'); - assertEq(r002?.supporting_slices, 'M001/S03', 'R002 supporting_slices round-trip'); - assertEq(r002?.notes, 'Transparent fallback', 'R002 notes round-trip'); - assertEq(r002?.validation, 'unmapped', 'R002 validation round-trip'); + assert.deepStrictEqual(r002?.class, 'failure-visibility', 'R002 class round-trip'); + assert.deepStrictEqual(r002?.description, 'Falls back to markdown if SQLite unavailable', 'R002 description round-trip'); + assert.deepStrictEqual(r002?.why, 'Must not break on exotic platforms', 'R002 why round-trip'); + assert.deepStrictEqual(r002?.primary_owner, 'M001/S01', 'R002 primary_owner round-trip'); + assert.deepStrictEqual(r002?.supporting_slices, 'M001/S03', 'R002 supporting_slices round-trip'); + assert.deepStrictEqual(r002?.notes, 'Transparent fallback', 'R002 notes round-trip'); + assert.deepStrictEqual(r002?.validation, 'unmapped', 'R002 validation round-trip'); // Verify artifact content is stored const adapter = _getAdapter(); const project = adapter?.prepare("SELECT * FROM artifacts WHERE path = :path").get({ ':path': 'PROJECT.md' }); - assertTrue((project?.full_content as string)?.includes('Test Project'), 'PROJECT.md content round-trip'); + assert.ok((project?.full_content as string)?.includes('Test Project'), 'PROJECT.md content round-trip'); closeDatabase(); } finally { cleanupDir(tmpDir); } -} +}); // ═══════════════════════════════════════════════════════════════════════════ -report(); diff --git a/src/resources/extensions/gsd/tests/memory-extractor.test.ts b/src/resources/extensions/gsd/tests/memory-extractor.test.ts index a4e4f7031..4df555470 100644 --- a/src/resources/extensions/gsd/tests/memory-extractor.test.ts +++ b/src/resources/extensions/gsd/tests/memory-extractor.test.ts @@ -1,4 +1,3 @@ -import { createTestContext } from './test-helpers.ts'; import { parseMemoryResponse, _resetExtractionState } from '../memory-extractor.ts'; import { openDatabase, @@ -10,15 +9,14 @@ import { getActiveMemoriesRanked, } from '../memory-store.ts'; import type { MemoryAction } from '../memory-store.ts'; - -const { assertEq, assertTrue, report } = createTestContext(); +import { describe, test, beforeEach, afterEach } from 'node:test'; +import assert from 'node:assert/strict'; // ═══════════════════════════════════════════════════════════════════════════ // memory-extractor: parse valid JSON response // ═══════════════════════════════════════════════════════════════════════════ -console.log('\n=== memory-extractor: parse valid JSON ==='); -{ +test('memory-extractor: parse valid JSON', () => { const response = JSON.stringify([ { action: 'CREATE', category: 'gotcha', content: 'esbuild drops binaries', confidence: 0.85 }, { action: 'REINFORCE', id: 'MEM001' }, @@ -27,56 +25,52 @@ console.log('\n=== memory-extractor: parse valid JSON ==='); ]); const actions = parseMemoryResponse(response); - assertEq(actions.length, 4, 'should parse 4 actions'); - assertEq(actions[0].action, 'CREATE', 'first action should be CREATE'); - assertEq((actions[0] as any).category, 'gotcha', 'CREATE category'); - assertEq((actions[0] as any).confidence, 0.85, 'CREATE confidence'); - assertEq(actions[1].action, 'REINFORCE', 'second action should be REINFORCE'); - assertEq(actions[2].action, 'UPDATE', 'third action should be UPDATE'); - assertEq(actions[3].action, 'SUPERSEDE', 'fourth action should be SUPERSEDE'); -} + assert.deepStrictEqual(actions.length, 4, 'should parse 4 actions'); + assert.deepStrictEqual(actions[0].action, 'CREATE', 'first action should be CREATE'); + assert.deepStrictEqual((actions[0] as any).category, 'gotcha', 'CREATE category'); + assert.deepStrictEqual((actions[0] as any).confidence, 0.85, 'CREATE confidence'); + assert.deepStrictEqual(actions[1].action, 'REINFORCE', 'second action should be REINFORCE'); + assert.deepStrictEqual(actions[2].action, 'UPDATE', 'third action should be UPDATE'); + assert.deepStrictEqual(actions[3].action, 'SUPERSEDE', 'fourth action should be SUPERSEDE'); +}); // ═══════════════════════════════════════════════════════════════════════════ // memory-extractor: parse fenced JSON response // ═══════════════════════════════════════════════════════════════════════════ -console.log('\n=== memory-extractor: parse fenced JSON ==='); -{ +test('memory-extractor: parse fenced JSON', () => { const response = '```json\n[\n {"action": "CREATE", "category": "convention", "content": "test memory"}\n]\n```'; const actions = parseMemoryResponse(response); - assertEq(actions.length, 1, 'should parse 1 action from fenced JSON'); - assertEq(actions[0].action, 'CREATE', 'action should be CREATE'); -} + assert.deepStrictEqual(actions.length, 1, 'should parse 1 action from fenced JSON'); + assert.deepStrictEqual(actions[0].action, 'CREATE', 'action should be CREATE'); +}); // ═══════════════════════════════════════════════════════════════════════════ // memory-extractor: parse empty array response // ═══════════════════════════════════════════════════════════════════════════ -console.log('\n=== memory-extractor: parse empty array ==='); -{ +test('memory-extractor: parse empty array', () => { const actions = parseMemoryResponse('[]'); - assertEq(actions.length, 0, 'empty array should parse to empty actions'); -} + assert.deepStrictEqual(actions.length, 0, 'empty array should parse to empty actions'); +}); // ═══════════════════════════════════════════════════════════════════════════ // memory-extractor: parse malformed response // ═══════════════════════════════════════════════════════════════════════════ -console.log('\n=== memory-extractor: malformed responses ==='); -{ - assertEq(parseMemoryResponse('not json at all'), [], 'garbage text should return []'); - assertEq(parseMemoryResponse('{"action": "CREATE"}'), [], 'non-array should return []'); - assertEq(parseMemoryResponse(''), [], 'empty string should return []'); - assertEq(parseMemoryResponse('```\nbroken\n```'), [], 'fenced non-JSON should return []'); -} +test('memory-extractor: malformed responses', () => { + assert.deepStrictEqual(parseMemoryResponse('not json at all'), [], 'garbage text should return []'); + assert.deepStrictEqual(parseMemoryResponse('{"action": "CREATE"}'), [], 'non-array should return []'); + assert.deepStrictEqual(parseMemoryResponse(''), [], 'empty string should return []'); + assert.deepStrictEqual(parseMemoryResponse('```\nbroken\n```'), [], 'fenced non-JSON should return []'); +}); // ═══════════════════════════════════════════════════════════════════════════ // memory-extractor: validation of required fields // ═══════════════════════════════════════════════════════════════════════════ -console.log('\n=== memory-extractor: field validation ==='); -{ +test('memory-extractor: field validation', () => { const response = JSON.stringify([ // Valid CREATE { action: 'CREATE', category: 'gotcha', content: 'valid' }, @@ -103,19 +97,18 @@ console.log('\n=== memory-extractor: field validation ==='); ]); const actions = parseMemoryResponse(response); - assertEq(actions.length, 4, 'should only accept 4 valid actions'); - assertEq(actions[0].action, 'CREATE', 'first valid is CREATE'); - assertEq(actions[1].action, 'REINFORCE', 'second valid is REINFORCE'); - assertEq(actions[2].action, 'UPDATE', 'third valid is UPDATE'); - assertEq(actions[3].action, 'SUPERSEDE', 'fourth valid is SUPERSEDE'); -} + assert.deepStrictEqual(actions.length, 4, 'should only accept 4 valid actions'); + assert.deepStrictEqual(actions[0].action, 'CREATE', 'first valid is CREATE'); + assert.deepStrictEqual(actions[1].action, 'REINFORCE', 'second valid is REINFORCE'); + assert.deepStrictEqual(actions[2].action, 'UPDATE', 'third valid is UPDATE'); + assert.deepStrictEqual(actions[3].action, 'SUPERSEDE', 'fourth valid is SUPERSEDE'); +}); // ═══════════════════════════════════════════════════════════════════════════ // Integration: applyMemoryActions with mixed actions // ═══════════════════════════════════════════════════════════════════════════ -console.log('\n=== integration: mixed action lifecycle ==='); -{ +test('integration: mixed action lifecycle', () => { openDatabase(':memory:'); // Phase 1: Create initial memories @@ -126,7 +119,7 @@ console.log('\n=== integration: mixed action lifecycle ==='); ], 'plan-slice', 'M001/S01'); let active = getActiveMemoriesRanked(30); - assertEq(active.length, 3, 'phase 1: 3 active memories'); + assert.deepStrictEqual(active.length, 3, 'phase 1: 3 active memories'); // Phase 2: Reinforce one, update another, create new applyMemoryActions([ @@ -136,13 +129,13 @@ console.log('\n=== integration: mixed action lifecycle ==='); ], 'execute-task', 'M001/S01/T01'); active = getActiveMemoriesRanked(30); - assertEq(active.length, 4, 'phase 2: 4 active memories'); - assertEq( + assert.deepStrictEqual(active.length, 4, 'phase 2: 4 active memories'); + assert.deepStrictEqual( active.find(m => m.id === 'MEM001')?.content, 'npm run build requires tsc --noEmit first', 'MEM001 content should be updated', ); - assertEq(active.find(m => m.id === 'MEM002')?.hit_count, 1, 'MEM002 should be reinforced'); + assert.deepStrictEqual(active.find(m => m.id === 'MEM002')?.hit_count, 1, 'MEM002 should be reinforced'); // Phase 3: Supersede MEM001 with MEM005 applyMemoryActions([ @@ -151,30 +144,28 @@ console.log('\n=== integration: mixed action lifecycle ==='); ], 'execute-task', 'M001/S01/T02'); active = getActiveMemoriesRanked(30); - assertEq(active.length, 4, 'phase 3: 4 active (1 superseded, 1 created)'); - assertTrue(!active.find(m => m.id === 'MEM001'), 'MEM001 should be superseded'); - assertTrue(!!active.find(m => m.id === 'MEM005'), 'MEM005 should be active'); + assert.deepStrictEqual(active.length, 4, 'phase 3: 4 active (1 superseded, 1 created)'); + assert.ok(!active.find(m => m.id === 'MEM001'), 'MEM001 should be superseded'); + assert.ok(!!active.find(m => m.id === 'MEM005'), 'MEM005 should be active'); // Verify ranking: MEM003 (0.85) > MEM005 (0.9) but MEM002 has 1 hit // MEM002: 0.8 * (1 + 1*0.1) = 0.88 // MEM003: 0.85 * 1.0 = 0.85 // MEM005: 0.9 * 1.0 = 0.9 // MEM004: 0.75 * 1.0 = 0.75 - assertEq(active[0].id, 'MEM005', 'MEM005 should rank first (0.9)'); - assertEq(active[1].id, 'MEM002', 'MEM002 should rank second (0.88)'); + assert.deepStrictEqual(active[0].id, 'MEM005', 'MEM005 should rank first (0.9)'); + assert.deepStrictEqual(active[1].id, 'MEM002', 'MEM002 should rank second (0.88)'); closeDatabase(); -} +}); // ═══════════════════════════════════════════════════════════════════════════ // memory-extractor: _resetExtractionState // ═══════════════════════════════════════════════════════════════════════════ -console.log('\n=== memory-extractor: reset extraction state ==='); -{ +test('memory-extractor: reset extraction state', () => { // Just verify it doesn't throw _resetExtractionState(); - assertTrue(true, '_resetExtractionState should not throw'); -} + assert.ok(true, '_resetExtractionState should not throw'); +}); -report(); diff --git a/src/resources/extensions/gsd/tests/memory-store.test.ts b/src/resources/extensions/gsd/tests/memory-store.test.ts index 062e86ff5..48217a163 100644 --- a/src/resources/extensions/gsd/tests/memory-store.test.ts +++ b/src/resources/extensions/gsd/tests/memory-store.test.ts @@ -1,4 +1,3 @@ -import { createTestContext } from './test-helpers.ts'; import { openDatabase, closeDatabase, @@ -21,94 +20,90 @@ import { formatMemoriesForPrompt, } from '../memory-store.ts'; import type { MemoryAction } from '../memory-store.ts'; - -const { assertEq, assertTrue, assertMatch, report } = createTestContext(); +import { describe, test, beforeEach, afterEach } from 'node:test'; +import assert from 'node:assert/strict'; // ═══════════════════════════════════════════════════════════════════════════ // memory-store: fallback when DB not open // ═══════════════════════════════════════════════════════════════════════════ -console.log('\n=== memory-store: fallback returns empty when DB not open ==='); -{ +test('memory-store: fallback returns empty when DB not open', () => { closeDatabase(); - assertTrue(!isDbAvailable(), 'DB should not be available'); + assert.ok(!isDbAvailable(), 'DB should not be available'); - assertEq(getActiveMemories(), [], 'getActiveMemories returns [] when DB closed'); - assertEq(getActiveMemoriesRanked(), [], 'getActiveMemoriesRanked returns [] when DB closed'); - assertEq(nextMemoryId(), 'MEM001', 'nextMemoryId returns MEM001 when DB closed'); - assertEq(createMemory({ category: 'test', content: 'test' }), null, 'createMemory returns null when DB closed'); - assertTrue(!reinforceMemory('MEM001'), 'reinforceMemory returns false when DB closed'); - assertTrue(!isUnitProcessed('test/key'), 'isUnitProcessed returns false when DB closed'); -} + assert.deepStrictEqual(getActiveMemories(), [], 'getActiveMemories returns [] when DB closed'); + assert.deepStrictEqual(getActiveMemoriesRanked(), [], 'getActiveMemoriesRanked returns [] when DB closed'); + assert.deepStrictEqual(nextMemoryId(), 'MEM001', 'nextMemoryId returns MEM001 when DB closed'); + assert.deepStrictEqual(createMemory({ category: 'test', content: 'test' }), null, 'createMemory returns null when DB closed'); + assert.ok(!reinforceMemory('MEM001'), 'reinforceMemory returns false when DB closed'); + assert.ok(!isUnitProcessed('test/key'), 'isUnitProcessed returns false when DB closed'); +}); // ═══════════════════════════════════════════════════════════════════════════ // memory-store: CRUD operations // ═══════════════════════════════════════════════════════════════════════════ -console.log('\n=== memory-store: create and query memories ==='); -{ +test('memory-store: create and query memories', () => { openDatabase(':memory:'); // Create memories const id1 = createMemory({ category: 'gotcha', content: 'esbuild drops .node binaries' }); - assertTrue(id1 !== null, 'createMemory should return an ID'); - assertEq(id1, 'MEM001', 'first memory ID should be MEM001'); + assert.ok(id1 !== null, 'createMemory should return an ID'); + assert.deepStrictEqual(id1, 'MEM001', 'first memory ID should be MEM001'); const id2 = createMemory({ category: 'convention', content: 'use :memory: for tests', confidence: 0.9 }); - assertEq(id2, 'MEM002', 'second memory ID should be MEM002'); + assert.deepStrictEqual(id2, 'MEM002', 'second memory ID should be MEM002'); const id3 = createMemory({ category: 'architecture', content: 'extensions discovered from src/resources/' }); - assertEq(id3, 'MEM003', 'third memory ID should be MEM003'); + assert.deepStrictEqual(id3, 'MEM003', 'third memory ID should be MEM003'); // Query all active const active = getActiveMemories(); - assertEq(active.length, 3, 'should have 3 active memories'); - assertEq(active[0].category, 'gotcha', 'first memory category'); - assertEq(active[0].content, 'esbuild drops .node binaries', 'first memory content'); - assertEq(active[1].confidence, 0.9, 'second memory confidence'); + assert.deepStrictEqual(active.length, 3, 'should have 3 active memories'); + assert.deepStrictEqual(active[0].category, 'gotcha', 'first memory category'); + assert.deepStrictEqual(active[0].content, 'esbuild drops .node binaries', 'first memory content'); + assert.deepStrictEqual(active[1].confidence, 0.9, 'second memory confidence'); closeDatabase(); -} +}); // ═══════════════════════════════════════════════════════════════════════════ // memory-store: update and reinforce // ═══════════════════════════════════════════════════════════════════════════ -console.log('\n=== memory-store: update and reinforce ==='); -{ +test('memory-store: update and reinforce', () => { openDatabase(':memory:'); createMemory({ category: 'gotcha', content: 'original content' }); // Update content const updated = updateMemoryContent('MEM001', 'revised content', 0.95); - assertTrue(updated, 'updateMemoryContent should return true'); + assert.ok(updated, 'updateMemoryContent should return true'); const active = getActiveMemories(); - assertEq(active[0].content, 'revised content', 'content should be updated'); - assertEq(active[0].confidence, 0.95, 'confidence should be updated'); + assert.deepStrictEqual(active[0].content, 'revised content', 'content should be updated'); + assert.deepStrictEqual(active[0].confidence, 0.95, 'confidence should be updated'); // Reinforce const reinforced = reinforceMemory('MEM001'); - assertTrue(reinforced, 'reinforceMemory should return true'); + assert.ok(reinforced, 'reinforceMemory should return true'); const after = getActiveMemories(); - assertEq(after[0].hit_count, 1, 'hit_count should be 1 after reinforce'); + assert.deepStrictEqual(after[0].hit_count, 1, 'hit_count should be 1 after reinforce'); // Reinforce again reinforceMemory('MEM001'); const after2 = getActiveMemories(); - assertEq(after2[0].hit_count, 2, 'hit_count should be 2 after second reinforce'); + assert.deepStrictEqual(after2[0].hit_count, 2, 'hit_count should be 2 after second reinforce'); closeDatabase(); -} +}); // ═══════════════════════════════════════════════════════════════════════════ // memory-store: supersede // ═══════════════════════════════════════════════════════════════════════════ -console.log('\n=== memory-store: supersede ==='); -{ +test('memory-store: supersede', () => { openDatabase(':memory:'); createMemory({ category: 'convention', content: 'old convention' }); @@ -117,18 +112,17 @@ console.log('\n=== memory-store: supersede ==='); supersedeMemory('MEM001', 'MEM002'); const active = getActiveMemories(); - assertEq(active.length, 1, 'should have 1 active memory after supersede'); - assertEq(active[0].id, 'MEM002', 'active memory should be MEM002'); + assert.deepStrictEqual(active.length, 1, 'should have 1 active memory after supersede'); + assert.deepStrictEqual(active[0].id, 'MEM002', 'active memory should be MEM002'); closeDatabase(); -} +}); // ═══════════════════════════════════════════════════════════════════════════ // memory-store: ranked query ordering // ═══════════════════════════════════════════════════════════════════════════ -console.log('\n=== memory-store: ranked query ordering ==='); -{ +test('memory-store: ranked query ordering', () => { openDatabase(':memory:'); // Low confidence, no hits @@ -142,45 +136,43 @@ console.log('\n=== memory-store: ranked query ordering ==='); for (let i = 0; i < 10; i++) reinforceMemory('MEM003'); const ranked = getActiveMemoriesRanked(10); - assertEq(ranked.length, 3, 'should have 3 ranked memories'); + assert.deepStrictEqual(ranked.length, 3, 'should have 3 ranked memories'); // MEM003: 0.7 * (1 + 10*0.1) = 0.7 * 2.0 = 1.4 // MEM002: 0.95 * (1 + 0*0.1) = 0.95 // MEM001: 0.5 * (1 + 0*0.1) = 0.5 - assertEq(ranked[0].id, 'MEM003', 'highest ranked should be MEM003 (reinforced)'); - assertEq(ranked[1].id, 'MEM002', 'second ranked should be MEM002 (high confidence)'); - assertEq(ranked[2].id, 'MEM001', 'lowest ranked should be MEM001'); + assert.deepStrictEqual(ranked[0].id, 'MEM003', 'highest ranked should be MEM003 (reinforced)'); + assert.deepStrictEqual(ranked[1].id, 'MEM002', 'second ranked should be MEM002 (high confidence)'); + assert.deepStrictEqual(ranked[2].id, 'MEM001', 'lowest ranked should be MEM001'); // Test limit const limited = getActiveMemoriesRanked(2); - assertEq(limited.length, 2, 'limit should cap results'); + assert.deepStrictEqual(limited.length, 2, 'limit should cap results'); closeDatabase(); -} +}); // ═══════════════════════════════════════════════════════════════════════════ // memory-store: processed unit tracking // ═══════════════════════════════════════════════════════════════════════════ -console.log('\n=== memory-store: processed unit tracking ==='); -{ +test('memory-store: processed unit tracking', () => { openDatabase(':memory:'); - assertTrue(!isUnitProcessed('execute-task/M001/S01/T01'), 'should not be processed initially'); + assert.ok(!isUnitProcessed('execute-task/M001/S01/T01'), 'should not be processed initially'); markUnitProcessed('execute-task/M001/S01/T01', '/path/to/activity.jsonl'); - assertTrue(isUnitProcessed('execute-task/M001/S01/T01'), 'should be processed after marking'); - assertTrue(!isUnitProcessed('execute-task/M001/S01/T02'), 'different key should not be processed'); + assert.ok(isUnitProcessed('execute-task/M001/S01/T01'), 'should be processed after marking'); + assert.ok(!isUnitProcessed('execute-task/M001/S01/T02'), 'different key should not be processed'); closeDatabase(); -} +}); // ═══════════════════════════════════════════════════════════════════════════ // memory-store: enforce memory cap // ═══════════════════════════════════════════════════════════════════════════ -console.log('\n=== memory-store: enforce memory cap ==='); -{ +test('memory-store: enforce memory cap', () => { openDatabase(':memory:'); // Create 5 memories with varying confidence @@ -194,23 +186,22 @@ console.log('\n=== memory-store: enforce memory cap ==='); enforceMemoryCap(3); const active = getActiveMemories(); - assertEq(active.length, 3, 'should have 3 active memories after cap enforcement'); + assert.deepStrictEqual(active.length, 3, 'should have 3 active memories after cap enforcement'); // The 2 lowest-ranked (MEM003=0.3 and MEM002=0.5) should be superseded const ids = active.map(m => m.id).sort(); - assertTrue(ids.includes('MEM001'), 'MEM001 (0.9) should survive'); - assertTrue(ids.includes('MEM004'), 'MEM004 (0.95) should survive'); - assertTrue(ids.includes('MEM005'), 'MEM005 (0.7) should survive'); + assert.ok(ids.includes('MEM001'), 'MEM001 (0.9) should survive'); + assert.ok(ids.includes('MEM004'), 'MEM004 (0.95) should survive'); + assert.ok(ids.includes('MEM005'), 'MEM005 (0.7) should survive'); closeDatabase(); -} +}); // ═══════════════════════════════════════════════════════════════════════════ // memory-store: applyMemoryActions transaction // ═══════════════════════════════════════════════════════════════════════════ -console.log('\n=== memory-store: applyMemoryActions ==='); -{ +test('memory-store: applyMemoryActions', () => { openDatabase(':memory:'); const actions: MemoryAction[] = [ @@ -221,7 +212,7 @@ console.log('\n=== memory-store: applyMemoryActions ==='); applyMemoryActions(actions, 'execute-task', 'M001/S01/T01'); let active = getActiveMemories(); - assertEq(active.length, 2, 'should have 2 memories after CREATE actions'); + assert.deepStrictEqual(active.length, 2, 'should have 2 memories after CREATE actions'); // Now apply UPDATE + REINFORCE const updateActions: MemoryAction[] = [ @@ -232,8 +223,8 @@ console.log('\n=== memory-store: applyMemoryActions ==='); applyMemoryActions(updateActions, 'execute-task', 'M001/S01/T02'); active = getActiveMemories(); - assertEq(active.find(m => m.id === 'MEM001')?.content, 'updated gotcha', 'MEM001 should be updated'); - assertEq(active.find(m => m.id === 'MEM002')?.hit_count, 1, 'MEM002 should be reinforced'); + assert.deepStrictEqual(active.find(m => m.id === 'MEM001')?.content, 'updated gotcha', 'MEM001 should be updated'); + assert.deepStrictEqual(active.find(m => m.id === 'MEM002')?.hit_count, 1, 'MEM002 should be reinforced'); // SUPERSEDE const supersedeActions: MemoryAction[] = [ @@ -244,19 +235,18 @@ console.log('\n=== memory-store: applyMemoryActions ==='); applyMemoryActions(supersedeActions, 'execute-task', 'M001/S01/T03'); active = getActiveMemories(); - assertEq(active.length, 2, 'should have 2 active after supersede'); - assertTrue(!active.find(m => m.id === 'MEM001'), 'MEM001 should be superseded'); - assertTrue(!!active.find(m => m.id === 'MEM003'), 'MEM003 should be active'); + assert.deepStrictEqual(active.length, 2, 'should have 2 active after supersede'); + assert.ok(!active.find(m => m.id === 'MEM001'), 'MEM001 should be superseded'); + assert.ok(!!active.find(m => m.id === 'MEM003'), 'MEM003 should be active'); closeDatabase(); -} +}); // ═══════════════════════════════════════════════════════════════════════════ // memory-store: formatMemoriesForPrompt // ═══════════════════════════════════════════════════════════════════════════ -console.log('\n=== memory-store: formatMemoriesForPrompt ==='); -{ +test('memory-store: formatMemoriesForPrompt', () => { openDatabase(':memory:'); createMemory({ category: 'gotcha', content: 'esbuild drops .node binaries' }); @@ -267,18 +257,18 @@ console.log('\n=== memory-store: formatMemoriesForPrompt ==='); const memories = getActiveMemoriesRanked(30); const formatted = formatMemoriesForPrompt(memories); - assertTrue(formatted.includes('## Project Memory (auto-learned)'), 'should have header'); - assertTrue(formatted.includes('### Gotcha'), 'should have gotcha category'); - assertTrue(formatted.includes('### Convention'), 'should have convention category'); - assertTrue(formatted.includes('### Architecture'), 'should have architecture category'); - assertTrue(formatted.includes('- esbuild drops .node binaries'), 'should have gotcha content'); - assertTrue(formatted.includes('- use :memory: for tests'), 'should have convention content'); + assert.ok(formatted.includes('## Project Memory (auto-learned)'), 'should have header'); + assert.ok(formatted.includes('### Gotcha'), 'should have gotcha category'); + assert.ok(formatted.includes('### Convention'), 'should have convention category'); + assert.ok(formatted.includes('### Architecture'), 'should have architecture category'); + assert.ok(formatted.includes('- esbuild drops .node binaries'), 'should have gotcha content'); + assert.ok(formatted.includes('- use :memory: for tests'), 'should have convention content'); // Test empty memories closeDatabase(); openDatabase(':memory:'); const emptyFormatted = formatMemoriesForPrompt([]); - assertEq(emptyFormatted, '', 'empty memories should return empty string'); + assert.deepStrictEqual(emptyFormatted, '', 'empty memories should return empty string'); // Test token budget truncation closeDatabase(); @@ -288,58 +278,55 @@ console.log('\n=== memory-store: formatMemoriesForPrompt ==='); } const budgetMemories = getActiveMemoriesRanked(30); const truncated = formatMemoriesForPrompt(budgetMemories, 500); - assertTrue(truncated.length < 2500, `formatted length ${truncated.length} should be under budget`); + assert.ok(truncated.length < 2500, `formatted length ${truncated.length} should be under budget`); closeDatabase(); -} +}); // ═══════════════════════════════════════════════════════════════════════════ // memory-store: ID generation // ═══════════════════════════════════════════════════════════════════════════ -console.log('\n=== memory-store: ID generation ==='); -{ +test('memory-store: ID generation', () => { openDatabase(':memory:'); - assertEq(nextMemoryId(), 'MEM001', 'first ID should be MEM001'); + assert.deepStrictEqual(nextMemoryId(), 'MEM001', 'first ID should be MEM001'); createMemory({ category: 'test', content: 'test' }); - assertEq(nextMemoryId(), 'MEM002', 'after first create, next should be MEM002'); + assert.deepStrictEqual(nextMemoryId(), 'MEM002', 'after first create, next should be MEM002'); // Create several more for (let i = 0; i < 98; i++) createMemory({ category: 'test', content: `test ${i}` }); - assertEq(nextMemoryId(), 'MEM100', 'after 99 creates, next should be MEM100'); + assert.deepStrictEqual(nextMemoryId(), 'MEM100', 'after 99 creates, next should be MEM100'); closeDatabase(); -} +}); // ═══════════════════════════════════════════════════════════════════════════ // memory-store: schema migration (v2 → v3) // ═══════════════════════════════════════════════════════════════════════════ -console.log('\n=== memory-store: schema includes memories table ==='); -{ +test('memory-store: schema includes memories table', () => { openDatabase(':memory:'); const adapter = _getAdapter()!; // Verify memories table exists const memCount = adapter.prepare('SELECT count(*) as cnt FROM memories').get(); - assertEq(memCount?.['cnt'], 0, 'memories table should exist and be empty'); + assert.deepStrictEqual(memCount?.['cnt'], 0, 'memories table should exist and be empty'); // Verify memory_processed_units table exists const procCount = adapter.prepare('SELECT count(*) as cnt FROM memory_processed_units').get(); - assertEq(procCount?.['cnt'], 0, 'memory_processed_units table should exist and be empty'); + assert.deepStrictEqual(procCount?.['cnt'], 0, 'memory_processed_units table should exist and be empty'); // Verify active_memories view exists const viewCount = adapter.prepare('SELECT count(*) as cnt FROM active_memories').get(); - assertEq(viewCount?.['cnt'], 0, 'active_memories view should exist'); + assert.deepStrictEqual(viewCount?.['cnt'], 0, 'active_memories view should exist'); // Verify schema version is 10 (after M001 planning migrations) const version = adapter.prepare('SELECT MAX(version) as v FROM schema_version').get(); - assertEq(version?.['v'], 10, 'schema version should be 10'); + assert.deepStrictEqual(version?.['v'], 10, 'schema version should be 10'); closeDatabase(); -} +}); -report(); diff --git a/src/resources/extensions/gsd/tests/migrate-command.test.ts b/src/resources/extensions/gsd/tests/migrate-command.test.ts index d05cc0619..52473ed66 100644 --- a/src/resources/extensions/gsd/tests/migrate-command.test.ts +++ b/src/resources/extensions/gsd/tests/migrate-command.test.ts @@ -15,9 +15,9 @@ import { writeGSDDirectory, } from '../migrate/index.ts'; import { deriveState } from '../state.ts'; -import { createTestContext } from './test-helpers.ts'; +import { describe, test, beforeEach, afterEach } from 'node:test'; +import assert from 'node:assert/strict'; -const { assertEq, assertTrue, report } = createTestContext(); // ─── Fixture Helpers ─────────────────────────────────────────────────────── const SAMPLE_PROJECT = `# Integration Test Project @@ -195,11 +195,9 @@ function createCompleteFixture(): string { // Tests // ═══════════════════════════════════════════════════════════════════════════ -async function main(): Promise { - // ─── Test 1: Path resolution — .planning appended when missing ───────── - console.log('\n=== Path resolution: .planning appended when source path lacks it ==='); - { + +test('Path resolution: .planning appended when source path lacks it', () => { const base = createCompleteFixture(); try { // Simulate the command's path resolution logic @@ -207,16 +205,16 @@ async function main(): Promise { if (!sourcePath.endsWith('.planning')) { sourcePath = join(sourcePath, '.planning'); } - assertTrue(sourcePath.endsWith('.planning'), 'path-resolution: .planning appended'); - assertTrue(existsSync(sourcePath), 'path-resolution: appended path exists'); + assert.ok(sourcePath.endsWith('.planning'), 'path-resolution: .planning appended'); + assert.ok(existsSync(sourcePath), 'path-resolution: appended path exists'); } finally { rmSync(base, { recursive: true, force: true }); } - } +}); // ─── Test 2: Path resolution — .planning used as-is ──────────────────── - console.log('\n=== Path resolution: .planning used as-is when already present ==='); - { + +test('Path resolution: .planning used as-is when already present', () => { const base = createCompleteFixture(); try { const planningPath = join(base, '.planning'); @@ -224,39 +222,39 @@ async function main(): Promise { if (!sourcePath.endsWith('.planning')) { sourcePath = join(sourcePath, '.planning'); } - assertEq(sourcePath, resolve(planningPath), 'path-resolution: .planning not double-appended'); - assertTrue(existsSync(sourcePath), 'path-resolution: direct path exists'); + assert.deepStrictEqual(sourcePath, resolve(planningPath), 'path-resolution: .planning not double-appended'); + assert.ok(existsSync(sourcePath), 'path-resolution: direct path exists'); } finally { rmSync(base, { recursive: true, force: true }); } - } +}); // ─── Test 3: Validation gating — non-existent path ───────────────────── - console.log('\n=== Validation gating: non-existent path returns invalid ==='); - { + +test('Validation gating: non-existent path returns invalid', async () => { const fakePath = join(tmpdir(), 'gsd-cmd-nonexistent-' + Date.now(), '.planning'); const result = await validatePlanningDirectory(fakePath); - assertEq(result.valid, false, 'validation: non-existent path is invalid'); - assertTrue(result.issues.length > 0, 'validation: has issues for non-existent path'); + assert.deepStrictEqual(result.valid, false, 'validation: non-existent path is invalid'); + assert.ok(result.issues.length > 0, 'validation: has issues for non-existent path'); const hasFatal = result.issues.some(i => i.severity === 'fatal'); - assertTrue(hasFatal, 'validation: non-existent path has fatal issue'); - } + assert.ok(hasFatal, 'validation: non-existent path has fatal issue'); +}); // ─── Test 4: Validation gating — valid fixture passes ────────────────── - console.log('\n=== Validation gating: valid fixture passes validation ==='); - { + +test('Validation gating: valid fixture passes validation', async () => { const base = createCompleteFixture(); try { const result = await validatePlanningDirectory(join(base, '.planning')); - assertTrue(result.valid === true, 'validation: valid fixture passes'); + assert.ok(result.valid === true, 'validation: valid fixture passes'); } finally { rmSync(base, { recursive: true, force: true }); } - } +}); // ─── Test 5: Full pipeline round-trip ────────────────────────────────── - console.log('\n=== Full pipeline: parse → transform → preview → write → deriveState ==='); - { + +test('Full pipeline: parse → transform → preview → write → deriveState', async () => { const base = createCompleteFixture(); const writeTarget = mkdtempSync(join(tmpdir(), 'gsd-cmd-write-')); try { @@ -264,17 +262,17 @@ async function main(): Promise { // (a) Validate const validation = await validatePlanningDirectory(planningPath); - assertTrue(validation.valid === true, 'pipeline: validation passes'); + assert.ok(validation.valid === true, 'pipeline: validation passes'); // (b) Parse const parsed = await parsePlanningDirectory(planningPath); - assertTrue(parsed.roadmap !== null, 'pipeline: roadmap parsed'); - assertTrue(Object.keys(parsed.phases).length >= 2, 'pipeline: phases parsed'); + assert.ok(parsed.roadmap !== null, 'pipeline: roadmap parsed'); + assert.ok(Object.keys(parsed.phases).length >= 2, 'pipeline: phases parsed'); // (c) Transform const project = transformToGSD(parsed); - assertTrue(project.milestones.length >= 1, 'pipeline: has milestones'); - assertTrue(project.milestones[0].slices.length >= 1, 'pipeline: has slices'); + assert.ok(project.milestones.length >= 1, 'pipeline: has milestones'); + assert.ok(project.milestones[0].slices.length >= 1, 'pipeline: has slices'); // Count totals for preview verification let totalTasks = 0; @@ -294,76 +292,69 @@ async function main(): Promise { // (d) Preview — verify counts match project data const preview = generatePreview(project); - assertEq(preview.milestoneCount, project.milestones.length, 'pipeline: preview milestoneCount'); - assertEq(preview.totalSlices, totalSlices, 'pipeline: preview totalSlices'); - assertEq(preview.totalTasks, totalTasks, 'pipeline: preview totalTasks'); - assertEq(preview.doneSlices, doneSlices, 'pipeline: preview doneSlices'); - assertEq(preview.doneTasks, doneTasks, 'pipeline: preview doneTasks'); + assert.deepStrictEqual(preview.milestoneCount, project.milestones.length, 'pipeline: preview milestoneCount'); + assert.deepStrictEqual(preview.totalSlices, totalSlices, 'pipeline: preview totalSlices'); + assert.deepStrictEqual(preview.totalTasks, totalTasks, 'pipeline: preview totalTasks'); + assert.deepStrictEqual(preview.doneSlices, doneSlices, 'pipeline: preview doneSlices'); + assert.deepStrictEqual(preview.doneTasks, doneTasks, 'pipeline: preview doneTasks'); // Completion percentages const expectedSlicePct = totalSlices > 0 ? Math.round((doneSlices / totalSlices) * 100) : 0; const expectedTaskPct = totalTasks > 0 ? Math.round((doneTasks / totalTasks) * 100) : 0; - assertEq(preview.sliceCompletionPct, expectedSlicePct, 'pipeline: preview sliceCompletionPct'); - assertEq(preview.taskCompletionPct, expectedTaskPct, 'pipeline: preview taskCompletionPct'); + assert.deepStrictEqual(preview.sliceCompletionPct, expectedSlicePct, 'pipeline: preview sliceCompletionPct'); + assert.deepStrictEqual(preview.taskCompletionPct, expectedTaskPct, 'pipeline: preview taskCompletionPct'); // Requirements in preview - assertEq(preview.requirements.active, 1, 'pipeline: preview requirements active'); - assertEq(preview.requirements.validated, 1, 'pipeline: preview requirements validated'); - assertEq(preview.requirements.total, 2, 'pipeline: preview requirements total'); + assert.deepStrictEqual(preview.requirements.active, 1, 'pipeline: preview requirements active'); + assert.deepStrictEqual(preview.requirements.validated, 1, 'pipeline: preview requirements validated'); + assert.deepStrictEqual(preview.requirements.total, 2, 'pipeline: preview requirements total'); // (e) Write const result = await writeGSDDirectory(project, writeTarget); - assertTrue(result.paths.length > 0, 'pipeline: files written'); + assert.ok(result.paths.length > 0, 'pipeline: files written'); // Key files exist const gsd = join(writeTarget, '.gsd'); - assertTrue(existsSync(join(gsd, 'PROJECT.md')), 'pipeline: PROJECT.md written'); - assertTrue(existsSync(join(gsd, 'STATE.md')), 'pipeline: STATE.md written'); - assertTrue(existsSync(join(gsd, 'REQUIREMENTS.md')), 'pipeline: REQUIREMENTS.md written'); + assert.ok(existsSync(join(gsd, 'PROJECT.md')), 'pipeline: PROJECT.md written'); + assert.ok(existsSync(join(gsd, 'STATE.md')), 'pipeline: STATE.md written'); + assert.ok(existsSync(join(gsd, 'REQUIREMENTS.md')), 'pipeline: REQUIREMENTS.md written'); const m001 = join(gsd, 'milestones', 'M001'); - assertTrue(existsSync(join(m001, 'M001-ROADMAP.md')), 'pipeline: M001-ROADMAP.md written'); - assertTrue(existsSync(join(m001, 'M001-CONTEXT.md')), 'pipeline: M001-CONTEXT.md written'); + assert.ok(existsSync(join(m001, 'M001-ROADMAP.md')), 'pipeline: M001-ROADMAP.md written'); + assert.ok(existsSync(join(m001, 'M001-CONTEXT.md')), 'pipeline: M001-CONTEXT.md written'); // At least one slice plan exists const s01Plan = join(m001, 'slices', 'S01', 'S01-PLAN.md'); - assertTrue(existsSync(s01Plan), 'pipeline: S01-PLAN.md written'); + assert.ok(existsSync(s01Plan), 'pipeline: S01-PLAN.md written'); // (f) deriveState — coherent state from written output console.log(' --- deriveState ---'); const state = await deriveState(writeTarget); - assertTrue(state.phase !== undefined, 'pipeline: deriveState returns phase'); - assertTrue(state.activeMilestone !== null, 'pipeline: deriveState has activeMilestone'); - assertEq(state.activeMilestone!.id, 'M001', 'pipeline: deriveState activeMilestone is M001'); - assertTrue(state.progress!.slices !== undefined, 'pipeline: deriveState has slices progress'); - assertTrue(state.progress!.tasks !== undefined, 'pipeline: deriveState has tasks progress'); + assert.ok(state.phase !== undefined, 'pipeline: deriveState returns phase'); + assert.ok(state.activeMilestone !== null, 'pipeline: deriveState has activeMilestone'); + assert.deepStrictEqual(state.activeMilestone!.id, 'M001', 'pipeline: deriveState activeMilestone is M001'); + assert.ok(state.progress!.slices !== undefined, 'pipeline: deriveState has slices progress'); + assert.ok(state.progress!.tasks !== undefined, 'pipeline: deriveState has tasks progress'); } finally { rmSync(base, { recursive: true, force: true }); rmSync(writeTarget, { recursive: true, force: true }); } - } +}); // ─── Test 6: .gsd/ exists detection ──────────────────────────────────── - console.log('\n=== .gsd/ exists detection ==='); - { + +test('.gsd/ exists detection', () => { const base = mkdtempSync(join(tmpdir(), 'gsd-cmd-exists-')); try { // No .gsd/ yet - assertTrue(!existsSync(join(base, '.gsd')), 'exists-detection: .gsd absent initially'); + assert.ok(!existsSync(join(base, '.gsd')), 'exists-detection: .gsd absent initially'); // Create .gsd/ mkdirSync(join(base, '.gsd'), { recursive: true }); - assertTrue(existsSync(join(base, '.gsd')), 'exists-detection: .gsd detected after creation'); + assert.ok(existsSync(join(base, '.gsd')), 'exists-detection: .gsd detected after creation'); } finally { rmSync(base, { recursive: true, force: true }); } - } - - report(); -} - -main().catch((err) => { - console.error('Unhandled error:', err); - process.exit(1); }); + diff --git a/src/resources/extensions/gsd/tests/migrate-hierarchy.test.ts b/src/resources/extensions/gsd/tests/migrate-hierarchy.test.ts index 4fa4c960d..27c8f74b8 100644 --- a/src/resources/extensions/gsd/tests/migrate-hierarchy.test.ts +++ b/src/resources/extensions/gsd/tests/migrate-hierarchy.test.ts @@ -18,9 +18,8 @@ import { getActiveTaskFromDb, } from '../gsd-db.ts'; import { migrateHierarchyToDb } from '../md-importer.ts'; -import { createTestContext } from './test-helpers.ts'; - -const { assertEq, assertTrue, report } = createTestContext(); +import { describe, test, beforeEach, afterEach } from 'node:test'; +import assert from 'node:assert/strict'; // ─── Fixture Helpers ─────────────────────────────────────────────────────── @@ -98,11 +97,9 @@ const PLAN_S02_1_TASK = `# S02: Second Slice // Test Cases // ═══════════════════════════════════════════════════════════════════════════ -async function main(): Promise { - // ─── Test (a): Single milestone with 2 slices, 3 tasks ──────────────── - console.log('\n=== migrate-hier: single milestone with 2 slices, 3 tasks ==='); - { + +test('migrate-hier: single milestone with 2 slices, 3 tasks', () => { const base = createFixtureBase(); try { writeFile(base, 'milestones/M001/M001-ROADMAP.md', ROADMAP_2_SLICES); @@ -112,48 +109,48 @@ async function main(): Promise { openDatabase(':memory:'); const counts = migrateHierarchyToDb(base); - assertEq(counts.milestones, 1, 'single-ms: 1 milestone inserted'); - assertEq(counts.slices, 2, 'single-ms: 2 slices inserted'); - assertEq(counts.tasks, 4, 'single-ms: 4 tasks inserted (3 + 1)'); + assert.deepStrictEqual(counts.milestones, 1, 'single-ms: 1 milestone inserted'); + assert.deepStrictEqual(counts.slices, 2, 'single-ms: 2 slices inserted'); + assert.deepStrictEqual(counts.tasks, 4, 'single-ms: 4 tasks inserted (3 + 1)'); const milestones = getAllMilestones(); - assertEq(milestones.length, 1, 'single-ms: 1 milestone in DB'); - assertEq(milestones[0]!.id, 'M001', 'single-ms: milestone ID is M001'); - assertEq(milestones[0]!.title, 'M001: Test Milestone', 'single-ms: milestone title correct'); - assertEq(milestones[0]!.status, 'active', 'single-ms: milestone status is active'); + assert.deepStrictEqual(milestones.length, 1, 'single-ms: 1 milestone in DB'); + assert.deepStrictEqual(milestones[0]!.id, 'M001', 'single-ms: milestone ID is M001'); + assert.deepStrictEqual(milestones[0]!.title, 'M001: Test Milestone', 'single-ms: milestone title correct'); + assert.deepStrictEqual(milestones[0]!.status, 'active', 'single-ms: milestone status is active'); const slices = getMilestoneSlices('M001'); - assertEq(slices.length, 2, 'single-ms: 2 slices in DB'); - assertEq(slices[0]!.id, 'S01', 'single-ms: first slice is S01'); - assertEq(slices[0]!.title, 'First Slice', 'single-ms: S01 title correct'); - assertEq(slices[0]!.risk, 'low', 'single-ms: S01 risk is low'); - assertEq(slices[0]!.status, 'pending', 'single-ms: S01 status is pending'); - assertEq(slices[1]!.id, 'S02', 'single-ms: second slice is S02'); - assertEq(slices[1]!.risk, 'high', 'single-ms: S02 risk is high'); + assert.deepStrictEqual(slices.length, 2, 'single-ms: 2 slices in DB'); + assert.deepStrictEqual(slices[0]!.id, 'S01', 'single-ms: first slice is S01'); + assert.deepStrictEqual(slices[0]!.title, 'First Slice', 'single-ms: S01 title correct'); + assert.deepStrictEqual(slices[0]!.risk, 'low', 'single-ms: S01 risk is low'); + assert.deepStrictEqual(slices[0]!.status, 'pending', 'single-ms: S01 status is pending'); + assert.deepStrictEqual(slices[1]!.id, 'S02', 'single-ms: second slice is S02'); + assert.deepStrictEqual(slices[1]!.risk, 'high', 'single-ms: S02 risk is high'); const s01Tasks = getSliceTasks('M001', 'S01'); - assertEq(s01Tasks.length, 3, 'single-ms: 3 tasks for S01'); - assertEq(s01Tasks[0]!.id, 'T01', 'single-ms: first task is T01'); - assertEq(s01Tasks[0]!.title, 'First Task', 'single-ms: T01 title correct'); - assertEq(s01Tasks[0]!.status, 'pending', 'single-ms: T01 status is pending'); - assertEq(s01Tasks[1]!.id, 'T02', 'single-ms: second task is T02'); - assertEq(s01Tasks[1]!.status, 'complete', 'single-ms: T02 status is complete (was [x])'); - assertEq(s01Tasks[2]!.id, 'T03', 'single-ms: third task is T03'); + assert.deepStrictEqual(s01Tasks.length, 3, 'single-ms: 3 tasks for S01'); + assert.deepStrictEqual(s01Tasks[0]!.id, 'T01', 'single-ms: first task is T01'); + assert.deepStrictEqual(s01Tasks[0]!.title, 'First Task', 'single-ms: T01 title correct'); + assert.deepStrictEqual(s01Tasks[0]!.status, 'pending', 'single-ms: T01 status is pending'); + assert.deepStrictEqual(s01Tasks[1]!.id, 'T02', 'single-ms: second task is T02'); + assert.deepStrictEqual(s01Tasks[1]!.status, 'complete', 'single-ms: T02 status is complete (was [x])'); + assert.deepStrictEqual(s01Tasks[2]!.id, 'T03', 'single-ms: third task is T03'); const s02Tasks = getSliceTasks('M001', 'S02'); - assertEq(s02Tasks.length, 1, 'single-ms: 1 task for S02'); - assertEq(s02Tasks[0]!.id, 'T01', 'single-ms: S02 T01 correct'); + assert.deepStrictEqual(s02Tasks.length, 1, 'single-ms: 1 task for S02'); + assert.deepStrictEqual(s02Tasks[0]!.id, 'T01', 'single-ms: S02 T01 correct'); closeDatabase(); } finally { closeDatabase(); cleanup(base); } - } +}); // ─── Test (b): Multi-milestone — M001 complete, M002 active with deps ─ - console.log('\n=== migrate-hier: multi-milestone with deps ==='); - { + +test('migrate-hier: multi-milestone with deps', () => { const base = createFixtureBase(); try { // M001: complete (has SUMMARY) @@ -197,35 +194,35 @@ Depends on M001 completion. openDatabase(':memory:'); const counts = migrateHierarchyToDb(base); - assertEq(counts.milestones, 2, 'multi-ms: 2 milestones inserted'); + assert.deepStrictEqual(counts.milestones, 2, 'multi-ms: 2 milestones inserted'); const m001 = getMilestone('M001'); - assertTrue(m001 !== null, 'multi-ms: M001 exists'); - assertEq(m001!.status, 'complete', 'multi-ms: M001 is complete'); + assert.ok(m001 !== null, 'multi-ms: M001 exists'); + assert.deepStrictEqual(m001!.status, 'complete', 'multi-ms: M001 is complete'); const m002 = getMilestone('M002'); - assertTrue(m002 !== null, 'multi-ms: M002 exists'); - assertEq(m002!.status, 'active', 'multi-ms: M002 is active'); - assertEq(m002!.depends_on, ['M001'], 'multi-ms: M002 depends on M001'); + assert.ok(m002 !== null, 'multi-ms: M002 exists'); + assert.deepStrictEqual(m002!.status, 'active', 'multi-ms: M002 is active'); + assert.deepStrictEqual(m002!.depends_on, ['M001'], 'multi-ms: M002 depends on M001'); // Active milestone should be M002 const active = getActiveMilestoneFromDb(); - assertEq(active?.id, 'M002', 'multi-ms: active milestone is M002'); + assert.deepStrictEqual(active?.id, 'M002', 'multi-ms: active milestone is M002'); // Active slice in M002 should be S01 (S02 depends on S01) const activeSlice = getActiveSliceFromDb('M002'); - assertEq(activeSlice?.id, 'S01', 'multi-ms: active slice is S01'); + assert.deepStrictEqual(activeSlice?.id, 'S01', 'multi-ms: active slice is S01'); closeDatabase(); } finally { closeDatabase(); cleanup(base); } - } +}); // ─── Test (c): Partially-completed slice — some tasks [x], some [ ] ─── - console.log('\n=== migrate-hier: partially-completed slice ==='); - { + +test('migrate-hier: partially-completed slice', () => { const base = createFixtureBase(); try { const roadmap = `# M001: Partial @@ -260,25 +257,25 @@ Depends on M001 completion. migrateHierarchyToDb(base); const tasks = getSliceTasks('M001', 'S01'); - assertEq(tasks.length, 3, 'partial: 3 tasks'); - assertEq(tasks[0]!.status, 'complete', 'partial: T01 is complete'); - assertEq(tasks[1]!.status, 'complete', 'partial: T02 is complete'); - assertEq(tasks[2]!.status, 'pending', 'partial: T03 is pending'); + assert.deepStrictEqual(tasks.length, 3, 'partial: 3 tasks'); + assert.deepStrictEqual(tasks[0]!.status, 'complete', 'partial: T01 is complete'); + assert.deepStrictEqual(tasks[1]!.status, 'complete', 'partial: T02 is complete'); + assert.deepStrictEqual(tasks[2]!.status, 'pending', 'partial: T03 is pending'); // Active task should be T03 const activeTask = getActiveTaskFromDb('M001', 'S01'); - assertEq(activeTask?.id, 'T03', 'partial: active task is T03'); + assert.deepStrictEqual(activeTask?.id, 'T03', 'partial: active task is T03'); closeDatabase(); } finally { closeDatabase(); cleanup(base); } - } +}); // ─── Test (d): Ghost milestone skipped ──────────────────────────────── - console.log('\n=== migrate-hier: ghost milestone skipped ==='); - { + +test('migrate-hier: ghost milestone skipped', () => { const base = createFixtureBase(); try { // M001: real milestone @@ -289,21 +286,21 @@ Depends on M001 completion. openDatabase(':memory:'); const counts = migrateHierarchyToDb(base); - assertEq(counts.milestones, 1, 'ghost: only 1 milestone inserted'); + assert.deepStrictEqual(counts.milestones, 1, 'ghost: only 1 milestone inserted'); const milestones = getAllMilestones(); - assertEq(milestones.length, 1, 'ghost: 1 milestone in DB'); - assertEq(milestones[0]!.id, 'M001', 'ghost: only M001 in DB'); + assert.deepStrictEqual(milestones.length, 1, 'ghost: 1 milestone in DB'); + assert.deepStrictEqual(milestones[0]!.id, 'M001', 'ghost: only M001 in DB'); closeDatabase(); } finally { closeDatabase(); cleanup(base); } - } +}); // ─── Test (e): Idempotent re-run — calling twice doesn't duplicate ──── - console.log('\n=== migrate-hier: idempotent re-run ==='); - { + +test('migrate-hier: idempotent re-run', () => { const base = createFixtureBase(); try { writeFile(base, 'milestones/M001/M001-ROADMAP.md', ROADMAP_2_SLICES); @@ -313,31 +310,31 @@ Depends on M001 completion. // First run const counts1 = migrateHierarchyToDb(base); - assertEq(counts1.milestones, 1, 'idempotent-1: 1 milestone first run'); - assertEq(counts1.slices, 2, 'idempotent-1: 2 slices first run'); - assertEq(counts1.tasks, 3, 'idempotent-1: 3 tasks first run'); + assert.deepStrictEqual(counts1.milestones, 1, 'idempotent-1: 1 milestone first run'); + assert.deepStrictEqual(counts1.slices, 2, 'idempotent-1: 2 slices first run'); + assert.deepStrictEqual(counts1.tasks, 3, 'idempotent-1: 3 tasks first run'); // Second run — INSERT OR IGNORE means no duplicates const counts2 = migrateHierarchyToDb(base); // Counts reflect attempts, not actual inserts (INSERT OR IGNORE silently skips) // The important thing: DB doesn't have duplicates const milestones = getAllMilestones(); - assertEq(milestones.length, 1, 'idempotent-2: still 1 milestone after second run'); + assert.deepStrictEqual(milestones.length, 1, 'idempotent-2: still 1 milestone after second run'); const slices = getMilestoneSlices('M001'); - assertEq(slices.length, 2, 'idempotent-2: still 2 slices after second run'); + assert.deepStrictEqual(slices.length, 2, 'idempotent-2: still 2 slices after second run'); const tasks = getSliceTasks('M001', 'S01'); - assertEq(tasks.length, 3, 'idempotent-2: still 3 tasks for S01 after second run'); + assert.deepStrictEqual(tasks.length, 3, 'idempotent-2: still 3 tasks for S01 after second run'); closeDatabase(); } finally { closeDatabase(); cleanup(base); } - } +}); // ─── Test (f): Empty roadmap — milestone inserted but no slices ─────── - console.log('\n=== migrate-hier: empty roadmap, no slices ==='); - { + +test('migrate-hier: empty roadmap, no slices', () => { const base = createFixtureBase(); try { const emptyRoadmap = `# M001: Empty Milestone @@ -353,27 +350,27 @@ Depends on M001 completion. openDatabase(':memory:'); const counts = migrateHierarchyToDb(base); - assertEq(counts.milestones, 1, 'empty-roadmap: 1 milestone inserted'); - assertEq(counts.slices, 0, 'empty-roadmap: 0 slices inserted'); - assertEq(counts.tasks, 0, 'empty-roadmap: 0 tasks inserted'); + assert.deepStrictEqual(counts.milestones, 1, 'empty-roadmap: 1 milestone inserted'); + assert.deepStrictEqual(counts.slices, 0, 'empty-roadmap: 0 slices inserted'); + assert.deepStrictEqual(counts.tasks, 0, 'empty-roadmap: 0 tasks inserted'); const milestones = getAllMilestones(); - assertEq(milestones.length, 1, 'empty-roadmap: 1 milestone in DB'); - assertEq(milestones[0]!.title, 'M001: Empty Milestone', 'empty-roadmap: title correct'); + assert.deepStrictEqual(milestones.length, 1, 'empty-roadmap: 1 milestone in DB'); + assert.deepStrictEqual(milestones[0]!.title, 'M001: Empty Milestone', 'empty-roadmap: title correct'); const slices = getMilestoneSlices('M001'); - assertEq(slices.length, 0, 'empty-roadmap: no slices in DB'); + assert.deepStrictEqual(slices.length, 0, 'empty-roadmap: no slices in DB'); closeDatabase(); } finally { closeDatabase(); cleanup(base); } - } +}); // ─── Test (g): Slice depends parsed correctly ───────────────────────── - console.log('\n=== migrate-hier: slice depends parsed ==='); - { + +test('migrate-hier: slice depends parsed', () => { const base = createFixtureBase(); try { const roadmap = `# M001: Deps Test @@ -397,21 +394,21 @@ Depends on M001 completion. migrateHierarchyToDb(base); const slices = getMilestoneSlices('M001'); - assertEq(slices.length, 3, 'depends: 3 slices'); - assertEq(slices[0]!.depends, [], 'depends: S01 has no deps'); - assertEq(slices[1]!.depends, ['S01'], 'depends: S02 depends on S01'); - assertEq(slices[2]!.depends, ['S01', 'S02'], 'depends: S03 depends on S01,S02'); + assert.deepStrictEqual(slices.length, 3, 'depends: 3 slices'); + assert.deepStrictEqual(slices[0]!.depends, [], 'depends: S01 has no deps'); + assert.deepStrictEqual(slices[1]!.depends, ['S01'], 'depends: S02 depends on S01'); + assert.deepStrictEqual(slices[2]!.depends, ['S01', 'S02'], 'depends: S03 depends on S01,S02'); closeDatabase(); } finally { closeDatabase(); cleanup(base); } - } +}); // ─── Test (h): Demo text extracted from roadmap ─────────────────────── - console.log('\n=== migrate-hier: demo text extracted ==='); - { + +test('migrate-hier: demo text extracted', () => { const base = createFixtureBase(); try { writeFile(base, 'milestones/M001/M001-ROADMAP.md', ROADMAP_2_SLICES); @@ -420,20 +417,13 @@ Depends on M001 completion. migrateHierarchyToDb(base); const slices = getMilestoneSlices('M001'); - assertEq(slices[0]!.demo, 'First slice done.', 'demo: S01 demo text correct'); - assertEq(slices[1]!.demo, 'All slices done.', 'demo: S02 demo text correct'); + assert.deepStrictEqual(slices[0]!.demo, 'First slice done.', 'demo: S01 demo text correct'); + assert.deepStrictEqual(slices[1]!.demo, 'All slices done.', 'demo: S02 demo text correct'); closeDatabase(); } finally { closeDatabase(); cleanup(base); } - } - - report(); -} - -main().catch((error) => { - console.error(error); - process.exit(1); }); + diff --git a/src/resources/extensions/gsd/tests/migrate-parser.test.ts b/src/resources/extensions/gsd/tests/migrate-parser.test.ts index c7d051da3..82d425292 100644 --- a/src/resources/extensions/gsd/tests/migrate-parser.test.ts +++ b/src/resources/extensions/gsd/tests/migrate-parser.test.ts @@ -10,9 +10,9 @@ import { parsePlanningDirectory } from '../migrate/parser.ts'; import { validatePlanningDirectory } from '../migrate/validator.ts'; import type { PlanningProject, ValidationResult } from '../migrate/types.ts'; -import { createTestContext } from './test-helpers.ts'; +import { describe, test, beforeEach, afterEach } from 'node:test'; +import assert from 'node:assert/strict'; -const { assertEq, assertTrue, report } = createTestContext(); // ─── Fixture Helpers ─────────────────────────────────────────────────────── function createFixtureBase(): string { @@ -241,11 +241,9 @@ Fixed the login button by correcting the touch event handler. // Test Groups // ═══════════════════════════════════════════════════════════════════════════ -async function main(): Promise { - // ─── Test 1: Complete .planning directory ────────────────────────────── - console.log('\n=== Complete .planning directory with all file types ==='); - { + +test('Complete .planning directory with all file types', async () => { const base = createFixtureBase(); try { const planning = createPlanningDir(base); @@ -313,86 +311,86 @@ Dashboard needs auth to be complete first. const project = await parsePlanningDirectory(planning); // Top-level structure - assertEq(project.path, planning, 'project.path matches'); - assertTrue(project.project !== null, 'PROJECT.md parsed'); - assertTrue(project.roadmap !== null, 'ROADMAP.md parsed'); - assertTrue(project.requirements.length > 0, 'requirements parsed'); - assertTrue(project.state !== null, 'STATE.md parsed'); - assertTrue(project.config !== null, 'config.json parsed'); + assert.deepStrictEqual(project.path, planning, 'project.path matches'); + assert.ok(project.project !== null, 'PROJECT.md parsed'); + assert.ok(project.roadmap !== null, 'ROADMAP.md parsed'); + assert.ok(project.requirements.length > 0, 'requirements parsed'); + assert.ok(project.state !== null, 'STATE.md parsed'); + assert.ok(project.config !== null, 'config.json parsed'); // Phases - assertTrue('29-auth-system' in project.phases, 'phase 29 present'); - assertTrue('30-dashboard' in project.phases, 'phase 30 present'); + assert.ok('29-auth-system' in project.phases, 'phase 29 present'); + assert.ok('30-dashboard' in project.phases, 'phase 30 present'); const phase29 = project.phases['29-auth-system']; - assertEq(phase29?.number, 29, 'phase 29 number'); - assertEq(phase29?.slug, 'auth-system', 'phase 29 slug'); - assertTrue('01' in (phase29?.plans ?? {}), 'phase 29 has plan 01'); - assertTrue('01' in (phase29?.summaries ?? {}), 'phase 29 has summary 01'); - assertTrue((phase29?.research?.length ?? 0) > 0, 'phase 29 has research'); + assert.deepStrictEqual(phase29?.number, 29, 'phase 29 number'); + assert.deepStrictEqual(phase29?.slug, 'auth-system', 'phase 29 slug'); + assert.ok('01' in (phase29?.plans ?? {}), 'phase 29 has plan 01'); + assert.ok('01' in (phase29?.summaries ?? {}), 'phase 29 has summary 01'); + assert.ok((phase29?.research?.length ?? 0) > 0, 'phase 29 has research'); // Plan content (XML-in-markdown) const plan29 = phase29?.plans?.['01']; - assertTrue(plan29 !== undefined, 'plan 29-01 exists'); - assertTrue(plan29?.objective?.includes('authentication') ?? false, 'plan objective extracted'); - assertTrue((plan29?.tasks?.length ?? 0) >= 3, 'plan tasks extracted'); - assertTrue(plan29?.context?.includes('JWT') ?? false, 'plan context extracted'); - assertTrue(plan29?.verification !== '', 'plan verification extracted'); - assertTrue(plan29?.successCriteria !== '', 'plan success criteria extracted'); + assert.ok(plan29 !== undefined, 'plan 29-01 exists'); + assert.ok(plan29?.objective?.includes('authentication') ?? false, 'plan objective extracted'); + assert.ok((plan29?.tasks?.length ?? 0) >= 3, 'plan tasks extracted'); + assert.ok(plan29?.context?.includes('JWT') ?? false, 'plan context extracted'); + assert.ok(plan29?.verification !== '', 'plan verification extracted'); + assert.ok(plan29?.successCriteria !== '', 'plan success criteria extracted'); // Plan frontmatter - assertEq(plan29?.frontmatter?.phase, '29-auth-system', 'plan frontmatter phase'); - assertEq(plan29?.frontmatter?.plan, '01', 'plan frontmatter plan'); - assertEq(plan29?.frontmatter?.type, 'implementation', 'plan frontmatter type'); - assertEq(plan29?.frontmatter?.wave, 1, 'plan frontmatter wave'); - assertEq(plan29?.frontmatter?.autonomous, true, 'plan frontmatter autonomous'); + assert.deepStrictEqual(plan29?.frontmatter?.phase, '29-auth-system', 'plan frontmatter phase'); + assert.deepStrictEqual(plan29?.frontmatter?.plan, '01', 'plan frontmatter plan'); + assert.deepStrictEqual(plan29?.frontmatter?.type, 'implementation', 'plan frontmatter type'); + assert.deepStrictEqual(plan29?.frontmatter?.wave, 1, 'plan frontmatter wave'); + assert.deepStrictEqual(plan29?.frontmatter?.autonomous, true, 'plan frontmatter autonomous'); // Summary content const summary29 = phase29?.summaries?.['01']; - assertTrue(summary29 !== undefined, 'summary 29-01 exists'); - assertEq(summary29?.frontmatter?.phase, '29-auth-system', 'summary frontmatter phase'); - assertEq(summary29?.frontmatter?.plan, '01', 'summary frontmatter plan'); - assertEq(summary29?.frontmatter?.subsystem, 'auth', 'summary frontmatter subsystem'); - assertTrue((summary29?.frontmatter?.tags?.length ?? 0) >= 2, 'summary frontmatter tags'); - assertTrue((summary29?.frontmatter?.provides?.length ?? 0) >= 2, 'summary frontmatter provides'); - assertTrue((summary29?.frontmatter?.affects?.length ?? 0) >= 1, 'summary frontmatter affects'); - assertTrue((summary29?.frontmatter?.['tech-stack']?.length ?? 0) >= 2, 'summary frontmatter tech-stack'); - assertTrue((summary29?.frontmatter?.['key-files']?.length ?? 0) >= 2, 'summary frontmatter key-files'); - assertTrue((summary29?.frontmatter?.['key-decisions']?.length ?? 0) >= 2, 'summary frontmatter key-decisions'); - assertTrue((summary29?.frontmatter?.['patterns-established']?.length ?? 0) >= 1, 'summary frontmatter patterns-established'); - assertEq(summary29?.frontmatter?.duration, '2h', 'summary frontmatter duration'); - assertEq(summary29?.frontmatter?.completed, '2026-01-15', 'summary frontmatter completed'); + assert.ok(summary29 !== undefined, 'summary 29-01 exists'); + assert.deepStrictEqual(summary29?.frontmatter?.phase, '29-auth-system', 'summary frontmatter phase'); + assert.deepStrictEqual(summary29?.frontmatter?.plan, '01', 'summary frontmatter plan'); + assert.deepStrictEqual(summary29?.frontmatter?.subsystem, 'auth', 'summary frontmatter subsystem'); + assert.ok((summary29?.frontmatter?.tags?.length ?? 0) >= 2, 'summary frontmatter tags'); + assert.ok((summary29?.frontmatter?.provides?.length ?? 0) >= 2, 'summary frontmatter provides'); + assert.ok((summary29?.frontmatter?.affects?.length ?? 0) >= 1, 'summary frontmatter affects'); + assert.ok((summary29?.frontmatter?.['tech-stack']?.length ?? 0) >= 2, 'summary frontmatter tech-stack'); + assert.ok((summary29?.frontmatter?.['key-files']?.length ?? 0) >= 2, 'summary frontmatter key-files'); + assert.ok((summary29?.frontmatter?.['key-decisions']?.length ?? 0) >= 2, 'summary frontmatter key-decisions'); + assert.ok((summary29?.frontmatter?.['patterns-established']?.length ?? 0) >= 1, 'summary frontmatter patterns-established'); + assert.deepStrictEqual(summary29?.frontmatter?.duration, '2h', 'summary frontmatter duration'); + assert.deepStrictEqual(summary29?.frontmatter?.completed, '2026-01-15', 'summary frontmatter completed'); // Quick tasks - assertTrue(project.quickTasks.length >= 1, 'quick tasks parsed'); - assertEq(project.quickTasks[0]?.number, 1, 'quick task number'); - assertTrue(project.quickTasks[0]?.plan !== null, 'quick task has plan'); - assertTrue(project.quickTasks[0]?.summary !== null, 'quick task has summary'); + assert.ok(project.quickTasks.length >= 1, 'quick tasks parsed'); + assert.deepStrictEqual(project.quickTasks[0]?.number, 1, 'quick task number'); + assert.ok(project.quickTasks[0]?.plan !== null, 'quick task has plan'); + assert.ok(project.quickTasks[0]?.summary !== null, 'quick task has summary'); // Milestones - assertTrue(project.milestones.length >= 1, 'milestones parsed'); + assert.ok(project.milestones.length >= 1, 'milestones parsed'); // Root research - assertTrue(project.research.length >= 1, 'root research parsed'); + assert.ok(project.research.length >= 1, 'root research parsed'); // Config - assertEq(project.config?.projectName, 'test-project', 'config projectName'); + assert.deepStrictEqual(project.config?.projectName, 'test-project', 'config projectName'); // State - assertTrue(project.state?.currentPhase?.includes('30') ?? false, 'state current phase'); - assertEq(project.state?.status, 'in-progress', 'state status'); + assert.ok(project.state?.currentPhase?.includes('30') ?? false, 'state current phase'); + assert.deepStrictEqual(project.state?.status, 'in-progress', 'state status'); // Validation - assertEq(project.validation.valid, true, 'validation passes for complete dir'); - assertEq(project.validation.issues.length, 0, 'no validation issues'); + assert.deepStrictEqual(project.validation.valid, true, 'validation passes for complete dir'); + assert.deepStrictEqual(project.validation.issues.length, 0, 'no validation issues'); } finally { cleanup(base); } - } +}); // ─── Test 2: Minimal .planning directory (only ROADMAP.md) ───────────── - console.log('\n=== Minimal .planning directory (only ROADMAP.md) ==='); - { + +test('Minimal .planning directory (only ROADMAP.md)', async () => { const base = createFixtureBase(); try { const planning = createPlanningDir(base); @@ -400,42 +398,42 @@ Dashboard needs auth to be complete first. const project = await parsePlanningDirectory(planning); - assertEq(project.project, null, 'minimal: PROJECT.md is null'); - assertTrue(project.roadmap !== null, 'minimal: ROADMAP.md parsed'); - assertEq(project.requirements.length, 0, 'minimal: no requirements'); - assertEq(project.state, null, 'minimal: no state'); - assertEq(project.config, null, 'minimal: no config'); - assertEq(Object.keys(project.phases).length, 0, 'minimal: no phases'); - assertEq(project.quickTasks.length, 0, 'minimal: no quick tasks'); - assertEq(project.milestones.length, 0, 'minimal: no milestones'); - assertEq(project.research.length, 0, 'minimal: no research'); - assertEq(project.validation.valid, true, 'minimal: validation passes'); + assert.deepStrictEqual(project.project, null, 'minimal: PROJECT.md is null'); + assert.ok(project.roadmap !== null, 'minimal: ROADMAP.md parsed'); + assert.deepStrictEqual(project.requirements.length, 0, 'minimal: no requirements'); + assert.deepStrictEqual(project.state, null, 'minimal: no state'); + assert.deepStrictEqual(project.config, null, 'minimal: no config'); + assert.deepStrictEqual(Object.keys(project.phases).length, 0, 'minimal: no phases'); + assert.deepStrictEqual(project.quickTasks.length, 0, 'minimal: no quick tasks'); + assert.deepStrictEqual(project.milestones.length, 0, 'minimal: no milestones'); + assert.deepStrictEqual(project.research.length, 0, 'minimal: no research'); + assert.deepStrictEqual(project.validation.valid, true, 'minimal: validation passes'); } finally { cleanup(base); } - } +}); // ─── Test 3: Missing directory → validation fatal error ──────────────── - console.log('\n=== Missing directory → validation returns fatal error ==='); - { + +test('Missing directory → validation returns fatal error', async () => { const base = createFixtureBase(); try { const result = await validatePlanningDirectory(join(base, 'nonexistent')); - assertEq(result.valid, false, 'missing dir: validation fails'); - assertTrue(result.issues.length > 0, 'missing dir: has issues'); - assertTrue( + assert.deepStrictEqual(result.valid, false, 'missing dir: validation fails'); + assert.ok(result.issues.length > 0, 'missing dir: has issues'); + assert.ok( result.issues.some(i => i.severity === 'fatal'), 'missing dir: has fatal issue' ); } finally { cleanup(base); } - } +}); // ─── Test 4: Duplicate phase numbers ─────────────────────────────────── - console.log('\n=== Phase directory with duplicate numbers ==='); - { + +test('Phase directory with duplicate numbers', async () => { const base = createFixtureBase(); try { const planning = createPlanningDir(base); @@ -456,18 +454,18 @@ Dashboard needs auth to be complete first. const project = await parsePlanningDirectory(planning); - assertTrue('45-core-infrastructure' in project.phases, 'dup nums: core-infrastructure phase present'); - assertTrue('45-logging-config' in project.phases, 'dup nums: logging-config phase present'); - assertEq(project.phases['45-core-infrastructure']?.number, 45, 'dup nums: both have number 45 (a)'); - assertEq(project.phases['45-logging-config']?.number, 45, 'dup nums: both have number 45 (b)'); + assert.ok('45-core-infrastructure' in project.phases, 'dup nums: core-infrastructure phase present'); + assert.ok('45-logging-config' in project.phases, 'dup nums: logging-config phase present'); + assert.deepStrictEqual(project.phases['45-core-infrastructure']?.number, 45, 'dup nums: both have number 45 (a)'); + assert.deepStrictEqual(project.phases['45-logging-config']?.number, 45, 'dup nums: both have number 45 (b)'); } finally { cleanup(base); } - } +}); // ─── Test 5: XML-in-markdown plan parsing ────────────────────────────── - console.log('\n=== Plan file with XML-in-markdown ==='); - { + +test('Plan file with XML-in-markdown', async () => { const base = createFixtureBase(); try { const planning = createPlanningDir(base); @@ -480,21 +478,21 @@ Dashboard needs auth to be complete first. const project = await parsePlanningDirectory(planning); const plan = project.phases['29-auth-system']?.plans?.['01']; - assertTrue(plan !== undefined, 'xml plan: plan exists'); - assertTrue(plan?.objective?.includes('authentication') ?? false, 'xml plan: objective extracted'); - assertTrue((plan?.tasks?.length ?? 0) === 3, 'xml plan: 3 tasks extracted'); - assertTrue(plan?.tasks?.[0]?.includes('auth middleware') ?? false, 'xml plan: first task content'); - assertTrue(plan?.context?.includes('JWT') ?? false, 'xml plan: context extracted'); - assertTrue(plan?.verification?.includes('Login returns') ?? false, 'xml plan: verification extracted'); - assertTrue(plan?.successCriteria?.includes('endpoints respond') ?? false, 'xml plan: success criteria extracted'); + assert.ok(plan !== undefined, 'xml plan: plan exists'); + assert.ok(plan?.objective?.includes('authentication') ?? false, 'xml plan: objective extracted'); + assert.ok((plan?.tasks?.length ?? 0) === 3, 'xml plan: 3 tasks extracted'); + assert.ok(plan?.tasks?.[0]?.includes('auth middleware') ?? false, 'xml plan: first task content'); + assert.ok(plan?.context?.includes('JWT') ?? false, 'xml plan: context extracted'); + assert.ok(plan?.verification?.includes('Login returns') ?? false, 'xml plan: verification extracted'); + assert.ok(plan?.successCriteria?.includes('endpoints respond') ?? false, 'xml plan: success criteria extracted'); } finally { cleanup(base); } - } +}); // ─── Test 6: Summary file with YAML frontmatter ─────────────────────── - console.log('\n=== Summary file with YAML frontmatter ==='); - { + +test('Summary file with YAML frontmatter', async () => { const base = createFixtureBase(); try { const planning = createPlanningDir(base); @@ -507,27 +505,27 @@ Dashboard needs auth to be complete first. const project = await parsePlanningDirectory(planning); const summary = project.phases['29-auth-system']?.summaries?.['01']; - assertTrue(summary !== undefined, 'summary fm: summary exists'); - assertEq(summary?.frontmatter?.phase, '29-auth-system', 'summary fm: phase'); - assertEq(summary?.frontmatter?.plan, '01', 'summary fm: plan'); - assertEq(summary?.frontmatter?.subsystem, 'auth', 'summary fm: subsystem'); - assertEq(summary?.frontmatter?.tags, ['authentication', 'security'], 'summary fm: tags'); - assertEq(summary?.frontmatter?.provides, ['auth-middleware', 'jwt-validation'], 'summary fm: provides'); - assertEq(summary?.frontmatter?.affects, ['api-routes'], 'summary fm: affects'); - assertEq(summary?.frontmatter?.['tech-stack'], ['jsonwebtoken', 'express'], 'summary fm: tech-stack'); - assertEq(summary?.frontmatter?.['key-files'], ['src/auth.ts', 'src/middleware/auth.ts'], 'summary fm: key-files'); - assertEq(summary?.frontmatter?.['key-decisions'], ['Use RS256 for JWT signing', 'Store refresh tokens in DB'], 'summary fm: key-decisions'); - assertEq(summary?.frontmatter?.['patterns-established'], ['Middleware-based auth'], 'summary fm: patterns-established'); - assertEq(summary?.frontmatter?.duration, '2h', 'summary fm: duration'); - assertEq(summary?.frontmatter?.completed, '2026-01-15', 'summary fm: completed'); + assert.ok(summary !== undefined, 'summary fm: summary exists'); + assert.deepStrictEqual(summary?.frontmatter?.phase, '29-auth-system', 'summary fm: phase'); + assert.deepStrictEqual(summary?.frontmatter?.plan, '01', 'summary fm: plan'); + assert.deepStrictEqual(summary?.frontmatter?.subsystem, 'auth', 'summary fm: subsystem'); + assert.deepStrictEqual(summary?.frontmatter?.tags, ['authentication', 'security'], 'summary fm: tags'); + assert.deepStrictEqual(summary?.frontmatter?.provides, ['auth-middleware', 'jwt-validation'], 'summary fm: provides'); + assert.deepStrictEqual(summary?.frontmatter?.affects, ['api-routes'], 'summary fm: affects'); + assert.deepStrictEqual(summary?.frontmatter?.['tech-stack'], ['jsonwebtoken', 'express'], 'summary fm: tech-stack'); + assert.deepStrictEqual(summary?.frontmatter?.['key-files'], ['src/auth.ts', 'src/middleware/auth.ts'], 'summary fm: key-files'); + assert.deepStrictEqual(summary?.frontmatter?.['key-decisions'], ['Use RS256 for JWT signing', 'Store refresh tokens in DB'], 'summary fm: key-decisions'); + assert.deepStrictEqual(summary?.frontmatter?.['patterns-established'], ['Middleware-based auth'], 'summary fm: patterns-established'); + assert.deepStrictEqual(summary?.frontmatter?.duration, '2h', 'summary fm: duration'); + assert.deepStrictEqual(summary?.frontmatter?.completed, '2026-01-15', 'summary fm: completed'); } finally { cleanup(base); } - } +}); // ─── Test 7: Orphan summaries (no matching plan) ────────────────────── - console.log('\n=== Orphan summaries (no matching plan) ==='); - { + +test('Orphan summaries (no matching plan)', async () => { const base = createFixtureBase(); try { const planning = createPlanningDir(base); @@ -561,19 +559,19 @@ Another orphan. const project = await parsePlanningDirectory(planning); const phase = project.phases['45-logging-config']; - assertTrue(phase !== undefined, 'orphan: phase exists'); - assertEq(Object.keys(phase?.plans ?? {}).length, 0, 'orphan: no plans'); - assertTrue(Object.keys(phase?.summaries ?? {}).length >= 2, 'orphan: summaries preserved'); - assertTrue('04' in (phase?.summaries ?? {}), 'orphan: summary 04 present'); - assertTrue('05' in (phase?.summaries ?? {}), 'orphan: summary 05 present'); + assert.ok(phase !== undefined, 'orphan: phase exists'); + assert.deepStrictEqual(Object.keys(phase?.plans ?? {}).length, 0, 'orphan: no plans'); + assert.ok(Object.keys(phase?.summaries ?? {}).length >= 2, 'orphan: summaries preserved'); + assert.ok('04' in (phase?.summaries ?? {}), 'orphan: summary 04 present'); + assert.ok('05' in (phase?.summaries ?? {}), 'orphan: summary 05 present'); } finally { cleanup(base); } - } +}); // ─── Test 8: .archive/ directory skipped ────────────────────────────── - console.log('\n=== .archive/ directory → skipped by default ==='); - { + +test('.archive/ directory → skipped by default', async () => { const base = createFixtureBase(); try { const planning = createPlanningDir(base); @@ -591,17 +589,17 @@ Another orphan. const project = await parsePlanningDirectory(planning); - assertTrue('29-auth-system' in project.phases, 'archive: normal phase present'); + assert.ok('29-auth-system' in project.phases, 'archive: normal phase present'); // Archive phases should not appear in the phases map - assertTrue(!Object.keys(project.phases).some(k => k.includes('old-auth')), 'archive: archived phase not present'); + assert.ok(!Object.keys(project.phases).some(k => k.includes('old-auth')), 'archive: archived phase not present'); } finally { cleanup(base); } - } +}); // ─── Test 9: Quick tasks ────────────────────────────────────────────── - console.log('\n=== Quick tasks parsed ==='); - { + +test('Quick tasks parsed', async () => { const base = createFixtureBase(); try { const planning = createPlanningDir(base); @@ -620,22 +618,22 @@ Another orphan. const project = await parsePlanningDirectory(planning); - assertEq(project.quickTasks.length, 2, 'quick: 2 quick tasks'); - assertEq(project.quickTasks[0]?.number, 1, 'quick: first task number'); - assertEq(project.quickTasks[0]?.slug, 'fix-login', 'quick: first task slug'); - assertTrue(project.quickTasks[0]?.plan !== null, 'quick: first task has plan'); - assertTrue(project.quickTasks[0]?.summary !== null, 'quick: first task has summary'); - assertEq(project.quickTasks[1]?.number, 2, 'quick: second task number'); - assertTrue(project.quickTasks[1]?.plan !== null, 'quick: second task has plan'); - assertEq(project.quickTasks[1]?.summary, null, 'quick: second task has no summary'); + assert.deepStrictEqual(project.quickTasks.length, 2, 'quick: 2 quick tasks'); + assert.deepStrictEqual(project.quickTasks[0]?.number, 1, 'quick: first task number'); + assert.deepStrictEqual(project.quickTasks[0]?.slug, 'fix-login', 'quick: first task slug'); + assert.ok(project.quickTasks[0]?.plan !== null, 'quick: first task has plan'); + assert.ok(project.quickTasks[0]?.summary !== null, 'quick: first task has summary'); + assert.deepStrictEqual(project.quickTasks[1]?.number, 2, 'quick: second task number'); + assert.ok(project.quickTasks[1]?.plan !== null, 'quick: second task has plan'); + assert.deepStrictEqual(project.quickTasks[1]?.summary, null, 'quick: second task has no summary'); } finally { cleanup(base); } - } +}); // ─── Test 10: Roadmap with milestone sections and
──────────── - console.log('\n=== Roadmap with milestone sections and
blocks ==='); - { + +test('Roadmap with milestone sections and
blocks', async () => { const base = createFixtureBase(); try { const planning = createPlanningDir(base); @@ -643,35 +641,35 @@ Another orphan. const project = await parsePlanningDirectory(planning); - assertTrue(project.roadmap !== null, 'ms roadmap: roadmap parsed'); - assertTrue((project.roadmap?.milestones?.length ?? 0) >= 2, 'ms roadmap: has milestone sections'); + assert.ok(project.roadmap !== null, 'ms roadmap: roadmap parsed'); + assert.ok((project.roadmap?.milestones?.length ?? 0) >= 2, 'ms roadmap: has milestone sections'); // Check collapsed milestone const v20 = project.roadmap?.milestones?.find(m => m.id.includes('2.0')); - assertTrue(v20 !== undefined, 'ms roadmap: v2.0 milestone found'); - assertEq(v20?.collapsed, true, 'ms roadmap: v2.0 is collapsed'); - assertTrue((v20?.phases?.length ?? 0) >= 2, 'ms roadmap: v2.0 has phases'); - assertTrue(v20?.phases?.every(p => p.done) ?? false, 'ms roadmap: v2.0 phases all done'); + assert.ok(v20 !== undefined, 'ms roadmap: v2.0 milestone found'); + assert.deepStrictEqual(v20?.collapsed, true, 'ms roadmap: v2.0 is collapsed'); + assert.ok((v20?.phases?.length ?? 0) >= 2, 'ms roadmap: v2.0 has phases'); + assert.ok(v20?.phases?.every(p => p.done) ?? false, 'ms roadmap: v2.0 phases all done'); // Check active milestone const v25 = project.roadmap?.milestones?.find(m => m.id.includes('2.5')); - assertTrue(v25 !== undefined, 'ms roadmap: v2.5 milestone found'); - assertEq(v25?.collapsed, false, 'ms roadmap: v2.5 is not collapsed'); - assertTrue((v25?.phases?.length ?? 0) >= 3, 'ms roadmap: v2.5 has phases'); + assert.ok(v25 !== undefined, 'ms roadmap: v2.5 milestone found'); + assert.deepStrictEqual(v25?.collapsed, false, 'ms roadmap: v2.5 is not collapsed'); + assert.ok((v25?.phases?.length ?? 0) >= 3, 'ms roadmap: v2.5 has phases'); // Check completion state const phase29 = v25?.phases?.find(p => p.number === 29); - assertTrue(phase29?.done === true, 'ms roadmap: phase 29 is done'); + assert.ok(phase29?.done === true, 'ms roadmap: phase 29 is done'); const phase30 = v25?.phases?.find(p => p.number === 30); - assertTrue(phase30?.done === false, 'ms roadmap: phase 30 is not done'); + assert.ok(phase30?.done === false, 'ms roadmap: phase 30 is not done'); } finally { cleanup(base); } - } +}); // ─── Test 11: Non-standard phase files → extra files ────────────────── - console.log('\n=== Non-standard phase files → collected as extra files ==='); - { + +test('Non-standard phase files → collected as extra files', async () => { const base = createFixtureBase(); try { const planning = createPlanningDir(base); @@ -687,28 +685,28 @@ Another orphan. const project = await parsePlanningDirectory(planning); const phase = project.phases['36-attachment-system']; - assertTrue(phase !== undefined, 'extra: phase exists'); - assertTrue((phase?.extraFiles?.length ?? 0) >= 3, 'extra: non-standard files collected'); - assertTrue( + assert.ok(phase !== undefined, 'extra: phase exists'); + assert.ok((phase?.extraFiles?.length ?? 0) >= 3, 'extra: non-standard files collected'); + assert.ok( phase?.extraFiles?.some(f => f.fileName === 'BASELINE.md') ?? false, 'extra: BASELINE.md collected' ); - assertTrue( + assert.ok( phase?.extraFiles?.some(f => f.fileName === 'BUNDLE-ANALYSIS.md') ?? false, 'extra: BUNDLE-ANALYSIS.md collected' ); - assertTrue( + assert.ok( phase?.extraFiles?.some(f => f.fileName === 'depcheck-results.txt') ?? false, 'extra: depcheck-results.txt collected' ); } finally { cleanup(base); } - } +}); // ─── Test 12: Validation — missing ROADMAP.md → warning (not fatal) ─── - console.log('\n=== Validation: missing ROADMAP.md → warning (not fatal) ==='); - { + +test('Validation: missing ROADMAP.md → warning (not fatal)', async () => { const base = createFixtureBase(); try { const planning = createPlanningDir(base); @@ -717,19 +715,19 @@ Another orphan. const result = await validatePlanningDirectory(planning); - assertEq(result.valid, true, 'no roadmap: validation still passes'); - assertTrue( + assert.deepStrictEqual(result.valid, true, 'no roadmap: validation still passes'); + assert.ok( result.issues.some(i => i.severity === 'warning' && i.file.includes('ROADMAP')), 'no roadmap: warning issue mentions ROADMAP' ); } finally { cleanup(base); } - } +}); // ─── Test 13: Validation — missing PROJECT.md → warning ─────────────── - console.log('\n=== Validation: missing PROJECT.md → warning ==='); - { + +test('Validation: missing PROJECT.md → warning', async () => { const base = createFixtureBase(); try { const planning = createPlanningDir(base); @@ -738,20 +736,13 @@ Another orphan. const result = await validatePlanningDirectory(planning); - assertEq(result.valid, true, 'no project: validation passes (warning only)'); - assertTrue( + assert.deepStrictEqual(result.valid, true, 'no project: validation passes (warning only)'); + assert.ok( result.issues.some(i => i.severity === 'warning' && i.file.includes('PROJECT')), 'no project: warning issue mentions PROJECT' ); } finally { cleanup(base); } - } - - report(); -} - -main().catch((error) => { - console.error(error); - process.exit(1); }); + diff --git a/src/resources/extensions/gsd/tests/migrate-transformer.test.ts b/src/resources/extensions/gsd/tests/migrate-transformer.test.ts index 618856288..378992772 100644 --- a/src/resources/extensions/gsd/tests/migrate-transformer.test.ts +++ b/src/resources/extensions/gsd/tests/migrate-transformer.test.ts @@ -19,9 +19,9 @@ import type { GSDSlice, GSDTask, } from '../migrate/types.ts'; -import { createTestContext } from './test-helpers.ts'; +import { describe, test, beforeEach, afterEach } from 'node:test'; +import assert from 'node:assert/strict'; -const { assertEq, assertTrue, report } = createTestContext(); // ─── Fixture Helpers ─────────────────────────────────────────────────────── function emptyProject(overrides: Partial = {}): PlanningProject { @@ -134,8 +134,7 @@ function makeResearch(fileName: string, content: string): PlanningResearch { // ─── Scenario 1: Flat Single-Milestone (3 phases → M001 with S01/S02/S03) ── -{ - console.log('Scenario 1: Flat single-milestone'); +test('Scenario 1: Flat single-milestone', () => { const project = emptyProject({ project: '# My Project\nA cool project.', @@ -159,26 +158,25 @@ function makeResearch(fileName: string, content: string): PlanningResearch { const result = transformToGSD(project); - assertEq(result.milestones.length, 1, 'flat: produces 1 milestone'); - assertTrue(result.milestones[0]?.id === 'M001', 'flat: milestone ID is M001'); - assertEq(result.milestones[0]?.slices.length, 3, 'flat: 3 slices'); - assertEq(result.milestones[0]?.slices[0]?.id, 'S01', 'flat: first slice is S01'); - assertEq(result.milestones[0]?.slices[1]?.id, 'S02', 'flat: second slice is S02'); - assertEq(result.milestones[0]?.slices[2]?.id, 'S03', 'flat: third slice is S03'); - assertTrue(result.milestones[0]?.slices[0]?.title.length > 0, 'flat: slice title not empty'); - assertEq(result.milestones[0]?.slices[0]?.tasks.length, 1, 'flat: S01 has 1 task'); - assertEq(result.milestones[0]?.slices[1]?.tasks.length, 2, 'flat: S02 has 2 tasks'); - assertEq(result.milestones[0]?.slices[2]?.tasks.length, 1, 'flat: S03 has 1 task'); - assertEq(result.milestones[0]?.slices[0]?.tasks[0]?.id, 'T01', 'flat: first task is T01'); - assertEq(result.milestones[0]?.slices[1]?.tasks[1]?.id, 'T02', 'flat: second task in S02 is T02'); - assertTrue(result.projectContent.includes('My Project'), 'flat: projectContent preserved'); - assertEq(result.milestones[0]?.boundaryMap, [], 'flat: boundaryMap defaults to empty'); -} + assert.deepStrictEqual(result.milestones.length, 1, 'flat: produces 1 milestone'); + assert.ok(result.milestones[0]?.id === 'M001', 'flat: milestone ID is M001'); + assert.deepStrictEqual(result.milestones[0]?.slices.length, 3, 'flat: 3 slices'); + assert.deepStrictEqual(result.milestones[0]?.slices[0]?.id, 'S01', 'flat: first slice is S01'); + assert.deepStrictEqual(result.milestones[0]?.slices[1]?.id, 'S02', 'flat: second slice is S02'); + assert.deepStrictEqual(result.milestones[0]?.slices[2]?.id, 'S03', 'flat: third slice is S03'); + assert.ok(result.milestones[0]?.slices[0]?.title.length > 0, 'flat: slice title not empty'); + assert.deepStrictEqual(result.milestones[0]?.slices[0]?.tasks.length, 1, 'flat: S01 has 1 task'); + assert.deepStrictEqual(result.milestones[0]?.slices[1]?.tasks.length, 2, 'flat: S02 has 2 tasks'); + assert.deepStrictEqual(result.milestones[0]?.slices[2]?.tasks.length, 1, 'flat: S03 has 1 task'); + assert.deepStrictEqual(result.milestones[0]?.slices[0]?.tasks[0]?.id, 'T01', 'flat: first task is T01'); + assert.deepStrictEqual(result.milestones[0]?.slices[1]?.tasks[1]?.id, 'T02', 'flat: second task in S02 is T02'); + assert.ok(result.projectContent.includes('My Project'), 'flat: projectContent preserved'); + assert.deepStrictEqual(result.milestones[0]?.boundaryMap, [], 'flat: boundaryMap defaults to empty'); +}); // ─── Scenario 2: Multi-Milestone (2 milestones with independent numbering) ── -{ - console.log('Scenario 2: Multi-milestone'); +test('Scenario 2: Multi-milestone', () => { const project = emptyProject({ roadmap: milestoneRoadmap([ @@ -206,23 +204,22 @@ function makeResearch(fileName: string, content: string): PlanningResearch { const result = transformToGSD(project); - assertEq(result.milestones.length, 2, 'multi: 2 milestones'); - assertEq(result.milestones[0]?.id, 'M001', 'multi: first milestone M001'); - assertEq(result.milestones[1]?.id, 'M002', 'multi: second milestone M002'); - assertEq(result.milestones[0]?.slices.length, 2, 'multi: M001 has 2 slices'); - assertEq(result.milestones[1]?.slices.length, 3, 'multi: M002 has 3 slices'); + assert.deepStrictEqual(result.milestones.length, 2, 'multi: 2 milestones'); + assert.deepStrictEqual(result.milestones[0]?.id, 'M001', 'multi: first milestone M001'); + assert.deepStrictEqual(result.milestones[1]?.id, 'M002', 'multi: second milestone M002'); + assert.deepStrictEqual(result.milestones[0]?.slices.length, 2, 'multi: M001 has 2 slices'); + assert.deepStrictEqual(result.milestones[1]?.slices.length, 3, 'multi: M002 has 3 slices'); // Independent numbering: both start at S01 - assertEq(result.milestones[0]?.slices[0]?.id, 'S01', 'multi: M001 starts at S01'); - assertEq(result.milestones[1]?.slices[0]?.id, 'S01', 'multi: M002 starts at S01'); - assertEq(result.milestones[1]?.slices[2]?.id, 'S03', 'multi: M002 third slice is S03'); - assertTrue(result.milestones[0]?.title.length > 0, 'multi: M001 has title'); - assertTrue(result.milestones[1]?.title.length > 0, 'multi: M002 has title'); -} + assert.deepStrictEqual(result.milestones[0]?.slices[0]?.id, 'S01', 'multi: M001 starts at S01'); + assert.deepStrictEqual(result.milestones[1]?.slices[0]?.id, 'S01', 'multi: M002 starts at S01'); + assert.deepStrictEqual(result.milestones[1]?.slices[2]?.id, 'S03', 'multi: M002 third slice is S03'); + assert.ok(result.milestones[0]?.title.length > 0, 'multi: M001 has title'); + assert.ok(result.milestones[1]?.title.length > 0, 'multi: M002 has title'); +}); // ─── Scenario 3: Decimal Phase Ordering (1, 2, 2.1, 2.2, 3 → S01–S05) ── -{ - console.log('Scenario 3: Decimal phase ordering'); +test('Scenario 3: Decimal phase ordering', () => { const project = emptyProject({ roadmap: flatRoadmap([ @@ -243,27 +240,26 @@ function makeResearch(fileName: string, content: string): PlanningResearch { const result = transformToGSD(project); - assertEq(result.milestones[0]?.slices.length, 5, 'decimal: 5 slices total'); - assertEq(result.milestones[0]?.slices[0]?.id, 'S01', 'decimal: first is S01'); - assertEq(result.milestones[0]?.slices[1]?.id, 'S02', 'decimal: second is S02'); - assertEq(result.milestones[0]?.slices[2]?.id, 'S03', 'decimal: third is S03'); - assertEq(result.milestones[0]?.slices[3]?.id, 'S04', 'decimal: fourth is S04'); - assertEq(result.milestones[0]?.slices[4]?.id, 'S05', 'decimal: fifth is S05'); + assert.deepStrictEqual(result.milestones[0]?.slices.length, 5, 'decimal: 5 slices total'); + assert.deepStrictEqual(result.milestones[0]?.slices[0]?.id, 'S01', 'decimal: first is S01'); + assert.deepStrictEqual(result.milestones[0]?.slices[1]?.id, 'S02', 'decimal: second is S02'); + assert.deepStrictEqual(result.milestones[0]?.slices[2]?.id, 'S03', 'decimal: third is S03'); + assert.deepStrictEqual(result.milestones[0]?.slices[3]?.id, 'S04', 'decimal: fourth is S04'); + assert.deepStrictEqual(result.milestones[0]?.slices[4]?.id, 'S05', 'decimal: fifth is S05'); // Order must be by float value: 1, 2, 2.1, 2.2, 3 - assertTrue( + assert.ok( result.milestones[0]?.slices[0]?.title.toLowerCase().includes('foundation'), 'decimal: S01 is foundation (phase 1)', ); - assertTrue( + assert.ok( result.milestones[0]?.slices[4]?.title.toLowerCase().includes('finalize'), 'decimal: S05 is finalize (phase 3)', ); -} +}); // ─── Scenario 4: Completion State ────────────────────────────────────────── -{ - console.log('Scenario 4: Completion state mapping'); +test('Scenario 4: Completion state mapping', () => { const project = emptyProject({ roadmap: flatRoadmap([ @@ -288,26 +284,25 @@ function makeResearch(fileName: string, content: string): PlanningResearch { const doneSlice = result.milestones[0]?.slices[0]; const activeSlice = result.milestones[0]?.slices[1]; - assertTrue(doneSlice?.done === true, 'completion: done phase → done slice'); - assertTrue(activeSlice?.done === false, 'completion: active phase → not-done slice'); - assertTrue(doneSlice?.tasks[0]?.done === true, 'completion: plan with summary → done task'); - assertTrue(doneSlice?.tasks[1]?.done === false, 'completion: plan without summary → not-done task'); - assertTrue(doneSlice?.tasks[0]?.summary !== null, 'completion: done task has summary data'); - assertTrue(doneSlice?.tasks[1]?.summary === null, 'completion: not-done task has null summary'); - assertEq(doneSlice?.tasks[0]?.summary?.completedAt, '2026-01-15', 'completion: summary completedAt from frontmatter'); - assertEq(doneSlice?.tasks[0]?.summary?.duration, '2h', 'completion: summary duration from frontmatter'); - assertEq(doneSlice?.tasks[0]?.summary?.provides, ['feature-01'], 'completion: summary provides from frontmatter'); - assertEq(doneSlice?.tasks[0]?.summary?.keyFiles, ['file-01.ts'], 'completion: summary keyFiles from frontmatter'); - assertTrue(doneSlice?.tasks[0]?.summary?.whatHappened?.includes('Summary body') ?? false, 'completion: summary whatHappened from body'); - assertTrue(doneSlice?.summary !== null, 'completion: done slice has slice summary'); - assertTrue(activeSlice?.summary === null, 'completion: active slice has null summary'); - assertEq(doneSlice?.tasks[0]?.estimate, '2h', 'completion: task estimate from summary duration'); -} + assert.ok(doneSlice?.done === true, 'completion: done phase → done slice'); + assert.ok(activeSlice?.done === false, 'completion: active phase → not-done slice'); + assert.ok(doneSlice?.tasks[0]?.done === true, 'completion: plan with summary → done task'); + assert.ok(doneSlice?.tasks[1]?.done === false, 'completion: plan without summary → not-done task'); + assert.ok(doneSlice?.tasks[0]?.summary !== null, 'completion: done task has summary data'); + assert.ok(doneSlice?.tasks[1]?.summary === null, 'completion: not-done task has null summary'); + assert.deepStrictEqual(doneSlice?.tasks[0]?.summary?.completedAt, '2026-01-15', 'completion: summary completedAt from frontmatter'); + assert.deepStrictEqual(doneSlice?.tasks[0]?.summary?.duration, '2h', 'completion: summary duration from frontmatter'); + assert.deepStrictEqual(doneSlice?.tasks[0]?.summary?.provides, ['feature-01'], 'completion: summary provides from frontmatter'); + assert.deepStrictEqual(doneSlice?.tasks[0]?.summary?.keyFiles, ['file-01.ts'], 'completion: summary keyFiles from frontmatter'); + assert.ok(doneSlice?.tasks[0]?.summary?.whatHappened?.includes('Summary body') ?? false, 'completion: summary whatHappened from body'); + assert.ok(doneSlice?.summary !== null, 'completion: done slice has slice summary'); + assert.ok(activeSlice?.summary === null, 'completion: active slice has null summary'); + assert.deepStrictEqual(doneSlice?.tasks[0]?.estimate, '2h', 'completion: task estimate from summary duration'); +}); // ─── Scenario 5: Research Consolidation ──────────────────────────────────── -{ - console.log('Scenario 5: Research consolidation'); +test('Scenario 5: Research consolidation', () => { const project = emptyProject({ roadmap: flatRoadmap([roadmapEntry(1, 'researched-phase')]), @@ -328,28 +323,27 @@ function makeResearch(fileName: string, content: string): PlanningResearch { const result = transformToGSD(project); // Project-level research → milestone research - assertTrue(result.milestones[0]?.research !== null, 'research: milestone has consolidated research'); - assertTrue(result.milestones[0]?.research!.includes('Project Summary'), 'research: includes SUMMARY content'); - assertTrue(result.milestones[0]?.research!.includes('Architecture'), 'research: includes ARCHITECTURE content'); - assertTrue(result.milestones[0]?.research!.includes('Pitfalls'), 'research: includes PITFALLS content'); + assert.ok(result.milestones[0]?.research !== null, 'research: milestone has consolidated research'); + assert.ok(result.milestones[0]?.research!.includes('Project Summary'), 'research: includes SUMMARY content'); + assert.ok(result.milestones[0]?.research!.includes('Architecture'), 'research: includes ARCHITECTURE content'); + assert.ok(result.milestones[0]?.research!.includes('Pitfalls'), 'research: includes PITFALLS content'); // Fixed ordering: SUMMARY before ARCHITECTURE before PITFALLS const summaryIdx = result.milestones[0]?.research!.indexOf('Project Summary') ?? -1; const archIdx = result.milestones[0]?.research!.indexOf('Architecture') ?? -1; const pitfallIdx = result.milestones[0]?.research!.indexOf('Pitfalls') ?? -1; - assertTrue(summaryIdx < archIdx, 'research: SUMMARY before ARCHITECTURE in consolidated'); - assertTrue(archIdx < pitfallIdx, 'research: ARCHITECTURE before PITFALLS in consolidated'); + assert.ok(summaryIdx < archIdx, 'research: SUMMARY before ARCHITECTURE in consolidated'); + assert.ok(archIdx < pitfallIdx, 'research: ARCHITECTURE before PITFALLS in consolidated'); // Phase-level research → slice research const slice = result.milestones[0]?.slices[0]; - assertTrue(slice?.research !== null, 'research: slice has phase research'); - assertTrue(slice?.research!.includes('Phase Features'), 'research: slice research includes phase content'); -} + assert.ok(slice?.research !== null, 'research: slice has phase research'); + assert.ok(slice?.research!.includes('Phase Features'), 'research: slice research includes phase content'); +}); // ─── Scenario 6: Requirements Classification ────────────────────────────── -{ - console.log('Scenario 6: Requirements classification'); +test('Scenario 6: Requirements classification', () => { const project = emptyProject({ roadmap: flatRoadmap([roadmapEntry(1, 'req-phase')]), @@ -365,22 +359,21 @@ function makeResearch(fileName: string, content: string): PlanningResearch { const result = transformToGSD(project); - assertEq(result.requirements.length, 3, 'requirements: 3 requirements'); - assertEq(result.requirements[0]?.id, 'R001', 'requirements: first is R001'); - assertEq(result.requirements[0]?.status, 'active', 'requirements: R001 status active'); - assertEq(result.requirements[1]?.status, 'validated', 'requirements: R002 status validated'); - assertEq(result.requirements[2]?.status, 'deferred', 'requirements: R003 status deferred'); - assertTrue(result.requirements[0]?.title === 'Core Feature', 'requirements: R001 title preserved'); - assertTrue(result.requirements[0]?.description.includes('Description for R001'), 'requirements: R001 description preserved'); - assertEq(result.requirements[0]?.class, 'core-capability', 'requirements: default class'); - assertEq(result.requirements[0]?.source, 'inferred', 'requirements: default source'); - assertEq(result.requirements[0]?.primarySlice, 'none yet', 'requirements: default primarySlice'); -} + assert.deepStrictEqual(result.requirements.length, 3, 'requirements: 3 requirements'); + assert.deepStrictEqual(result.requirements[0]?.id, 'R001', 'requirements: first is R001'); + assert.deepStrictEqual(result.requirements[0]?.status, 'active', 'requirements: R001 status active'); + assert.deepStrictEqual(result.requirements[1]?.status, 'validated', 'requirements: R002 status validated'); + assert.deepStrictEqual(result.requirements[2]?.status, 'deferred', 'requirements: R003 status deferred'); + assert.ok(result.requirements[0]?.title === 'Core Feature', 'requirements: R001 title preserved'); + assert.ok(result.requirements[0]?.description.includes('Description for R001'), 'requirements: R001 description preserved'); + assert.deepStrictEqual(result.requirements[0]?.class, 'core-capability', 'requirements: default class'); + assert.deepStrictEqual(result.requirements[0]?.source, 'inferred', 'requirements: default source'); + assert.deepStrictEqual(result.requirements[0]?.primarySlice, 'none yet', 'requirements: default primarySlice'); +}); // ─── Scenario 7: Empty Phase (no plans → slice with 0 tasks) ─────────────── -{ - console.log('Scenario 7: Empty phase'); +test('Scenario 7: Empty phase', () => { const project = emptyProject({ roadmap: flatRoadmap([ @@ -397,15 +390,14 @@ function makeResearch(fileName: string, content: string): PlanningResearch { const result = transformToGSD(project); - assertEq(result.milestones[0]?.slices[0]?.tasks.length, 0, 'empty: empty phase → 0 tasks'); - assertEq(result.milestones[0]?.slices[1]?.tasks.length, 1, 'empty: non-empty phase → 1 task'); - assertTrue(result.milestones[0]?.slices[0]?.id === 'S01', 'empty: empty slice still gets ID'); -} + assert.deepStrictEqual(result.milestones[0]?.slices[0]?.tasks.length, 0, 'empty: empty phase → 0 tasks'); + assert.deepStrictEqual(result.milestones[0]?.slices[1]?.tasks.length, 1, 'empty: non-empty phase → 1 task'); + assert.ok(result.milestones[0]?.slices[0]?.id === 'S01', 'empty: empty slice still gets ID'); +}); // ─── Scenario 8: Demo Derivation from Plan Objective ─────────────────────── -{ - console.log('Scenario 8: Demo derivation'); +test('Scenario 8: Demo derivation', () => { const project = emptyProject({ roadmap: flatRoadmap([roadmapEntry(1, 'demo-phase')]), @@ -420,19 +412,18 @@ function makeResearch(fileName: string, content: string): PlanningResearch { const result = transformToGSD(project); - assertTrue(result.milestones[0]?.slices[0]?.demo.length > 0, 'demo: slice demo is not empty'); - assertTrue( + assert.ok(result.milestones[0]?.slices[0]?.demo.length > 0, 'demo: slice demo is not empty'); + assert.ok( result.milestones[0]?.slices[0]?.demo.includes('authentication') || result.milestones[0]?.slices[0]?.demo.includes('Build'), 'demo: slice demo derived from first plan objective', ); - assertTrue(result.milestones[0]?.slices[0]?.goal.length > 0, 'demo: slice goal is not empty'); -} + assert.ok(result.milestones[0]?.slices[0]?.goal.length > 0, 'demo: slice goal is not empty'); +}); // ─── Scenario 9: Field Defaults and Type Safety ──────────────────────────── -{ - console.log('Scenario 9: Field defaults'); +test('Scenario 9: Field defaults', () => { const project = emptyProject({ roadmap: flatRoadmap([roadmapEntry(1, 'defaults-phase')]), @@ -460,20 +451,19 @@ function makeResearch(fileName: string, content: string): PlanningResearch { const slice = result.milestones[0]?.slices[0]; const task = slice?.tasks[0]; - assertEq(slice?.risk, 'medium', 'defaults: slice risk defaults to medium'); - assertEq(slice?.depends, [], 'defaults: S01 has no depends'); - assertTrue(task?.description.length > 0, 'defaults: task description not empty'); - assertEq(task?.files, ['src/auth.ts', 'src/db.ts'], 'defaults: task files from frontmatter'); - assertEq(task?.mustHaves, ['Auth works', 'DB connected'], 'defaults: task mustHaves from frontmatter'); - assertEq(task?.done, false, 'defaults: task without summary is not done'); - assertEq(task?.estimate, '', 'defaults: task without summary has empty estimate'); - assertTrue(task?.summary === null, 'defaults: task without summary has null summary'); -} + assert.deepStrictEqual(slice?.risk, 'medium', 'defaults: slice risk defaults to medium'); + assert.deepStrictEqual(slice?.depends, [], 'defaults: S01 has no depends'); + assert.ok(task?.description.length > 0, 'defaults: task description not empty'); + assert.deepStrictEqual(task?.files, ['src/auth.ts', 'src/db.ts'], 'defaults: task files from frontmatter'); + assert.deepStrictEqual(task?.mustHaves, ['Auth works', 'DB connected'], 'defaults: task mustHaves from frontmatter'); + assert.deepStrictEqual(task?.done, false, 'defaults: task without summary is not done'); + assert.deepStrictEqual(task?.estimate, '', 'defaults: task without summary has empty estimate'); + assert.ok(task?.summary === null, 'defaults: task without summary has null summary'); +}); // ─── Scenario 10: Sequential Depends ────────────────────────────────────── -{ - console.log('Scenario 10: Sequential depends'); +test('Scenario 10: Sequential depends', () => { const project = emptyProject({ roadmap: flatRoadmap([ @@ -491,15 +481,14 @@ function makeResearch(fileName: string, content: string): PlanningResearch { const result = transformToGSD(project); const slices = result.milestones[0]?.slices; - assertEq(slices?.[0]?.depends, [], 'depends: S01 has empty depends'); - assertEq(slices?.[1]?.depends, ['S01'], 'depends: S02 depends on S01'); - assertEq(slices?.[2]?.depends, ['S02'], 'depends: S03 depends on S02'); -} + assert.deepStrictEqual(slices?.[0]?.depends, [], 'depends: S01 has empty depends'); + assert.deepStrictEqual(slices?.[1]?.depends, ['S01'], 'depends: S02 depends on S01'); + assert.deepStrictEqual(slices?.[2]?.depends, ['S02'], 'depends: S03 depends on S02'); +}); // ─── Scenario 11: Requirements with unknown status and missing IDs ───────── -{ - console.log('Scenario 11: Requirements edge cases'); +test('Scenario 11: Requirements edge cases', () => { const project = emptyProject({ roadmap: flatRoadmap([roadmapEntry(1, 'req-edge')]), @@ -516,17 +505,16 @@ function makeResearch(fileName: string, content: string): PlanningResearch { const result = transformToGSD(project); - assertEq(result.requirements[0]?.id, 'R001', 'req-edge: empty id gets R001'); - assertEq(result.requirements[1]?.id, 'R002', 'req-edge: second empty id gets R002'); - assertEq(result.requirements[2]?.id, 'R005', 'req-edge: existing id preserved'); - assertEq(result.requirements[2]?.status, 'active', 'req-edge: unknown status normalized to active'); - assertEq(result.requirements[3]?.status, 'deferred', 'req-edge: uppercase DEFERRED normalized'); -} + assert.deepStrictEqual(result.requirements[0]?.id, 'R001', 'req-edge: empty id gets R001'); + assert.deepStrictEqual(result.requirements[1]?.id, 'R002', 'req-edge: second empty id gets R002'); + assert.deepStrictEqual(result.requirements[2]?.id, 'R005', 'req-edge: existing id preserved'); + assert.deepStrictEqual(result.requirements[2]?.status, 'active', 'req-edge: unknown status normalized to active'); + assert.deepStrictEqual(result.requirements[3]?.status, 'deferred', 'req-edge: uppercase DEFERRED normalized'); +}); // ─── Scenario 12: Vision derivation ──────────────────────────────────────── -{ - console.log('Scenario 12: Vision derivation'); +test('Scenario 12: Vision derivation', () => { // Vision from project description const project1 = emptyProject({ @@ -536,7 +524,7 @@ function makeResearch(fileName: string, content: string): PlanningResearch { }); const result1 = transformToGSD(project1); - assertTrue(result1.milestones[0]?.vision.includes('revolutionary'), 'vision: derived from project first line'); + assert.ok(result1.milestones[0]?.vision.includes('revolutionary'), 'vision: derived from project first line'); // Vision fallback when no project const project2 = emptyProject({ @@ -545,13 +533,12 @@ function makeResearch(fileName: string, content: string): PlanningResearch { }); const result2 = transformToGSD(project2); - assertTrue(result2.milestones[0]?.vision.length > 0, 'vision: fallback is non-empty'); -} + assert.ok(result2.milestones[0]?.vision.length > 0, 'vision: fallback is non-empty'); +}); // ─── Scenario 13: Decisions content from summaries ───────────────────────── -{ - console.log('Scenario 13: Decisions content'); +test('Scenario 13: Decisions content', () => { const project = emptyProject({ roadmap: flatRoadmap([roadmapEntry(1, 'decision-phase', true)]), @@ -565,13 +552,12 @@ function makeResearch(fileName: string, content: string): PlanningResearch { const result = transformToGSD(project); - assertTrue(result.decisionsContent.includes('decision-01'), 'decisions: extracts key-decisions from summaries'); -} + assert.ok(result.decisionsContent.includes('decision-01'), 'decisions: extracts key-decisions from summaries'); +}); // ─── Scenario 14: No undefined values in output ─────────────────────────── -{ - console.log('Scenario 14: No undefined values'); +test('Scenario 14: No undefined values', () => { const project = emptyProject({ project: '# Test\nDescription.', @@ -596,7 +582,7 @@ function makeResearch(fileName: string, content: string): PlanningResearch { // Deep check for undefined values function checkNoUndefined(obj: unknown, path: string): void { if (obj === undefined) { - assertTrue(false, `no-undefined: ${path} is undefined`); + assert.ok(false, `no-undefined: ${path} is undefined`); return; } if (obj === null) return; // null is allowed (e.g. research, summary) @@ -612,13 +598,12 @@ function makeResearch(fileName: string, content: string): PlanningResearch { } checkNoUndefined(result, 'result'); - assertTrue(true, 'no-undefined: deep check completed without finding undefined values'); -} + assert.ok(true, 'no-undefined: deep check completed without finding undefined values'); +}); // ─── Scenario 15: Research with no files ─────────────────────────────────── -{ - console.log('Scenario 15: Empty research'); +test('Scenario 15: Empty research', () => { const project = emptyProject({ roadmap: flatRoadmap([roadmapEntry(1, 'no-research')]), @@ -626,10 +611,9 @@ function makeResearch(fileName: string, content: string): PlanningResearch { }); const result = transformToGSD(project); - assertTrue(result.milestones[0]?.research === null, 'empty-research: milestone research is null'); - assertTrue(result.milestones[0]?.slices[0]?.research === null, 'empty-research: slice research is null'); -} + assert.ok(result.milestones[0]?.research === null, 'empty-research: milestone research is null'); + assert.ok(result.milestones[0]?.slices[0]?.research === null, 'empty-research: slice research is null'); +}); // ─── Results ─────────────────────────────────────────────────────────────── -report(); diff --git a/src/resources/extensions/gsd/tests/migrate-validator-parsers.test.ts b/src/resources/extensions/gsd/tests/migrate-validator-parsers.test.ts index 65052d46c..2466b9480 100644 --- a/src/resources/extensions/gsd/tests/migrate-validator-parsers.test.ts +++ b/src/resources/extensions/gsd/tests/migrate-validator-parsers.test.ts @@ -15,9 +15,9 @@ import { parseOldState, parseOldConfig, } from '../migrate/parsers.ts'; -import { createTestContext } from './test-helpers.ts'; +import { describe, test, beforeEach, afterEach } from 'node:test'; +import assert from 'node:assert/strict'; -const { assertEq, assertTrue, report } = createTestContext(); function createFixtureBase(): string { return mkdtempSync(join(tmpdir(), 'gsd-migrate-t02-')); } @@ -173,55 +173,49 @@ const SAMPLE_STATE = `# State **Status:** in-progress `; -async function main(): Promise { - // ═══════════════════════════════════════════════════════════════════════ // Validator Tests // ═══════════════════════════════════════════════════════════════════════ - console.log('\n=== Validator: missing directory → fatal ==='); - { +test('Validator: missing directory → fatal', async () => { const base = createFixtureBase(); try { const result = await validatePlanningDirectory(join(base, 'nonexistent')); - assertEq(result.valid, false, 'missing dir: validation fails'); - assertTrue(result.issues.length > 0, 'missing dir: has issues'); - assertTrue(result.issues.some(i => i.severity === 'fatal'), 'missing dir: has fatal issue'); + assert.deepStrictEqual(result.valid, false, 'missing dir: validation fails'); + assert.ok(result.issues.length > 0, 'missing dir: has issues'); + assert.ok(result.issues.some(i => i.severity === 'fatal'), 'missing dir: has fatal issue'); } finally { cleanup(base); } - } +}); - console.log('\n=== Validator: missing ROADMAP.md → warning (not fatal) ==='); - { +test('Validator: missing ROADMAP.md → warning (not fatal)', async () => { const base = createFixtureBase(); try { const planning = createPlanningDir(base); writeFileSync(join(planning, 'PROJECT.md'), SAMPLE_PROJECT); const result = await validatePlanningDirectory(planning); - assertEq(result.valid, true, 'no roadmap: validation still passes'); - assertTrue(result.issues.some(i => i.severity === 'warning' && i.file.includes('ROADMAP')), 'no roadmap: warning issue mentions ROADMAP'); + assert.deepStrictEqual(result.valid, true, 'no roadmap: validation still passes'); + assert.ok(result.issues.some(i => i.severity === 'warning' && i.file.includes('ROADMAP')), 'no roadmap: warning issue mentions ROADMAP'); } finally { cleanup(base); } - } +}); - console.log('\n=== Validator: missing PROJECT.md → warning ==='); - { +test('Validator: missing PROJECT.md → warning', async () => { const base = createFixtureBase(); try { const planning = createPlanningDir(base); writeFileSync(join(planning, 'ROADMAP.md'), SAMPLE_ROADMAP); const result = await validatePlanningDirectory(planning); - assertEq(result.valid, true, 'no project: validation passes (warning only)'); - assertTrue(result.issues.some(i => i.severity === 'warning' && i.file.includes('PROJECT')), 'no project: warning issue mentions PROJECT'); + assert.deepStrictEqual(result.valid, true, 'no project: validation passes (warning only)'); + assert.ok(result.issues.some(i => i.severity === 'warning' && i.file.includes('PROJECT')), 'no project: warning issue mentions PROJECT'); } finally { cleanup(base); } - } +}); - console.log('\n=== Validator: complete directory → valid with no issues ==='); - { +test('Validator: complete directory → valid with no issues', async () => { const base = createFixtureBase(); try { const planning = createPlanningDir(base); @@ -231,78 +225,74 @@ async function main(): Promise { writeFileSync(join(planning, 'STATE.md'), SAMPLE_STATE); mkdirSync(join(planning, 'phases'), { recursive: true }); const result = await validatePlanningDirectory(planning); - assertEq(result.valid, true, 'complete dir: validation passes'); - assertEq(result.issues.length, 0, 'complete dir: no issues'); + assert.deepStrictEqual(result.valid, true, 'complete dir: validation passes'); + assert.deepStrictEqual(result.issues.length, 0, 'complete dir: no issues'); } finally { cleanup(base); } - } +}); // ═══════════════════════════════════════════════════════════════════════ // Roadmap Parser Tests // ═══════════════════════════════════════════════════════════════════════ - console.log('\n=== parseOldRoadmap: flat format ==='); - { +test('parseOldRoadmap: flat format', () => { const roadmap = parseOldRoadmap(SAMPLE_ROADMAP); - assertEq(roadmap.milestones.length, 0, 'flat roadmap: no milestone sections'); - assertEq(roadmap.phases.length, 3, 'flat roadmap: 3 phases'); - assertEq(roadmap.phases[0].number, 29, 'flat roadmap: first phase number'); - assertEq(roadmap.phases[0].title, 'Auth System', 'flat roadmap: first phase title'); - assertEq(roadmap.phases[0].done, true, 'flat roadmap: first phase done'); - assertEq(roadmap.phases[1].done, false, 'flat roadmap: second phase not done'); - } + assert.deepStrictEqual(roadmap.milestones.length, 0, 'flat roadmap: no milestone sections'); + assert.deepStrictEqual(roadmap.phases.length, 3, 'flat roadmap: 3 phases'); + assert.deepStrictEqual(roadmap.phases[0].number, 29, 'flat roadmap: first phase number'); + assert.deepStrictEqual(roadmap.phases[0].title, 'Auth System', 'flat roadmap: first phase title'); + assert.deepStrictEqual(roadmap.phases[0].done, true, 'flat roadmap: first phase done'); + assert.deepStrictEqual(roadmap.phases[1].done, false, 'flat roadmap: second phase not done'); +}); - console.log('\n=== parseOldRoadmap: milestone-sectioned with
==='); - { +test('parseOldRoadmap: milestone-sectioned with
', () => { const roadmap = parseOldRoadmap(SAMPLE_MILESTONE_SECTIONED_ROADMAP); - assertTrue(roadmap.milestones.length >= 2, 'ms roadmap: has milestone sections'); + assert.ok(roadmap.milestones.length >= 2, 'ms roadmap: has milestone sections'); const v20 = roadmap.milestones.find(m => m.id.includes('2.0')); - assertTrue(v20 !== undefined, 'ms roadmap: v2.0 found'); - assertEq(v20?.collapsed, true, 'ms roadmap: v2.0 collapsed'); - assertTrue((v20?.phases.length ?? 0) >= 2, 'ms roadmap: v2.0 has phases'); - assertTrue(v20?.phases.every(p => p.done) ?? false, 'ms roadmap: v2.0 all done'); + assert.ok(v20 !== undefined, 'ms roadmap: v2.0 found'); + assert.deepStrictEqual(v20?.collapsed, true, 'ms roadmap: v2.0 collapsed'); + assert.ok((v20?.phases.length ?? 0) >= 2, 'ms roadmap: v2.0 has phases'); + assert.ok(v20?.phases.every(p => p.done) ?? false, 'ms roadmap: v2.0 all done'); const v25 = roadmap.milestones.find(m => m.id.includes('2.5')); - assertTrue(v25 !== undefined, 'ms roadmap: v2.5 found'); - assertEq(v25?.collapsed, false, 'ms roadmap: v2.5 not collapsed'); - assertTrue((v25?.phases.length ?? 0) >= 3, 'ms roadmap: v2.5 has 3 phases'); + assert.ok(v25 !== undefined, 'ms roadmap: v2.5 found'); + assert.deepStrictEqual(v25?.collapsed, false, 'ms roadmap: v2.5 not collapsed'); + assert.ok((v25?.phases.length ?? 0) >= 3, 'ms roadmap: v2.5 has 3 phases'); const p29 = v25?.phases.find(p => p.number === 29); - assertEq(p29?.done, true, 'ms roadmap: phase 29 done'); + assert.deepStrictEqual(p29?.done, true, 'ms roadmap: phase 29 done'); const p30 = v25?.phases.find(p => p.number === 30); - assertEq(p30?.done, false, 'ms roadmap: phase 30 not done'); - } + assert.deepStrictEqual(p30?.done, false, 'ms roadmap: phase 30 not done'); +}); // ═══════════════════════════════════════════════════════════════════════ // Plan Parser Tests // ═══════════════════════════════════════════════════════════════════════ - console.log('\n=== parseOldPlan: XML-in-markdown ==='); - { +test('parseOldPlan: XML-in-markdown', () => { const plan = parseOldPlan(SAMPLE_PLAN_XML, '29-01-PLAN.md', '01'); - assertTrue(plan.objective.includes('authentication'), 'plan: objective extracted'); - assertEq(plan.tasks.length, 3, 'plan: 3 tasks'); - assertTrue(plan.tasks[0].includes('auth middleware'), 'plan: first task content'); - assertTrue(plan.context.includes('JWT'), 'plan: context extracted'); - assertTrue(plan.verification.includes('Login returns'), 'plan: verification extracted'); - assertTrue(plan.successCriteria.includes('endpoints respond'), 'plan: success criteria extracted'); + assert.ok(plan.objective.includes('authentication'), 'plan: objective extracted'); + assert.deepStrictEqual(plan.tasks.length, 3, 'plan: 3 tasks'); + assert.ok(plan.tasks[0].includes('auth middleware'), 'plan: first task content'); + assert.ok(plan.context.includes('JWT'), 'plan: context extracted'); + assert.ok(plan.verification.includes('Login returns'), 'plan: verification extracted'); + assert.ok(plan.successCriteria.includes('endpoints respond'), 'plan: success criteria extracted'); // Frontmatter - assertEq(plan.frontmatter.phase, '29-auth-system', 'plan fm: phase'); - assertEq(plan.frontmatter.plan, '01', 'plan fm: plan'); - assertEq(plan.frontmatter.type, 'implementation', 'plan fm: type'); - assertEq(plan.frontmatter.wave, 1, 'plan fm: wave'); - assertEq(plan.frontmatter.autonomous, true, 'plan fm: autonomous'); - assertTrue(plan.frontmatter.files_modified.length >= 2, 'plan fm: files_modified'); - assertTrue(plan.frontmatter.must_haves !== null, 'plan fm: must_haves parsed'); - assertTrue((plan.frontmatter.must_haves?.truths.length ?? 0) >= 1, 'plan fm: must_haves truths'); - assertTrue((plan.frontmatter.must_haves?.artifacts.length ?? 0) >= 1, 'plan fm: must_haves artifacts'); - } + assert.deepStrictEqual(plan.frontmatter.phase, '29-auth-system', 'plan fm: phase'); + assert.deepStrictEqual(plan.frontmatter.plan, '01', 'plan fm: plan'); + assert.deepStrictEqual(plan.frontmatter.type, 'implementation', 'plan fm: type'); + assert.deepStrictEqual(plan.frontmatter.wave, 1, 'plan fm: wave'); + assert.deepStrictEqual(plan.frontmatter.autonomous, true, 'plan fm: autonomous'); + assert.ok(plan.frontmatter.files_modified.length >= 2, 'plan fm: files_modified'); + assert.ok(plan.frontmatter.must_haves !== null, 'plan fm: must_haves parsed'); + assert.ok((plan.frontmatter.must_haves?.truths.length ?? 0) >= 1, 'plan fm: must_haves truths'); + assert.ok((plan.frontmatter.must_haves?.artifacts.length ?? 0) >= 1, 'plan fm: must_haves artifacts'); +}); - console.log('\n=== parseOldPlan: plain markdown (no XML tags) ==='); - { +test('parseOldPlan: plain markdown (no XML tags)', () => { const plainPlan = `# 001: Fix Login Bug ## Description @@ -315,100 +305,86 @@ Fix the login button not responding on mobile. 2. Fix event propagation `; const plan = parseOldPlan(plainPlan, '001-PLAN.md', '001'); - assertEq(plan.objective, '', 'plain plan: no objective (no XML)'); - assertEq(plan.tasks.length, 0, 'plain plan: no tasks (no XML)'); - assertEq(plan.frontmatter.phase, '', 'plain plan: no frontmatter phase'); - } + assert.deepStrictEqual(plan.objective, '', 'plain plan: no objective (no XML)'); + assert.deepStrictEqual(plan.tasks.length, 0, 'plain plan: no tasks (no XML)'); + assert.deepStrictEqual(plan.frontmatter.phase, '', 'plain plan: no frontmatter phase'); +}); // ═══════════════════════════════════════════════════════════════════════ // Summary Parser Tests // ═══════════════════════════════════════════════════════════════════════ - console.log('\n=== parseOldSummary: YAML frontmatter ==='); - { +test('parseOldSummary: YAML frontmatter', () => { const summary = parseOldSummary(SAMPLE_SUMMARY, '29-01-SUMMARY.md', '01'); - assertEq(summary.frontmatter.phase, '29-auth-system', 'summary fm: phase'); - assertEq(summary.frontmatter.plan, '01', 'summary fm: plan'); - assertEq(summary.frontmatter.subsystem, 'auth', 'summary fm: subsystem'); - assertEq(summary.frontmatter.tags, ['authentication', 'security'], 'summary fm: tags'); - assertEq(summary.frontmatter.provides, ['auth-middleware', 'jwt-validation'], 'summary fm: provides'); - assertEq(summary.frontmatter.affects, ['api-routes'], 'summary fm: affects'); - assertEq(summary.frontmatter['tech-stack'], ['jsonwebtoken', 'express'], 'summary fm: tech-stack'); - assertEq(summary.frontmatter['key-files'], ['src/auth.ts', 'src/middleware/auth.ts'], 'summary fm: key-files'); - assertEq(summary.frontmatter['key-decisions'], ['Use RS256 for JWT signing', 'Store refresh tokens in DB'], 'summary fm: key-decisions'); - assertEq(summary.frontmatter['patterns-established'], ['Middleware-based auth'], 'summary fm: patterns-established'); - assertEq(summary.frontmatter.duration, '2h', 'summary fm: duration'); - assertEq(summary.frontmatter.completed, '2026-01-15', 'summary fm: completed'); - assertTrue(summary.body.includes('Auth Implementation Summary'), 'summary: body content present'); - } + assert.deepStrictEqual(summary.frontmatter.phase, '29-auth-system', 'summary fm: phase'); + assert.deepStrictEqual(summary.frontmatter.plan, '01', 'summary fm: plan'); + assert.deepStrictEqual(summary.frontmatter.subsystem, 'auth', 'summary fm: subsystem'); + assert.deepStrictEqual(summary.frontmatter.tags, ['authentication', 'security'], 'summary fm: tags'); + assert.deepStrictEqual(summary.frontmatter.provides, ['auth-middleware', 'jwt-validation'], 'summary fm: provides'); + assert.deepStrictEqual(summary.frontmatter.affects, ['api-routes'], 'summary fm: affects'); + assert.deepStrictEqual(summary.frontmatter['tech-stack'], ['jsonwebtoken', 'express'], 'summary fm: tech-stack'); + assert.deepStrictEqual(summary.frontmatter['key-files'], ['src/auth.ts', 'src/middleware/auth.ts'], 'summary fm: key-files'); + assert.deepStrictEqual(summary.frontmatter['key-decisions'], ['Use RS256 for JWT signing', 'Store refresh tokens in DB'], 'summary fm: key-decisions'); + assert.deepStrictEqual(summary.frontmatter['patterns-established'], ['Middleware-based auth'], 'summary fm: patterns-established'); + assert.deepStrictEqual(summary.frontmatter.duration, '2h', 'summary fm: duration'); + assert.deepStrictEqual(summary.frontmatter.completed, '2026-01-15', 'summary fm: completed'); + assert.ok(summary.body.includes('Auth Implementation Summary'), 'summary: body content present'); +}); // ═══════════════════════════════════════════════════════════════════════ // Requirements Parser Tests // ═══════════════════════════════════════════════════════════════════════ - console.log('\n=== parseOldRequirements ==='); - { +test('parseOldRequirements', () => { const reqs = parseOldRequirements(SAMPLE_REQUIREMENTS); - assertEq(reqs.length, 4, 'requirements: 4 entries'); - assertEq(reqs[0].id, 'R001', 'req 0: id'); - assertEq(reqs[0].title, 'User Authentication', 'req 0: title'); - assertEq(reqs[0].status, 'active', 'req 0: status'); - assertTrue(reqs[0].description.includes('log in'), 'req 0: description'); - assertEq(reqs[2].id, 'R003', 'req 2: id'); - assertEq(reqs[2].status, 'validated', 'req 2: status'); - assertEq(reqs[3].id, 'R004', 'req 3: id'); - assertEq(reqs[3].status, 'deferred', 'req 3: status'); - } + assert.deepStrictEqual(reqs.length, 4, 'requirements: 4 entries'); + assert.deepStrictEqual(reqs[0].id, 'R001', 'req 0: id'); + assert.deepStrictEqual(reqs[0].title, 'User Authentication', 'req 0: title'); + assert.deepStrictEqual(reqs[0].status, 'active', 'req 0: status'); + assert.ok(reqs[0].description.includes('log in'), 'req 0: description'); + assert.deepStrictEqual(reqs[2].id, 'R003', 'req 2: id'); + assert.deepStrictEqual(reqs[2].status, 'validated', 'req 2: status'); + assert.deepStrictEqual(reqs[3].id, 'R004', 'req 3: id'); + assert.deepStrictEqual(reqs[3].status, 'deferred', 'req 3: status'); +}); // ═══════════════════════════════════════════════════════════════════════ // State Parser Tests // ═══════════════════════════════════════════════════════════════════════ - console.log('\n=== parseOldState ==='); - { +test('parseOldState', () => { const state = parseOldState(SAMPLE_STATE); - assertTrue(state.currentPhase?.includes('30') ?? false, 'state: current phase includes 30'); - assertEq(state.status, 'in-progress', 'state: status'); - assertTrue(state.raw === SAMPLE_STATE, 'state: raw preserved'); - } + assert.ok(state.currentPhase?.includes('30') ?? false, 'state: current phase includes 30'); + assert.deepStrictEqual(state.status, 'in-progress', 'state: status'); + assert.ok(state.raw === SAMPLE_STATE, 'state: raw preserved'); +}); // ═══════════════════════════════════════════════════════════════════════ // Config Parser Tests // ═══════════════════════════════════════════════════════════════════════ - console.log('\n=== parseOldConfig: valid JSON ==='); - { +test('parseOldConfig: valid JSON', () => { const config = parseOldConfig('{"projectName":"test","version":"1.0"}'); - assertTrue(config !== null, 'config: parsed'); - assertEq(config?.projectName, 'test', 'config: projectName'); - } + assert.ok(config !== null, 'config: parsed'); + assert.deepStrictEqual(config?.projectName, 'test', 'config: projectName'); +}); - console.log('\n=== parseOldConfig: invalid JSON → null ==='); - { +test('parseOldConfig: invalid JSON → null', () => { const config = parseOldConfig('not json at all {{{'); - assertEq(config, null, 'config: invalid JSON returns null'); - } + assert.deepStrictEqual(config, null, 'config: invalid JSON returns null'); +}); - console.log('\n=== parseOldConfig: non-object JSON → null ==='); - { +test('parseOldConfig: non-object JSON → null', () => { const config = parseOldConfig('"just a string"'); - assertEq(config, null, 'config: non-object returns null'); - } + assert.deepStrictEqual(config, null, 'config: non-object returns null'); +}); // ═══════════════════════════════════════════════════════════════════════ // Project Parser Tests // ═══════════════════════════════════════════════════════════════════════ - console.log('\n=== parseOldProject ==='); - { +test('parseOldProject', () => { const project = parseOldProject(SAMPLE_PROJECT); - assertEq(project, SAMPLE_PROJECT, 'project: returns raw content'); - } - - report(); -} - -main().catch((error) => { - console.error(error); - process.exit(1); + assert.deepStrictEqual(project, SAMPLE_PROJECT, 'project: returns raw content'); }); + diff --git a/src/resources/extensions/gsd/tests/migrate-writer-integration.test.ts b/src/resources/extensions/gsd/tests/migrate-writer-integration.test.ts index 96deac0a7..8fa3d98d0 100644 --- a/src/resources/extensions/gsd/tests/migrate-writer-integration.test.ts +++ b/src/resources/extensions/gsd/tests/migrate-writer-integration.test.ts @@ -20,9 +20,9 @@ import type { GSDTask, GSDRequirement, } from '../migrate/types.ts'; -import { createTestContext } from './test-helpers.ts'; +import { describe, test, beforeEach, afterEach } from 'node:test'; +import assert from 'node:assert/strict'; -const { assertEq, assertTrue, report } = createTestContext(); // ─── Fixture Builders ────────────────────────────────────────────────────── function makeTask(id: string, title: string, done: boolean, hasSummary: boolean): GSDTask { @@ -130,11 +130,9 @@ function buildCompleteProject(): GSDProject { // Tests // ═══════════════════════════════════════════════════════════════════════════ -async function main(): Promise { - // ─── Scenario 1: Incomplete project ──────────────────────────────────── - console.log('\n=== Scenario 1: Incomplete project — write, parse, deriveState ==='); - { + +test('Scenario 1: Incomplete project — write, parse, deriveState', async () => { const base = mkdtempSync(join(tmpdir(), 'gsd-writer-int-')); try { const project = buildIncompleteProject(); @@ -145,64 +143,64 @@ async function main(): Promise { const gsd = join(base, '.gsd'); const m = join(gsd, 'milestones', 'M001'); - assertTrue(existsSync(join(m, 'M001-ROADMAP.md')), 'incomplete: M001-ROADMAP.md exists'); - assertTrue(existsSync(join(m, 'M001-CONTEXT.md')), 'incomplete: M001-CONTEXT.md exists'); - assertTrue(existsSync(join(m, 'M001-RESEARCH.md')), 'incomplete: M001-RESEARCH.md exists'); - assertTrue(existsSync(join(m, 'slices', 'S01', 'S01-PLAN.md')), 'incomplete: S01-PLAN.md exists'); - assertTrue(existsSync(join(m, 'slices', 'S02', 'S02-PLAN.md')), 'incomplete: S02-PLAN.md exists'); - assertTrue(existsSync(join(m, 'slices', 'S01', 'S01-SUMMARY.md')), 'incomplete: S01-SUMMARY.md exists'); - assertTrue(!existsSync(join(m, 'slices', 'S02', 'S02-SUMMARY.md')), 'incomplete: S02-SUMMARY.md NOT written (null)'); - assertTrue(existsSync(join(gsd, 'REQUIREMENTS.md')), 'incomplete: REQUIREMENTS.md exists'); - assertTrue(existsSync(join(gsd, 'PROJECT.md')), 'incomplete: PROJECT.md exists'); - assertTrue(existsSync(join(gsd, 'DECISIONS.md')), 'incomplete: DECISIONS.md exists'); - assertTrue(existsSync(join(gsd, 'STATE.md')), 'incomplete: STATE.md exists'); + assert.ok(existsSync(join(m, 'M001-ROADMAP.md')), 'incomplete: M001-ROADMAP.md exists'); + assert.ok(existsSync(join(m, 'M001-CONTEXT.md')), 'incomplete: M001-CONTEXT.md exists'); + assert.ok(existsSync(join(m, 'M001-RESEARCH.md')), 'incomplete: M001-RESEARCH.md exists'); + assert.ok(existsSync(join(m, 'slices', 'S01', 'S01-PLAN.md')), 'incomplete: S01-PLAN.md exists'); + assert.ok(existsSync(join(m, 'slices', 'S02', 'S02-PLAN.md')), 'incomplete: S02-PLAN.md exists'); + assert.ok(existsSync(join(m, 'slices', 'S01', 'S01-SUMMARY.md')), 'incomplete: S01-SUMMARY.md exists'); + assert.ok(!existsSync(join(m, 'slices', 'S02', 'S02-SUMMARY.md')), 'incomplete: S02-SUMMARY.md NOT written (null)'); + assert.ok(existsSync(join(gsd, 'REQUIREMENTS.md')), 'incomplete: REQUIREMENTS.md exists'); + assert.ok(existsSync(join(gsd, 'PROJECT.md')), 'incomplete: PROJECT.md exists'); + assert.ok(existsSync(join(gsd, 'DECISIONS.md')), 'incomplete: DECISIONS.md exists'); + assert.ok(existsSync(join(gsd, 'STATE.md')), 'incomplete: STATE.md exists'); // Task files - assertTrue(existsSync(join(m, 'slices', 'S01', 'tasks', 'T01-PLAN.md')), 'incomplete: T01-PLAN.md exists'); - assertTrue(existsSync(join(m, 'slices', 'S01', 'tasks', 'T01-SUMMARY.md')), 'incomplete: T01-SUMMARY.md exists'); - assertTrue(existsSync(join(m, 'slices', 'S01', 'tasks', 'T02-PLAN.md')), 'incomplete: T02-PLAN.md exists (auth task)'); - assertTrue(existsSync(join(m, 'slices', 'S01', 'tasks', 'T02-SUMMARY.md')), 'incomplete: T02-SUMMARY.md exists (auth task)'); - assertTrue(existsSync(join(m, 'slices', 'S02', 'tasks', 'T03-PLAN.md')), 'incomplete: T03-PLAN.md exists'); - assertTrue(!existsSync(join(m, 'slices', 'S02', 'tasks', 'T03-SUMMARY.md')), 'incomplete: T03-SUMMARY.md NOT written (null)'); + assert.ok(existsSync(join(m, 'slices', 'S01', 'tasks', 'T01-PLAN.md')), 'incomplete: T01-PLAN.md exists'); + assert.ok(existsSync(join(m, 'slices', 'S01', 'tasks', 'T01-SUMMARY.md')), 'incomplete: T01-SUMMARY.md exists'); + assert.ok(existsSync(join(m, 'slices', 'S01', 'tasks', 'T02-PLAN.md')), 'incomplete: T02-PLAN.md exists (auth task)'); + assert.ok(existsSync(join(m, 'slices', 'S01', 'tasks', 'T02-SUMMARY.md')), 'incomplete: T02-SUMMARY.md exists (auth task)'); + assert.ok(existsSync(join(m, 'slices', 'S02', 'tasks', 'T03-PLAN.md')), 'incomplete: T03-PLAN.md exists'); + assert.ok(!existsSync(join(m, 'slices', 'S02', 'tasks', 'T03-SUMMARY.md')), 'incomplete: T03-SUMMARY.md NOT written (null)'); // WrittenFiles counts console.log(' --- WrittenFiles counts ---'); - assertEq(result.counts.roadmaps, 1, 'incomplete: WrittenFiles roadmaps count'); - assertEq(result.counts.plans, 2, 'incomplete: WrittenFiles plans count'); - assertEq(result.counts.taskPlans, 3, 'incomplete: WrittenFiles taskPlans count'); - assertEq(result.counts.taskSummaries, 2, 'incomplete: WrittenFiles taskSummaries count'); - assertEq(result.counts.sliceSummaries, 1, 'incomplete: WrittenFiles sliceSummaries count'); - assertEq(result.counts.research, 1, 'incomplete: WrittenFiles research count'); - assertEq(result.counts.requirements, 1, 'incomplete: WrittenFiles requirements count'); - assertEq(result.counts.contexts, 1, 'incomplete: WrittenFiles contexts count'); + assert.deepStrictEqual(result.counts.roadmaps, 1, 'incomplete: WrittenFiles roadmaps count'); + assert.deepStrictEqual(result.counts.plans, 2, 'incomplete: WrittenFiles plans count'); + assert.deepStrictEqual(result.counts.taskPlans, 3, 'incomplete: WrittenFiles taskPlans count'); + assert.deepStrictEqual(result.counts.taskSummaries, 2, 'incomplete: WrittenFiles taskSummaries count'); + assert.deepStrictEqual(result.counts.sliceSummaries, 1, 'incomplete: WrittenFiles sliceSummaries count'); + assert.deepStrictEqual(result.counts.research, 1, 'incomplete: WrittenFiles research count'); + assert.deepStrictEqual(result.counts.requirements, 1, 'incomplete: WrittenFiles requirements count'); + assert.deepStrictEqual(result.counts.contexts, 1, 'incomplete: WrittenFiles contexts count'); // (b) parseRoadmap on written roadmap console.log(' --- parseRoadmap ---'); const roadmapContent = readFileSync(join(m, 'M001-ROADMAP.md'), 'utf-8'); const roadmap = parseRoadmap(roadmapContent); - assertEq(roadmap.slices.length, 2, 'incomplete: roadmap has 2 slices'); - assertTrue(roadmap.slices[0].done === true, 'incomplete: roadmap S01 is done'); - assertTrue(roadmap.slices[1].done === false, 'incomplete: roadmap S02 is not done'); - assertEq(roadmap.slices[0].id, 'S01', 'incomplete: roadmap slice 0 id'); - assertEq(roadmap.slices[1].id, 'S02', 'incomplete: roadmap slice 1 id'); + assert.deepStrictEqual(roadmap.slices.length, 2, 'incomplete: roadmap has 2 slices'); + assert.ok(roadmap.slices[0].done === true, 'incomplete: roadmap S01 is done'); + assert.ok(roadmap.slices[1].done === false, 'incomplete: roadmap S02 is not done'); + assert.deepStrictEqual(roadmap.slices[0].id, 'S01', 'incomplete: roadmap slice 0 id'); + assert.deepStrictEqual(roadmap.slices[1].id, 'S02', 'incomplete: roadmap slice 1 id'); // (c) parsePlan on S01 plan console.log(' --- parsePlan S01 ---'); const s01PlanContent = readFileSync(join(m, 'slices', 'S01', 'S01-PLAN.md'), 'utf-8'); const s01Plan = parsePlan(s01PlanContent); - assertEq(s01Plan.tasks.length, 2, 'incomplete: S01 plan has 2 tasks'); - assertTrue(s01Plan.tasks[0].done === true, 'incomplete: S01 T01 is done'); - assertTrue(s01Plan.tasks[1].done === true, 'incomplete: S01 T02 is done'); + assert.deepStrictEqual(s01Plan.tasks.length, 2, 'incomplete: S01 plan has 2 tasks'); + assert.ok(s01Plan.tasks[0].done === true, 'incomplete: S01 T01 is done'); + assert.ok(s01Plan.tasks[1].done === true, 'incomplete: S01 T02 is done'); // (d) parseSummary on S01 summary console.log(' --- parseSummary S01 ---'); const s01SummaryContent = readFileSync(join(m, 'slices', 'S01', 'S01-SUMMARY.md'), 'utf-8'); const s01Summary = parseSummary(s01SummaryContent); - assertTrue( + assert.ok( (s01Summary.frontmatter.key_files as string[]).length > 0, 'incomplete: S01 summary has key_files', ); - assertTrue( + assert.ok( (s01Summary.frontmatter.provides as string[]).length > 0, 'incomplete: S01 summary has provides', ); @@ -211,50 +209,50 @@ async function main(): Promise { console.log(' --- deriveState ---'); invalidateAllCaches(); const state = await deriveState(base); - assertEq(state.phase, 'executing', 'incomplete: deriveState phase is executing'); - assertTrue(state.activeMilestone !== null, 'incomplete: deriveState has activeMilestone'); - assertEq(state.activeMilestone!.id, 'M001', 'incomplete: deriveState activeMilestone is M001'); - assertTrue(state.activeSlice !== null, 'incomplete: deriveState has activeSlice'); - assertEq(state.activeSlice!.id, 'S02', 'incomplete: deriveState activeSlice is S02'); - assertTrue(state.activeTask !== null, 'incomplete: deriveState has activeTask'); - assertEq(state.activeTask!.id, 'T03', 'incomplete: deriveState activeTask is T03'); - assertTrue(state.progress!.slices !== undefined, 'incomplete: deriveState has slices progress'); - assertEq(state.progress!.slices!.done, 1, 'incomplete: deriveState slices done count'); - assertEq(state.progress!.slices!.total, 2, 'incomplete: deriveState slices total count'); - assertTrue(state.progress!.tasks !== undefined, 'incomplete: deriveState has tasks progress'); + assert.deepStrictEqual(state.phase, 'executing', 'incomplete: deriveState phase is executing'); + assert.ok(state.activeMilestone !== null, 'incomplete: deriveState has activeMilestone'); + assert.deepStrictEqual(state.activeMilestone!.id, 'M001', 'incomplete: deriveState activeMilestone is M001'); + assert.ok(state.activeSlice !== null, 'incomplete: deriveState has activeSlice'); + assert.deepStrictEqual(state.activeSlice!.id, 'S02', 'incomplete: deriveState activeSlice is S02'); + assert.ok(state.activeTask !== null, 'incomplete: deriveState has activeTask'); + assert.deepStrictEqual(state.activeTask!.id, 'T03', 'incomplete: deriveState activeTask is T03'); + assert.ok(state.progress!.slices !== undefined, 'incomplete: deriveState has slices progress'); + assert.deepStrictEqual(state.progress!.slices!.done, 1, 'incomplete: deriveState slices done count'); + assert.deepStrictEqual(state.progress!.slices!.total, 2, 'incomplete: deriveState slices total count'); + assert.ok(state.progress!.tasks !== undefined, 'incomplete: deriveState has tasks progress'); // S02 has 1 task, 0 done (only active slice tasks counted) - assertEq(state.progress!.tasks!.done, 0, 'incomplete: deriveState tasks done (in active slice)'); - assertEq(state.progress!.tasks!.total, 1, 'incomplete: deriveState tasks total (in active slice)'); + assert.deepStrictEqual(state.progress!.tasks!.done, 0, 'incomplete: deriveState tasks done (in active slice)'); + assert.deepStrictEqual(state.progress!.tasks!.total, 1, 'incomplete: deriveState tasks total (in active slice)'); // Requirements - assertEq(state.requirements!.active, 1, 'incomplete: deriveState requirements active'); - assertEq(state.requirements!.validated, 1, 'incomplete: deriveState requirements validated'); - assertEq(state.requirements!.deferred, 1, 'incomplete: deriveState requirements deferred'); - assertEq(state.requirements!.outOfScope, 1, 'incomplete: deriveState requirements outOfScope'); + assert.deepStrictEqual(state.requirements!.active, 1, 'incomplete: deriveState requirements active'); + assert.deepStrictEqual(state.requirements!.validated, 1, 'incomplete: deriveState requirements validated'); + assert.deepStrictEqual(state.requirements!.deferred, 1, 'incomplete: deriveState requirements deferred'); + assert.deepStrictEqual(state.requirements!.outOfScope, 1, 'incomplete: deriveState requirements outOfScope'); // (f) generatePreview console.log(' --- generatePreview ---'); const preview = generatePreview(project); - assertEq(preview.milestoneCount, 1, 'incomplete: preview milestoneCount'); - assertEq(preview.totalSlices, 2, 'incomplete: preview totalSlices'); - assertEq(preview.totalTasks, 3, 'incomplete: preview totalTasks'); - assertEq(preview.doneSlices, 1, 'incomplete: preview doneSlices'); - assertEq(preview.doneTasks, 2, 'incomplete: preview doneTasks'); - assertEq(preview.sliceCompletionPct, 50, 'incomplete: preview sliceCompletionPct'); - assertEq(preview.taskCompletionPct, 67, 'incomplete: preview taskCompletionPct'); - assertEq(preview.requirements.active, 1, 'incomplete: preview requirements active'); - assertEq(preview.requirements.validated, 1, 'incomplete: preview requirements validated'); - assertEq(preview.requirements.deferred, 1, 'incomplete: preview requirements deferred'); - assertEq(preview.requirements.outOfScope, 1, 'incomplete: preview requirements outOfScope'); - assertEq(preview.requirements.total, 4, 'incomplete: preview requirements total'); + assert.deepStrictEqual(preview.milestoneCount, 1, 'incomplete: preview milestoneCount'); + assert.deepStrictEqual(preview.totalSlices, 2, 'incomplete: preview totalSlices'); + assert.deepStrictEqual(preview.totalTasks, 3, 'incomplete: preview totalTasks'); + assert.deepStrictEqual(preview.doneSlices, 1, 'incomplete: preview doneSlices'); + assert.deepStrictEqual(preview.doneTasks, 2, 'incomplete: preview doneTasks'); + assert.deepStrictEqual(preview.sliceCompletionPct, 50, 'incomplete: preview sliceCompletionPct'); + assert.deepStrictEqual(preview.taskCompletionPct, 67, 'incomplete: preview taskCompletionPct'); + assert.deepStrictEqual(preview.requirements.active, 1, 'incomplete: preview requirements active'); + assert.deepStrictEqual(preview.requirements.validated, 1, 'incomplete: preview requirements validated'); + assert.deepStrictEqual(preview.requirements.deferred, 1, 'incomplete: preview requirements deferred'); + assert.deepStrictEqual(preview.requirements.outOfScope, 1, 'incomplete: preview requirements outOfScope'); + assert.deepStrictEqual(preview.requirements.total, 4, 'incomplete: preview requirements total'); } finally { rmSync(base, { recursive: true, force: true }); } - } +}); // ─── Scenario 2: Fully complete project ──────────────────────────────── - console.log('\n=== Scenario 2: Fully complete project — deriveState phase ==='); - { + +test('Scenario 2: Fully complete project — deriveState phase', async () => { const base = mkdtempSync(join(tmpdir(), 'gsd-writer-int-complete-')); try { const project = buildCompleteProject(); @@ -262,43 +260,36 @@ async function main(): Promise { // Null research should NOT produce a file const m = join(base, '.gsd', 'milestones', 'M001'); - assertTrue(!existsSync(join(m, 'M001-RESEARCH.md')), 'complete: M001-RESEARCH.md NOT written (null)'); + assert.ok(!existsSync(join(m, 'M001-RESEARCH.md')), 'complete: M001-RESEARCH.md NOT written (null)'); // No REQUIREMENTS.md since empty requirements - assertTrue(!existsSync(join(base, '.gsd', 'REQUIREMENTS.md')), 'complete: REQUIREMENTS.md NOT written (empty)'); + assert.ok(!existsSync(join(base, '.gsd', 'REQUIREMENTS.md')), 'complete: REQUIREMENTS.md NOT written (empty)'); // Completed milestone should have VALIDATION and SUMMARY from migration (#819) - assertTrue(existsSync(join(m, 'M001-VALIDATION.md')), 'complete: M001-VALIDATION.md written for completed milestone'); - assertTrue(existsSync(join(m, 'M001-SUMMARY.md')), 'complete: M001-SUMMARY.md written for completed milestone'); + assert.ok(existsSync(join(m, 'M001-VALIDATION.md')), 'complete: M001-VALIDATION.md written for completed milestone'); + assert.ok(existsSync(join(m, 'M001-SUMMARY.md')), 'complete: M001-SUMMARY.md written for completed milestone'); // deriveState: all slices done, all tasks done — migration now writes // VALIDATION.md and SUMMARY.md for completed milestones (#819), // so the milestone should be fully complete. invalidateAllCaches(); const state = await deriveState(base); - assertEq(state.phase, 'complete', 'complete: deriveState phase is complete (validation + summary written by migration)'); + assert.deepStrictEqual(state.phase, 'complete', 'complete: deriveState phase is complete (validation + summary written by migration)'); // When all milestones are complete, activeMilestone points to the last entry (for display) - assertTrue(state.activeMilestone !== null, 'complete: deriveState has activeMilestone (last entry)'); - assertEq(state.activeMilestone!.id, 'M001', 'complete: deriveState activeMilestone is M001'); + assert.ok(state.activeMilestone !== null, 'complete: deriveState has activeMilestone (last entry)'); + assert.deepStrictEqual(state.activeMilestone!.id, 'M001', 'complete: deriveState activeMilestone is M001'); // generatePreview for complete project const preview = generatePreview(project); - assertEq(preview.milestoneCount, 1, 'complete: preview milestoneCount'); - assertEq(preview.totalSlices, 1, 'complete: preview totalSlices'); - assertEq(preview.doneSlices, 1, 'complete: preview doneSlices'); - assertEq(preview.totalTasks, 1, 'complete: preview totalTasks'); - assertEq(preview.doneTasks, 1, 'complete: preview doneTasks'); - assertEq(preview.sliceCompletionPct, 100, 'complete: preview sliceCompletionPct'); - assertEq(preview.taskCompletionPct, 100, 'complete: preview taskCompletionPct'); - assertEq(preview.requirements.total, 0, 'complete: preview requirements total'); + assert.deepStrictEqual(preview.milestoneCount, 1, 'complete: preview milestoneCount'); + assert.deepStrictEqual(preview.totalSlices, 1, 'complete: preview totalSlices'); + assert.deepStrictEqual(preview.doneSlices, 1, 'complete: preview doneSlices'); + assert.deepStrictEqual(preview.totalTasks, 1, 'complete: preview totalTasks'); + assert.deepStrictEqual(preview.doneTasks, 1, 'complete: preview doneTasks'); + assert.deepStrictEqual(preview.sliceCompletionPct, 100, 'complete: preview sliceCompletionPct'); + assert.deepStrictEqual(preview.taskCompletionPct, 100, 'complete: preview taskCompletionPct'); + assert.deepStrictEqual(preview.requirements.total, 0, 'complete: preview requirements total'); } finally { rmSync(base, { recursive: true, force: true }); } - } - - report(); -} - -main().catch((err) => { - console.error('Unhandled error:', err); - process.exit(1); }); + diff --git a/src/resources/extensions/gsd/tests/migrate-writer.test.ts b/src/resources/extensions/gsd/tests/migrate-writer.test.ts index c779f2e31..cc5ea38dd 100644 --- a/src/resources/extensions/gsd/tests/migrate-writer.test.ts +++ b/src/resources/extensions/gsd/tests/migrate-writer.test.ts @@ -31,9 +31,9 @@ import type { GSDSliceSummaryData, GSDTaskSummaryData, } from '../migrate/types.ts'; -import { createTestContext } from './test-helpers.ts'; +import { describe, test, beforeEach, afterEach } from 'node:test'; +import assert from 'node:assert/strict'; -const { assertEq, assertTrue, report } = createTestContext(); // ─── Test Data Builders ──────────────────────────────────────────────────── function makeTask(overrides: Partial = {}): GSDTask { @@ -103,11 +103,7 @@ function makeTaskSummary(overrides: Partial = {}): GSDTaskSu }; } -// ═══════════════════════════════════════════════════════════════════════════ -// Scenario A: Roadmap round-trip with 2 slices (1 done, 1 not) -// ═══════════════════════════════════════════════════════════════════════════ - -{ +test('Scenario A: Roadmap round-trip with 2 slices (1 done, 1 not)', () => { const milestone = makeMilestone({ slices: [ makeSlice({ @@ -132,35 +128,31 @@ function makeTaskSummary(overrides: Partial = {}): GSDTaskSu const output = formatRoadmap(milestone); const parsed = parseRoadmap(output); - assertEq(parsed.title, 'M001: Core Platform', 'roadmap: title'); - assertEq(parsed.vision, 'Build the core platform', 'roadmap: vision'); - assertEq(parsed.successCriteria.length, 2, 'roadmap: successCriteria count'); - assertEq(parsed.successCriteria[0], 'All tests pass', 'roadmap: successCriteria[0]'); - assertEq(parsed.successCriteria[1], 'Deploy to staging', 'roadmap: successCriteria[1]'); - assertEq(parsed.slices.length, 2, 'roadmap: slices count'); + assert.deepStrictEqual(parsed.title, 'M001: Core Platform', 'roadmap: title'); + assert.deepStrictEqual(parsed.vision, 'Build the core platform', 'roadmap: vision'); + assert.deepStrictEqual(parsed.successCriteria.length, 2, 'roadmap: successCriteria count'); + assert.deepStrictEqual(parsed.successCriteria[0], 'All tests pass', 'roadmap: successCriteria[0]'); + assert.deepStrictEqual(parsed.successCriteria[1], 'Deploy to staging', 'roadmap: successCriteria[1]'); + assert.deepStrictEqual(parsed.slices.length, 2, 'roadmap: slices count'); - assertEq(parsed.slices[0].id, 'S01', 'roadmap: S01 id'); - assertEq(parsed.slices[0].title, 'Auth System', 'roadmap: S01 title'); - assertEq(parsed.slices[0].done, true, 'roadmap: S01 done'); - assertEq(parsed.slices[0].risk, 'high', 'roadmap: S01 risk'); - assertEq(parsed.slices[0].depends.length, 0, 'roadmap: S01 depends empty'); - assertEq(parsed.slices[0].demo, 'Login flow works', 'roadmap: S01 demo'); + assert.deepStrictEqual(parsed.slices[0].id, 'S01', 'roadmap: S01 id'); + assert.deepStrictEqual(parsed.slices[0].title, 'Auth System', 'roadmap: S01 title'); + assert.deepStrictEqual(parsed.slices[0].done, true, 'roadmap: S01 done'); + assert.deepStrictEqual(parsed.slices[0].risk, 'high', 'roadmap: S01 risk'); + assert.deepStrictEqual(parsed.slices[0].depends.length, 0, 'roadmap: S01 depends empty'); + assert.deepStrictEqual(parsed.slices[0].demo, 'Login flow works', 'roadmap: S01 demo'); - assertEq(parsed.slices[1].id, 'S02', 'roadmap: S02 id'); - assertEq(parsed.slices[1].title, 'Dashboard', 'roadmap: S02 title'); - assertEq(parsed.slices[1].done, false, 'roadmap: S02 done'); - assertEq(parsed.slices[1].risk, 'low', 'roadmap: S02 risk'); - assertEq(parsed.slices[1].depends, ['S01'], 'roadmap: S02 depends'); - assertEq(parsed.slices[1].demo, 'Dashboard renders data', 'roadmap: S02 demo'); + assert.deepStrictEqual(parsed.slices[1].id, 'S02', 'roadmap: S02 id'); + assert.deepStrictEqual(parsed.slices[1].title, 'Dashboard', 'roadmap: S02 title'); + assert.deepStrictEqual(parsed.slices[1].done, false, 'roadmap: S02 done'); + assert.deepStrictEqual(parsed.slices[1].risk, 'low', 'roadmap: S02 risk'); + assert.deepStrictEqual(parsed.slices[1].depends, ['S01'], 'roadmap: S02 depends'); + assert.deepStrictEqual(parsed.slices[1].demo, 'Dashboard renders data', 'roadmap: S02 demo'); - assertEq(parsed.boundaryMap.length, 0, 'roadmap: boundaryMap empty'); -} + assert.deepStrictEqual(parsed.boundaryMap.length, 0, 'roadmap: boundaryMap empty'); +}); -// ═══════════════════════════════════════════════════════════════════════════ -// Scenario B: Plan round-trip with 3 tasks (mixed done) -// ═══════════════════════════════════════════════════════════════════════════ - -{ +test('Scenario B: Plan round-trip with 3 tasks (mixed done)', () => { const slice = makeSlice({ id: 'S01', title: 'Auth System', @@ -176,31 +168,27 @@ function makeTaskSummary(overrides: Partial = {}): GSDTaskSu const output = formatPlan(slice); const parsed = parsePlan(output); - assertEq(parsed.id, 'S01', 'plan: id'); - assertEq(parsed.title, 'Auth System', 'plan: title'); - assertEq(parsed.goal, 'Working authentication system', 'plan: goal'); - assertEq(parsed.demo, 'Login works with valid credentials', 'plan: demo'); - assertEq(parsed.tasks.length, 3, 'plan: tasks count'); + assert.deepStrictEqual(parsed.id, 'S01', 'plan: id'); + assert.deepStrictEqual(parsed.title, 'Auth System', 'plan: title'); + assert.deepStrictEqual(parsed.goal, 'Working authentication system', 'plan: goal'); + assert.deepStrictEqual(parsed.demo, 'Login works with valid credentials', 'plan: demo'); + assert.deepStrictEqual(parsed.tasks.length, 3, 'plan: tasks count'); - assertEq(parsed.tasks[0].id, 'T01', 'plan: T01 id'); - assertEq(parsed.tasks[0].title, 'Setup Models', 'plan: T01 title'); - assertEq(parsed.tasks[0].done, true, 'plan: T01 done'); - assertEq(parsed.tasks[0].estimate, '15m', 'plan: T01 estimate'); + assert.deepStrictEqual(parsed.tasks[0].id, 'T01', 'plan: T01 id'); + assert.deepStrictEqual(parsed.tasks[0].title, 'Setup Models', 'plan: T01 title'); + assert.deepStrictEqual(parsed.tasks[0].done, true, 'plan: T01 done'); + assert.deepStrictEqual(parsed.tasks[0].estimate, '15m', 'plan: T01 estimate'); - assertEq(parsed.tasks[1].id, 'T02', 'plan: T02 id'); - assertEq(parsed.tasks[1].done, false, 'plan: T02 done'); - assertEq(parsed.tasks[1].estimate, '30m', 'plan: T02 estimate'); + assert.deepStrictEqual(parsed.tasks[1].id, 'T02', 'plan: T02 id'); + assert.deepStrictEqual(parsed.tasks[1].done, false, 'plan: T02 done'); + assert.deepStrictEqual(parsed.tasks[1].estimate, '30m', 'plan: T02 estimate'); - assertEq(parsed.tasks[2].id, 'T03', 'plan: T03 id'); - assertEq(parsed.tasks[2].done, true, 'plan: T03 done'); - assertEq(parsed.tasks[2].estimate, '20m', 'plan: T03 estimate'); -} + assert.deepStrictEqual(parsed.tasks[2].id, 'T03', 'plan: T03 id'); + assert.deepStrictEqual(parsed.tasks[2].done, true, 'plan: T03 done'); + assert.deepStrictEqual(parsed.tasks[2].estimate, '20m', 'plan: T03 estimate'); +}); -// ═══════════════════════════════════════════════════════════════════════════ -// Scenario C: Slice summary round-trip with full data -// ═══════════════════════════════════════════════════════════════════════════ - -{ +test('Scenario C: Slice summary round-trip with full data', () => { const slice = makeSlice({ id: 'S01', title: 'Auth System', @@ -211,28 +199,24 @@ function makeTaskSummary(overrides: Partial = {}): GSDTaskSu const output = formatSliceSummary(slice, 'M001'); const parsed = parseSummary(output); - assertEq(parsed.frontmatter.id, 'S01', 'sliceSummary: id'); - assertEq(parsed.frontmatter.parent, 'M001', 'sliceSummary: parent'); - assertEq(parsed.frontmatter.milestone, 'M001', 'sliceSummary: milestone'); - assertEq(parsed.frontmatter.provides, ['auth-flow', 'jwt-tokens'], 'sliceSummary: provides'); - assertEq(parsed.frontmatter.requires.length, 0, 'sliceSummary: requires empty'); - assertEq(parsed.frontmatter.affects.length, 0, 'sliceSummary: affects empty'); - assertEq(parsed.frontmatter.key_files, ['src/auth.ts', 'src/middleware.ts'], 'sliceSummary: key_files'); - assertEq(parsed.frontmatter.key_decisions, ['Use JWT over sessions'], 'sliceSummary: key_decisions'); - assertEq(parsed.frontmatter.patterns_established, ['Middleware pattern'], 'sliceSummary: patterns_established'); - assertEq(parsed.frontmatter.duration, '2h', 'sliceSummary: duration'); - assertEq(parsed.frontmatter.completed_at, '2026-03-10', 'sliceSummary: completed_at'); - assertEq(parsed.frontmatter.verification_result, 'passed', 'sliceSummary: verification_result'); - assertEq(parsed.frontmatter.blocker_discovered, false, 'sliceSummary: blocker_discovered'); - assertTrue(parsed.whatHappened.includes('Implemented full auth system'), 'sliceSummary: whatHappened content'); - assertEq(parsed.title, 'S01: Auth System', 'sliceSummary: title'); -} + assert.deepStrictEqual(parsed.frontmatter.id, 'S01', 'sliceSummary: id'); + assert.deepStrictEqual(parsed.frontmatter.parent, 'M001', 'sliceSummary: parent'); + assert.deepStrictEqual(parsed.frontmatter.milestone, 'M001', 'sliceSummary: milestone'); + assert.deepStrictEqual(parsed.frontmatter.provides, ['auth-flow', 'jwt-tokens'], 'sliceSummary: provides'); + assert.deepStrictEqual(parsed.frontmatter.requires.length, 0, 'sliceSummary: requires empty'); + assert.deepStrictEqual(parsed.frontmatter.affects.length, 0, 'sliceSummary: affects empty'); + assert.deepStrictEqual(parsed.frontmatter.key_files, ['src/auth.ts', 'src/middleware.ts'], 'sliceSummary: key_files'); + assert.deepStrictEqual(parsed.frontmatter.key_decisions, ['Use JWT over sessions'], 'sliceSummary: key_decisions'); + assert.deepStrictEqual(parsed.frontmatter.patterns_established, ['Middleware pattern'], 'sliceSummary: patterns_established'); + assert.deepStrictEqual(parsed.frontmatter.duration, '2h', 'sliceSummary: duration'); + assert.deepStrictEqual(parsed.frontmatter.completed_at, '2026-03-10', 'sliceSummary: completed_at'); + assert.deepStrictEqual(parsed.frontmatter.verification_result, 'passed', 'sliceSummary: verification_result'); + assert.deepStrictEqual(parsed.frontmatter.blocker_discovered, false, 'sliceSummary: blocker_discovered'); + assert.ok(parsed.whatHappened.includes('Implemented full auth system'), 'sliceSummary: whatHappened content'); + assert.deepStrictEqual(parsed.title, 'S01: Auth System', 'sliceSummary: title'); +}); -// ═══════════════════════════════════════════════════════════════════════════ -// Scenario D: Task summary round-trip -// ═══════════════════════════════════════════════════════════════════════════ - -{ +test('Scenario D: Task summary round-trip', () => { const task = makeTask({ id: 'T01', title: 'Setup Auth', @@ -243,22 +227,18 @@ function makeTaskSummary(overrides: Partial = {}): GSDTaskSu const output = formatTaskSummary(task, 'S01', 'M001'); const parsed = parseSummary(output); - assertEq(parsed.frontmatter.id, 'T01', 'taskSummary: id'); - assertEq(parsed.frontmatter.parent, 'S01', 'taskSummary: parent'); - assertEq(parsed.frontmatter.milestone, 'M001', 'taskSummary: milestone'); - assertEq(parsed.frontmatter.provides, ['auth-endpoint'], 'taskSummary: provides'); - assertEq(parsed.frontmatter.key_files, ['src/auth.ts'], 'taskSummary: key_files'); - assertEq(parsed.frontmatter.duration, '45m', 'taskSummary: duration'); - assertEq(parsed.frontmatter.completed_at, '2026-03-09', 'taskSummary: completed_at'); - assertTrue(parsed.whatHappened.includes('Built the auth endpoint'), 'taskSummary: whatHappened content'); - assertEq(parsed.title, 'T01: Setup Auth', 'taskSummary: title'); -} + assert.deepStrictEqual(parsed.frontmatter.id, 'T01', 'taskSummary: id'); + assert.deepStrictEqual(parsed.frontmatter.parent, 'S01', 'taskSummary: parent'); + assert.deepStrictEqual(parsed.frontmatter.milestone, 'M001', 'taskSummary: milestone'); + assert.deepStrictEqual(parsed.frontmatter.provides, ['auth-endpoint'], 'taskSummary: provides'); + assert.deepStrictEqual(parsed.frontmatter.key_files, ['src/auth.ts'], 'taskSummary: key_files'); + assert.deepStrictEqual(parsed.frontmatter.duration, '45m', 'taskSummary: duration'); + assert.deepStrictEqual(parsed.frontmatter.completed_at, '2026-03-09', 'taskSummary: completed_at'); + assert.ok(parsed.whatHappened.includes('Built the auth endpoint'), 'taskSummary: whatHappened content'); + assert.deepStrictEqual(parsed.title, 'T01: Setup Auth', 'taskSummary: title'); +}); -// ═══════════════════════════════════════════════════════════════════════════ -// Scenario E: Requirements round-trip with mixed statuses -// ═══════════════════════════════════════════════════════════════════════════ - -{ +test('Scenario E: Requirements round-trip with mixed statuses', () => { const requirements: GSDRequirement[] = [ { id: 'R001', title: 'Auth Required', class: 'core-capability', status: 'active', description: 'Must have auth', source: 'spec', primarySlice: 'S01' }, { id: 'R002', title: 'Logging', class: 'observability', status: 'active', description: 'Must log', source: 'spec', primarySlice: 'S02' }, @@ -270,110 +250,93 @@ function makeTaskSummary(overrides: Partial = {}): GSDTaskSu const output = formatRequirements(requirements); const counts = parseRequirementCounts(output); - assertEq(counts.active, 2, 'requirements: active count'); - assertEq(counts.validated, 1, 'requirements: validated count'); - assertEq(counts.deferred, 1, 'requirements: deferred count'); - assertEq(counts.outOfScope, 1, 'requirements: outOfScope count'); - assertEq(counts.total, 5, 'requirements: total count'); -} + assert.deepStrictEqual(counts.active, 2, 'requirements: active count'); + assert.deepStrictEqual(counts.validated, 1, 'requirements: validated count'); + assert.deepStrictEqual(counts.deferred, 1, 'requirements: deferred count'); + assert.deepStrictEqual(counts.outOfScope, 1, 'requirements: outOfScope count'); + assert.deepStrictEqual(counts.total, 5, 'requirements: total count'); +}); -// ═══════════════════════════════════════════════════════════════════════════ -// Scenario F: Edge cases -// ═══════════════════════════════════════════════════════════════════════════ - -// F1: Empty vision → fallback text -{ +test('F1: Empty vision → fallback text', () => { const milestone = makeMilestone({ vision: '' }); const output = formatRoadmap(milestone); const parsed = parseRoadmap(output); - assertEq(parsed.vision, '(migrated project)', 'edge: empty vision fallback'); -} + assert.deepStrictEqual(parsed.vision, '(migrated project)', 'edge: empty vision fallback'); +}); -// F2: Empty successCriteria → empty array -{ +test('F2: Empty successCriteria → empty array', () => { const milestone = makeMilestone({ successCriteria: [] }); const output = formatRoadmap(milestone); const parsed = parseRoadmap(output); - assertEq(parsed.successCriteria.length, 0, 'edge: empty successCriteria'); -} + assert.deepStrictEqual(parsed.successCriteria.length, 0, 'edge: empty successCriteria'); +}); -// F3: Empty tasks → empty array in parsed plan -{ +test('F3: Empty tasks → empty array in parsed plan', () => { const slice = makeSlice({ tasks: [] }); const output = formatPlan(slice); const parsed = parsePlan(output); - assertEq(parsed.tasks.length, 0, 'edge: empty tasks'); -} + assert.deepStrictEqual(parsed.tasks.length, 0, 'edge: empty tasks'); +}); -// F4: Null summary → empty string from formatSliceSummary -{ +test('F4: Null summary → empty string from formatSliceSummary', () => { const slice = makeSlice({ summary: null }); const output = formatSliceSummary(slice, 'M001'); - assertEq(output, '', 'edge: null summary returns empty string'); -} + assert.deepStrictEqual(output, '', 'edge: null summary returns empty string'); +}); -// F5: Done=true checkbox in roadmap -{ +test('F5: Done=true checkbox in roadmap', () => { const milestone = makeMilestone({ slices: [makeSlice({ id: 'S01', done: true })], }); const output = formatRoadmap(milestone); const parsed = parseRoadmap(output); - assertEq(parsed.slices[0].done, true, 'edge: done checkbox true'); -} + assert.deepStrictEqual(parsed.slices[0].done, true, 'edge: done checkbox true'); +}); -// F6: Done=false checkbox in roadmap -{ +test('F6: Done=false checkbox in roadmap', () => { const milestone = makeMilestone({ slices: [makeSlice({ id: 'S01', done: false })], }); const output = formatRoadmap(milestone); const parsed = parseRoadmap(output); - assertEq(parsed.slices[0].done, false, 'edge: done checkbox false'); -} + assert.deepStrictEqual(parsed.slices[0].done, false, 'edge: done checkbox false'); +}); -// F7: Null task summary → empty string from formatTaskSummary -{ +test('F7: Null task summary → empty string from formatTaskSummary', () => { const task = makeTask({ summary: null }); const output = formatTaskSummary(task, 'S01', 'M001'); - assertEq(output, '', 'edge: null task summary returns empty string'); -} + assert.deepStrictEqual(output, '', 'edge: null task summary returns empty string'); +}); -// F8: Empty requirements → all zeros -{ +test('F8: Empty requirements → all zeros', () => { const output = formatRequirements([]); const counts = parseRequirementCounts(output); - assertEq(counts.total, 0, 'edge: empty requirements total 0'); -} + assert.deepStrictEqual(counts.total, 0, 'edge: empty requirements total 0'); +}); -// F9: formatProject with empty content → produces valid stub -{ +test('F9: formatProject with empty content → produces valid stub', () => { const output = formatProject(''); - assertTrue(output.includes('# Project'), 'edge: empty project has heading'); - assertTrue(output.length > 10, 'edge: empty project not blank'); -} + assert.ok(output.includes('# Project'), 'edge: empty project has heading'); + assert.ok(output.length > 10, 'edge: empty project not blank'); +}); -// F10: formatProject with existing content → passes through -{ +test('F10: formatProject with existing content → passes through', () => { const content = '# My Project\n\nDescription here.\n'; const output = formatProject(content); - assertEq(output, content, 'edge: project passthrough'); -} + assert.deepStrictEqual(output, content, 'edge: project passthrough'); +}); -// F11: formatDecisions with empty content → produces valid stub -{ +test('F11: formatDecisions with empty content → produces valid stub', () => { const output = formatDecisions(''); - assertTrue(output.includes('# Decisions'), 'edge: empty decisions has heading'); -} + assert.ok(output.includes('# Decisions'), 'edge: empty decisions has heading'); +}); -// F12: formatContext produces valid content -{ +test('F12: formatContext produces valid content', () => { const output = formatContext('M001'); - assertTrue(output.includes('M001'), 'edge: context mentions milestone'); -} + assert.ok(output.includes('M001'), 'edge: context mentions milestone'); +}); -// F13: formatState produces valid content -{ +test('F13: formatState produces valid content', () => { const milestones = [makeMilestone({ slices: [ makeSlice({ done: true }), @@ -381,20 +344,18 @@ function makeTaskSummary(overrides: Partial = {}): GSDTaskSu ], })]; const output = formatState(milestones); - assertTrue(output.includes('1/2'), 'edge: state shows slice progress'); -} + assert.ok(output.includes('1/2'), 'edge: state shows slice progress'); +}); -// F14: Task with no estimate → no est backtick in plan -{ +test('F14: Task with no estimate → no est backtick in plan', () => { const slice = makeSlice({ tasks: [makeTask({ id: 'T01', title: 'Quick Fix', estimate: '' })], }); const output = formatPlan(slice); const parsed = parsePlan(output); - assertEq(parsed.tasks[0].id, 'T01', 'edge: task no estimate id'); - assertEq(parsed.tasks[0].estimate, '', 'edge: task no estimate empty'); -} + assert.deepStrictEqual(parsed.tasks[0].id, 'T01', 'edge: task no estimate id'); + assert.deepStrictEqual(parsed.tasks[0].estimate, '', 'edge: task no estimate empty'); +}); // ═══════════════════════════════════════════════════════════════════════════ -report(); diff --git a/src/resources/extensions/gsd/tests/must-have-parser.test.ts b/src/resources/extensions/gsd/tests/must-have-parser.test.ts index 23cfa4c81..28eb19c98 100644 --- a/src/resources/extensions/gsd/tests/must-have-parser.test.ts +++ b/src/resources/extensions/gsd/tests/must-have-parser.test.ts @@ -1,13 +1,12 @@ import { parseTaskPlanMustHaves } from '../files.ts'; -import { createTestContext } from './test-helpers.ts'; +import { describe, test, beforeEach, afterEach } from 'node:test'; +import assert from 'node:assert/strict'; -const { assertEq, assertTrue, report } = createTestContext(); // ═══════════════════════════════════════════════════════════════════════════ // (a) Standard unchecked format: - [ ] text // ═══════════════════════════════════════════════════════════════════════════ -console.log('\n=== parseTaskPlanMustHaves: standard unchecked ==='); -{ +test('parseTaskPlanMustHaves: standard unchecked', () => { const content = `# T01: Test Task ## Must-Haves @@ -16,56 +15,53 @@ console.log('\n=== parseTaskPlanMustHaves: standard unchecked ==='); - [ ] Second must-have item `; const result = parseTaskPlanMustHaves(content); - assertEq(result.length, 2, 'should return 2 items'); - assertEq(result[0].text, 'First must-have item', 'first item text'); - assertEq(result[0].checked, false, 'first item unchecked'); - assertEq(result[1].text, 'Second must-have item', 'second item text'); - assertEq(result[1].checked, false, 'second item unchecked'); -} + assert.deepStrictEqual(result.length, 2, 'should return 2 items'); + assert.deepStrictEqual(result[0].text, 'First must-have item', 'first item text'); + assert.deepStrictEqual(result[0].checked, false, 'first item unchecked'); + assert.deepStrictEqual(result[1].text, 'Second must-have item', 'second item text'); + assert.deepStrictEqual(result[1].checked, false, 'second item unchecked'); +}); // ═══════════════════════════════════════════════════════════════════════════ // (b) Checked variants: - [x] and - [X] // ═══════════════════════════════════════════════════════════════════════════ -console.log('\n=== parseTaskPlanMustHaves: checked [x] and [X] ==='); -{ +test('parseTaskPlanMustHaves: checked [x] and [X]', () => { const content = `## Must-Haves - [x] Lowercase checked item - [X] Uppercase checked item `; const result = parseTaskPlanMustHaves(content); - assertEq(result.length, 2, 'should return 2 items'); - assertEq(result[0].checked, true, 'lowercase x is checked'); - assertEq(result[0].text, 'Lowercase checked item', 'lowercase x text'); - assertEq(result[1].checked, true, 'uppercase X is checked'); - assertEq(result[1].text, 'Uppercase checked item', 'uppercase X text'); -} + assert.deepStrictEqual(result.length, 2, 'should return 2 items'); + assert.deepStrictEqual(result[0].checked, true, 'lowercase x is checked'); + assert.deepStrictEqual(result[0].text, 'Lowercase checked item', 'lowercase x text'); + assert.deepStrictEqual(result[1].checked, true, 'uppercase X is checked'); + assert.deepStrictEqual(result[1].text, 'Uppercase checked item', 'uppercase X text'); +}); // ═══════════════════════════════════════════════════════════════════════════ // (c) No-checkbox bullets: - text // ═══════════════════════════════════════════════════════════════════════════ -console.log('\n=== parseTaskPlanMustHaves: no-checkbox bullets ==='); -{ +test('parseTaskPlanMustHaves: no-checkbox bullets', () => { const content = `## Must-Haves - Plain bullet item - Another plain item `; const result = parseTaskPlanMustHaves(content); - assertEq(result.length, 2, 'should return 2 items'); - assertEq(result[0].text, 'Plain bullet item', 'plain bullet text'); - assertEq(result[0].checked, false, 'plain bullet defaults to unchecked'); - assertEq(result[1].text, 'Another plain item', 'second plain bullet text'); -} + assert.deepStrictEqual(result.length, 2, 'should return 2 items'); + assert.deepStrictEqual(result[0].text, 'Plain bullet item', 'plain bullet text'); + assert.deepStrictEqual(result[0].checked, false, 'plain bullet defaults to unchecked'); + assert.deepStrictEqual(result[1].text, 'Another plain item', 'second plain bullet text'); +}); // ═══════════════════════════════════════════════════════════════════════════ // (d) Indented variants // ═══════════════════════════════════════════════════════════════════════════ -console.log('\n=== parseTaskPlanMustHaves: indented variants ==='); -{ +test('parseTaskPlanMustHaves: indented variants', () => { const content = `## Must-Haves - [ ] Indented unchecked item @@ -73,21 +69,20 @@ console.log('\n=== parseTaskPlanMustHaves: indented variants ==='); - Plain indented item `; const result = parseTaskPlanMustHaves(content); - assertEq(result.length, 3, 'should return 3 items'); - assertEq(result[0].text, 'Indented unchecked item', 'indented unchecked text'); - assertEq(result[0].checked, false, 'indented unchecked state'); - assertEq(result[1].text, 'Indented checked item', 'indented checked text'); - assertEq(result[1].checked, true, 'indented checked state'); - assertEq(result[2].text, 'Plain indented item', 'indented plain text'); - assertEq(result[2].checked, false, 'indented plain state'); -} + assert.deepStrictEqual(result.length, 3, 'should return 3 items'); + assert.deepStrictEqual(result[0].text, 'Indented unchecked item', 'indented unchecked text'); + assert.deepStrictEqual(result[0].checked, false, 'indented unchecked state'); + assert.deepStrictEqual(result[1].text, 'Indented checked item', 'indented checked text'); + assert.deepStrictEqual(result[1].checked, true, 'indented checked state'); + assert.deepStrictEqual(result[2].text, 'Plain indented item', 'indented plain text'); + assert.deepStrictEqual(result[2].checked, false, 'indented plain state'); +}); // ═══════════════════════════════════════════════════════════════════════════ // (e) Mixed checkbox states in one section // ═══════════════════════════════════════════════════════════════════════════ -console.log('\n=== parseTaskPlanMustHaves: mixed states ==='); -{ +test('parseTaskPlanMustHaves: mixed states', () => { const content = `## Must-Haves - [ ] Unchecked one @@ -97,20 +92,19 @@ console.log('\n=== parseTaskPlanMustHaves: mixed states ==='); - [ ] Another unchecked `; const result = parseTaskPlanMustHaves(content); - assertEq(result.length, 5, 'should return 5 items'); - assertEq(result[0].checked, false, 'first is unchecked'); - assertEq(result[1].checked, true, 'second is checked'); - assertEq(result[2].checked, true, 'third is checked (uppercase)'); - assertEq(result[3].checked, false, 'fourth (plain) is unchecked'); - assertEq(result[4].checked, false, 'fifth is unchecked'); -} + assert.deepStrictEqual(result.length, 5, 'should return 5 items'); + assert.deepStrictEqual(result[0].checked, false, 'first is unchecked'); + assert.deepStrictEqual(result[1].checked, true, 'second is checked'); + assert.deepStrictEqual(result[2].checked, true, 'third is checked (uppercase)'); + assert.deepStrictEqual(result[3].checked, false, 'fourth (plain) is unchecked'); + assert.deepStrictEqual(result[4].checked, false, 'fifth is unchecked'); +}); // ═══════════════════════════════════════════════════════════════════════════ // (f) Missing Must-Haves section → empty array // ═══════════════════════════════════════════════════════════════════════════ -console.log('\n=== parseTaskPlanMustHaves: missing section ==='); -{ +test('parseTaskPlanMustHaves: missing section', () => { const content = `# T01: Some Task ## Description @@ -122,16 +116,15 @@ Some description here. - Run tests `; const result = parseTaskPlanMustHaves(content); - assertEq(result.length, 0, 'returns empty array when section missing'); - assertTrue(Array.isArray(result), 'result is an array'); -} + assert.deepStrictEqual(result.length, 0, 'returns empty array when section missing'); + assert.ok(Array.isArray(result), 'result is an array'); +}); // ═══════════════════════════════════════════════════════════════════════════ // (g) Empty Must-Haves section → empty array // ═══════════════════════════════════════════════════════════════════════════ -console.log('\n=== parseTaskPlanMustHaves: empty section ==='); -{ +test('parseTaskPlanMustHaves: empty section', () => { const content = `## Must-Haves ## Verification @@ -139,15 +132,14 @@ console.log('\n=== parseTaskPlanMustHaves: empty section ==='); - Run tests `; const result = parseTaskPlanMustHaves(content); - assertEq(result.length, 0, 'returns empty array when section is empty'); -} + assert.deepStrictEqual(result.length, 0, 'returns empty array when section is empty'); +}); // ═══════════════════════════════════════════════════════════════════════════ // (h) Content with YAML frontmatter // ═══════════════════════════════════════════════════════════════════════════ -console.log('\n=== parseTaskPlanMustHaves: YAML frontmatter ==='); -{ +test('parseTaskPlanMustHaves: YAML frontmatter', () => { const content = `--- estimated_steps: 5 estimated_files: 3 @@ -161,16 +153,16 @@ estimated_files: 3 - [x] Checked must-have after frontmatter `; const result = parseTaskPlanMustHaves(content); - assertEq(result.length, 2, 'frontmatter does not pollute results'); - assertEq(result[0].text, 'Real must-have after frontmatter', 'first item text correct'); - assertEq(result[0].checked, false, 'first item unchecked'); - assertEq(result[1].text, 'Checked must-have after frontmatter', 'second item text correct'); - assertEq(result[1].checked, true, 'second item checked'); -} + assert.deepStrictEqual(result.length, 2, 'frontmatter does not pollute results'); + assert.deepStrictEqual(result[0].text, 'Real must-have after frontmatter', 'first item text correct'); + assert.deepStrictEqual(result[0].checked, false, 'first item unchecked'); + assert.deepStrictEqual(result[1].text, 'Checked must-have after frontmatter', 'second item text correct'); + assert.deepStrictEqual(result[1].checked, true, 'second item checked'); +}); // Verify frontmatter content is not misinterpreted as must-haves -console.log('\n=== parseTaskPlanMustHaves: frontmatter-only content ==='); -{ + +test('parseTaskPlanMustHaves: frontmatter-only content', () => { const content = `--- estimated_steps: 5 estimated_files: 3 @@ -183,15 +175,14 @@ estimated_files: 3 No must-haves section here. `; const result = parseTaskPlanMustHaves(content); - assertEq(result.length, 0, 'frontmatter-only content returns empty array'); -} + assert.deepStrictEqual(result.length, 0, 'frontmatter-only content returns empty array'); +}); // ═══════════════════════════════════════════════════════════════════════════ // (i) Real task plan format (based on S01/T01-PLAN.md structure) // ═══════════════════════════════════════════════════════════════════════════ -console.log('\n=== parseTaskPlanMustHaves: real task plan format ==='); -{ +test('parseTaskPlanMustHaves: real task plan format', () => { const content = `--- estimated_steps: 5 estimated_files: 3 @@ -239,40 +230,37 @@ Add the \`completing-milestone\` phase to the GSD state machine. - \`agent/extensions/gsd/types.ts\` — Phase union includes \`'completing-milestone'\` `; const result = parseTaskPlanMustHaves(content); - assertEq(result.length, 5, 'real plan has 5 must-haves'); - assertTrue(result[0].text.includes('`Phase` type includes'), 'first must-have text matches'); - assertTrue(result[1].text.includes('`deriveState` returns'), 'second must-have text matches'); - assertEq(result[0].checked, false, 'all real must-haves are unchecked'); - assertEq(result[4].checked, false, 'last real must-have is unchecked'); - assertTrue(result[4].text.includes('multi-milestone'), 'last must-have references multi-milestone'); -} + assert.deepStrictEqual(result.length, 5, 'real plan has 5 must-haves'); + assert.ok(result[0].text.includes('`Phase` type includes'), 'first must-have text matches'); + assert.ok(result[1].text.includes('`deriveState` returns'), 'second must-have text matches'); + assert.deepStrictEqual(result[0].checked, false, 'all real must-haves are unchecked'); + assert.deepStrictEqual(result[4].checked, false, 'last real must-have is unchecked'); + assert.ok(result[4].text.includes('multi-milestone'), 'last must-have references multi-milestone'); +}); // ═══════════════════════════════════════════════════════════════════════════ // Edge cases // ═══════════════════════════════════════════════════════════════════════════ -console.log('\n=== parseTaskPlanMustHaves: empty string ==='); -{ +test('parseTaskPlanMustHaves: empty string', () => { const result = parseTaskPlanMustHaves(''); - assertEq(result.length, 0, 'empty string returns empty array'); -} + assert.deepStrictEqual(result.length, 0, 'empty string returns empty array'); +}); -console.log('\n=== parseTaskPlanMustHaves: must-haves with inline code and backticks ==='); -{ +test('parseTaskPlanMustHaves: must-haves with inline code and backticks', () => { const content = `## Must-Haves - [ ] \`functionName\` is exported from \`module.ts\` - [x] Returns \`Array<{ text: string }>\` with correct extraction `; const result = parseTaskPlanMustHaves(content); - assertEq(result.length, 2, 'handles backtick content'); - assertTrue(result[0].text.includes('`functionName`'), 'preserves backticks in text'); - assertEq(result[0].checked, false, 'backtick item unchecked'); - assertEq(result[1].checked, true, 'backtick item checked'); -} + assert.deepStrictEqual(result.length, 2, 'handles backtick content'); + assert.ok(result[0].text.includes('`functionName`'), 'preserves backticks in text'); + assert.deepStrictEqual(result[0].checked, false, 'backtick item unchecked'); + assert.deepStrictEqual(result[1].checked, true, 'backtick item checked'); +}); -console.log('\n=== parseTaskPlanMustHaves: asterisk bullets ==='); -{ +test('parseTaskPlanMustHaves: asterisk bullets', () => { const content = `## Must-Haves * [ ] Asterisk unchecked @@ -280,12 +268,11 @@ console.log('\n=== parseTaskPlanMustHaves: asterisk bullets ==='); * Plain asterisk `; const result = parseTaskPlanMustHaves(content); - assertEq(result.length, 3, 'handles asterisk bullets'); - assertEq(result[0].checked, false, 'asterisk unchecked'); - assertEq(result[1].checked, true, 'asterisk checked'); - assertEq(result[2].checked, false, 'plain asterisk unchecked'); -} + assert.deepStrictEqual(result.length, 3, 'handles asterisk bullets'); + assert.deepStrictEqual(result[0].checked, false, 'asterisk unchecked'); + assert.deepStrictEqual(result[1].checked, true, 'asterisk checked'); + assert.deepStrictEqual(result[2].checked, false, 'plain asterisk unchecked'); +}); // ═══════════════════════════════════════════════════════════════════════════ -report(); diff --git a/src/resources/extensions/gsd/tests/none-mode-gates.test.ts b/src/resources/extensions/gsd/tests/none-mode-gates.test.ts index e28efd760..400288348 100644 --- a/src/resources/extensions/gsd/tests/none-mode-gates.test.ts +++ b/src/resources/extensions/gsd/tests/none-mode-gates.test.ts @@ -19,9 +19,8 @@ import { shouldUseWorktreeIsolation } from "../auto.ts"; import { getIsolationMode } from "../preferences.ts"; import { getActiveAutoWorktreeContext } from "../auto-worktree.ts"; import { invalidateAllCaches } from "../cache.ts"; -import { createTestContext } from "./test-helpers.ts"; - -const { assertEq, assertTrue, report } = createTestContext(); +import { describe, test, beforeEach, afterEach } from 'node:test'; +import assert from 'node:assert/strict'; // --- Preferences helpers (same pattern as doctor-git.test.ts K001) --- @@ -38,77 +37,77 @@ function removeRunnerPreferences(): void { // --- Tests --- -// Test 1: shouldUseWorktreeIsolation returns false for none -console.log("Test 1: shouldUseWorktreeIsolation returns false for none"); +test('shouldUseWorktreeIsolation returns false for none', () => { try { writeRunnerPreferences("none"); invalidateAllCaches(); - assertEq(shouldUseWorktreeIsolation(), false, "shouldUseWorktreeIsolation() with none prefs"); + assert.deepStrictEqual(shouldUseWorktreeIsolation(), false, "shouldUseWorktreeIsolation() with none prefs"); } finally { removeRunnerPreferences(); invalidateAllCaches(); } +}); -// Test 2: shouldUseWorktreeIsolation returns false for branch -console.log("Test 2: shouldUseWorktreeIsolation returns false for branch"); +test('shouldUseWorktreeIsolation returns false for branch', () => { try { writeRunnerPreferences("branch"); invalidateAllCaches(); - assertEq(shouldUseWorktreeIsolation(), false, "shouldUseWorktreeIsolation() with branch prefs"); + assert.deepStrictEqual(shouldUseWorktreeIsolation(), false, "shouldUseWorktreeIsolation() with branch prefs"); } finally { removeRunnerPreferences(); invalidateAllCaches(); } +}); -// Test 3: shouldUseWorktreeIsolation returns true for worktree -console.log("Test 3: shouldUseWorktreeIsolation returns true for worktree"); +test('shouldUseWorktreeIsolation returns true for worktree', () => { try { writeRunnerPreferences("worktree"); invalidateAllCaches(); - assertEq(shouldUseWorktreeIsolation(), true, "shouldUseWorktreeIsolation() with worktree prefs"); + assert.deepStrictEqual(shouldUseWorktreeIsolation(), true, "shouldUseWorktreeIsolation() with worktree prefs"); } finally { removeRunnerPreferences(); invalidateAllCaches(); } +}); // Test 4: shouldUseWorktreeIsolation returns true for no prefs (default) // Skip if global prefs exist — they override the default and this test // cannot control ~/.gsd/preferences.md. -const globalPrefsExist = existsSync(join(homedir(), ".gsd", "preferences.md")) - || existsSync(join(homedir(), ".gsd", "PREFERENCES.md")); -if (!globalPrefsExist) { - console.log("Test 4: shouldUseWorktreeIsolation returns true for no prefs (default)"); - try { - removeRunnerPreferences(); // ensure no prefs file - invalidateAllCaches(); - assertEq(shouldUseWorktreeIsolation(), true, "shouldUseWorktreeIsolation() with no prefs (default worktree)"); - } finally { - invalidateAllCaches(); - } -} else { - console.log("Test 4: SKIPPED — global prefs file exists, cannot test bare default"); -} -// Test 5: getIsolationMode returns "none" with none prefs -console.log("Test 5: getIsolationMode returns 'none' with none prefs"); +test('shouldUseWorktreeIsolation returns true for no prefs (default)', () => { + const globalPrefsExist = existsSync(join(homedir(), ".gsd", "preferences.md")) + || existsSync(join(homedir(), ".gsd", "PREFERENCES.md")); + if (!globalPrefsExist) { + try { + removeRunnerPreferences(); // ensure no prefs file + invalidateAllCaches(); + assert.deepStrictEqual(shouldUseWorktreeIsolation(), true, "shouldUseWorktreeIsolation() with no prefs (default worktree)"); + } finally { + invalidateAllCaches(); + } + } else { + } +}); + +test('getIsolationMode returns "none" with none prefs', () => { try { writeRunnerPreferences("none"); invalidateAllCaches(); - assertEq(getIsolationMode(), "none", "getIsolationMode() with none prefs"); + assert.deepStrictEqual(getIsolationMode(), "none", "getIsolationMode() with none prefs"); } finally { removeRunnerPreferences(); invalidateAllCaches(); } +}); -// Test 6: getActiveAutoWorktreeContext returns null at baseline -console.log("Test 6: getActiveAutoWorktreeContext returns null at baseline"); -assertEq(getActiveAutoWorktreeContext(), null, "getActiveAutoWorktreeContext() returns null without enterAutoWorktree()"); +test('getActiveAutoWorktreeContext returns null at baseline', () => { +assert.deepStrictEqual(getActiveAutoWorktreeContext(), null, "getActiveAutoWorktreeContext() returns null without enterAutoWorktree()"); +}); // Test 7: System prompt worktree block absent without active worktree -console.log("Test 7: System prompt worktree block absent without active worktree"); -{ - const ctx = getActiveAutoWorktreeContext(); - assertTrue(ctx === null, "getActiveAutoWorktreeContext() null confirms system prompt worktree block will not be injected"); -} -report(); +test('Test 7: System prompt worktree block absent without active worktree', () => { + const ctx = getActiveAutoWorktreeContext(); + assert.ok(ctx === null, "getActiveAutoWorktreeContext() null confirms system prompt worktree block will not be injected"); +}); +