diff --git a/packages/pi-ai/src/providers/provider-capabilities.test.ts b/packages/pi-ai/src/providers/provider-capabilities.test.ts index 5d3e8d112..03efe95c7 100644 --- a/packages/pi-ai/src/providers/provider-capabilities.test.ts +++ b/packages/pi-ai/src/providers/provider-capabilities.test.ts @@ -1,5 +1,5 @@ // SF — Provider Capabilities Registry Tests (ADR-005 Phase 1) -import { describe } from 'vitest'; +import { describe, test } from 'vitest'; import assert from "node:assert/strict"; import { diff --git a/src/resources/extensions/sf/milestone-quality.ts b/src/resources/extensions/sf/milestone-quality.ts index 7f749990c..8f82fe9ca 100644 --- a/src/resources/extensions/sf/milestone-quality.ts +++ b/src/resources/extensions/sf/milestone-quality.ts @@ -180,7 +180,10 @@ export function inspectMilestoneRoadmapMarkdown( }; const blockingIssue = getVisionAlignmentBlockingIssue(meeting); - if (blockingIssue) issues.push(blockingIssue); + if (blockingIssue) { + console.log(`[sf:debug] blockingIssue: ${blockingIssue}`); + issues.push(blockingIssue); + } return { issues }; } diff --git a/src/resources/extensions/sf/tests/derive-state-draft.test.ts b/src/resources/extensions/sf/tests/derive-state-draft.test.ts index baed33f47..f1a96adfc 100644 --- a/src/resources/extensions/sf/tests/derive-state-draft.test.ts +++ b/src/resources/extensions/sf/tests/derive-state-draft.test.ts @@ -1,23 +1,11 @@ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; +import { describe, it } from "vitest"; +import assert from "node:assert/strict"; import { deriveState } from "../state.js"; -let passed = 0; -let failed = 0; - -function assertEq(actual: T, expected: T, message: string): void { - if (JSON.stringify(actual) === JSON.stringify(expected)) { - passed++; - } else { - failed++; - console.error( - ` FAIL: ${message} — expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`, - ); - } -} - // ─── Fixture Helpers ─────────────────────────────────────────────────────── function createFixtureBase(): string { @@ -80,16 +68,13 @@ function cleanup(base: string): void { } // ═══════════════════════════════════════════════════════════════════════════ -// Test Groups +// Tests // ═══════════════════════════════════════════════════════════════════════════ -async function main(): Promise { - // ─── Test 1: CONTEXT-DRAFT.md only → needs-discussion ────────────────── - console.log("\n=== CONTEXT-DRAFT.md only → needs-discussion ==="); - { +describe("deriveState draft-aware", () => { + it("CONTEXT-DRAFT.md only → needs-discussion", async () => { const base = createFixtureBase(); try { - // M001 directory with only CONTEXT-DRAFT.md — no CONTEXT.md, no ROADMAP.md writeContextDraft( base, "M001", @@ -98,31 +83,23 @@ async function main(): Promise { const state = await deriveState(base); - assertEq(state.phase, "needs-discussion", "phase is needs-discussion"); - assertEq(state.activeMilestone?.id, "M001", "activeMilestone id is M001"); - assertEq(state.activeSlice, null, "activeSlice is null"); - assertEq(state.activeTask, null, "activeTask is null"); - assertEq( - state.registry[0]?.status, - "active", - "registry[0] status is active", - ); - assertEq( + assert.strictEqual(state.phase, "needs-discussion"); + assert.strictEqual(state.activeMilestone?.id, "M001"); + assert.strictEqual(state.activeSlice, null); + assert.strictEqual(state.activeTask, null); + assert.strictEqual(state.registry[0]?.status, "active"); + assert.ok( state.nextAction.includes("Discuss"), - true, "nextAction mentions Discuss", ); } finally { cleanup(base); } - } + }); - // ─── Test 2: CONTEXT.md only → pre-planning (unchanged) ─────────────── - console.log("\n=== CONTEXT.md only → pre-planning (unchanged) ==="); - { + it("CONTEXT.md only → pre-planning (unchanged)", async () => { const base = createFixtureBase(); try { - // M001 directory with CONTEXT.md but no ROADMAP.md writeContext( base, "M001", @@ -131,30 +108,19 @@ async function main(): Promise { const state = await deriveState(base); - assertEq( - state.phase, - "pre-planning", - "phase is pre-planning with CONTEXT.md", - ); - assertEq(state.activeMilestone?.id, "M001", "activeMilestone id is M001"); - assertEq(state.activeSlice, null, "activeSlice is null"); - assertEq(state.activeTask, null, "activeTask is null"); - assertEq( - state.registry[0]?.status, - "active", - "registry[0] status is active", - ); + assert.strictEqual(state.phase, "pre-planning"); + assert.strictEqual(state.activeMilestone?.id, "M001"); + assert.strictEqual(state.activeSlice, null); + assert.strictEqual(state.activeTask, null); + assert.strictEqual(state.registry[0]?.status, "active"); } finally { cleanup(base); } - } + }); - // ─── Test 3: Both CONTEXT.md and CONTEXT-DRAFT.md → CONTEXT wins ────── - console.log("\n=== both CONTEXT.md and CONTEXT-DRAFT.md → CONTEXT wins ==="); - { + it("both CONTEXT.md and CONTEXT-DRAFT.md → CONTEXT wins", async () => { const base = createFixtureBase(); try { - // M001 has both files — CONTEXT.md should take precedence writeContext( base, "M001", @@ -164,30 +130,17 @@ async function main(): Promise { const state = await deriveState(base); - assertEq( - state.phase, - "pre-planning", - "phase is pre-planning when CONTEXT.md exists", - ); - assertEq(state.activeMilestone?.id, "M001", "activeMilestone id is M001"); - assertEq( - state.registry[0]?.status, - "active", - "registry[0] status is active", - ); + assert.strictEqual(state.phase, "pre-planning"); + assert.strictEqual(state.activeMilestone?.id, "M001"); + assert.strictEqual(state.registry[0]?.status, "active"); } finally { cleanup(base); } - } + }); - // ─── Test 4: M001 complete, M002 has CONTEXT-DRAFT → M002 needs-discussion ── - console.log( - "\n=== M001 complete, M002 has CONTEXT-DRAFT → M002 needs-discussion ===", - ); - { + it("M001 complete, M002 has CONTEXT-DRAFT → M002 needs-discussion", async () => { const base = createFixtureBase(); try { - // M001: complete (roadmap with all slices done + summary) writeRoadmap( base, "M001", @@ -208,36 +161,26 @@ async function main(): Promise { "# M001 Summary\n\nFirst milestone complete.", ); - // M002: only CONTEXT-DRAFT.md writeContextDraft(base, "M002", "# Draft for M002\n\nSeed material."); const state = await deriveState(base); - assertEq( - state.phase, - "needs-discussion", - "phase is needs-discussion for M002", - ); - assertEq(state.activeMilestone?.id, "M002", "activeMilestone id is M002"); - assertEq(state.activeSlice, null, "activeSlice is null"); - assertEq(state.registry.length, 2, "registry has 2 entries"); - assertEq(state.registry[0]?.status, "complete", "M001 is complete"); - assertEq(state.registry[1]?.status, "active", "M002 is active"); - assertEq(state.progress?.milestones?.done, 1, "milestones done = 1"); - assertEq(state.progress?.milestones?.total, 2, "milestones total = 2"); + assert.strictEqual(state.phase, "needs-discussion"); + assert.strictEqual(state.activeMilestone?.id, "M002"); + assert.strictEqual(state.activeSlice, null); + assert.strictEqual(state.registry.length, 2); + assert.strictEqual(state.registry[0]?.status, "complete"); + assert.strictEqual(state.registry[1]?.status, "active"); + assert.strictEqual(state.progress?.milestones?.done, 1); + assert.strictEqual(state.progress?.milestones?.total, 2); } finally { cleanup(base); } - } + }); - // ─── Test 5: Multi-milestone: M001 complete, M002 CONTEXT-DRAFT, M003 pending ── - console.log( - "\n=== multi-milestone: M001 complete, M002 draft, M003 pending ===", - ); - { + it("multi-milestone: M001 complete, M002 draft, M003 pending", async () => { const base = createFixtureBase(); try { - // M001: complete writeRoadmap( base, "M001", @@ -254,10 +197,8 @@ async function main(): Promise { writeMilestoneValidation(base, "M001"); writeMilestoneSummary(base, "M001", "# M001 Summary\n\nComplete."); - // M002: draft only — should become active with needs-discussion writeContextDraft(base, "M002", "# M002 Draft\n\nSeed."); - // M003: milestone directory with CONTEXT — should be pending mkdirSync(join(base, ".sf", "milestones", "M003"), { recursive: true }); writeFileSync( join(base, ".sf", "milestones", "M003", "M003-CONTEXT.md"), @@ -266,30 +207,20 @@ async function main(): Promise { const state = await deriveState(base); - assertEq( - state.phase, - "needs-discussion", - "phase is needs-discussion for M002", - ); - assertEq(state.activeMilestone?.id, "M002", "activeMilestone is M002"); - assertEq(state.registry.length, 3, "registry has 3 entries"); - assertEq(state.registry[0]?.status, "complete", "M001 is complete"); - assertEq(state.registry[1]?.status, "active", "M002 is active"); - assertEq(state.registry[2]?.status, "pending", "M003 is pending"); + assert.strictEqual(state.phase, "needs-discussion"); + assert.strictEqual(state.activeMilestone?.id, "M002"); + assert.strictEqual(state.registry.length, 3); + assert.strictEqual(state.registry[0]?.status, "complete"); + assert.strictEqual(state.registry[1]?.status, "active"); + assert.strictEqual(state.registry[2]?.status, "pending"); } finally { cleanup(base); } - } + }); - // ─── Test 6: Milestone with ROADMAP + CONTEXT-DRAFT → ROADMAP takes precedence ── - console.log( - "\n=== milestone with ROADMAP + CONTEXT-DRAFT → normal execution ===", - ); - { + it("milestone with ROADMAP + CONTEXT-DRAFT → ROADMAP takes precedence", async () => { const base = createFixtureBase(); try { - // M001 has ROADMAP.md (active slice, incomplete tasks) and CONTEXT-DRAFT.md - // The ROADMAP should take precedence — we're past the draft phase writeRoadmap( base, "M001", @@ -309,7 +240,6 @@ async function main(): Promise { "# Draft\n\nThis should be ignored — roadmap exists.", ); - // Add a plan so it goes to executing phase writePlan( base, "M001", @@ -326,93 +256,49 @@ async function main(): Promise { const state = await deriveState(base); - assertEq( - state.phase, - "executing", - "phase is executing (ROADMAP takes precedence over CONTEXT-DRAFT)", - ); - assertEq(state.activeMilestone?.id, "M001", "activeMilestone is M001"); - assertEq(state.activeSlice?.id, "S01", "activeSlice is S01"); - assertEq(state.activeTask?.id, "T01", "activeTask is T01"); + assert.strictEqual(state.phase, "executing"); + assert.strictEqual(state.activeMilestone?.id, "M001"); + assert.strictEqual(state.activeSlice?.id, "S01"); + assert.strictEqual(state.activeTask?.id, "T01"); } finally { cleanup(base); } - } + }); - // ─── Test 7: Empty milestone dir (ghost — no files at all) → skipped ─── - console.log("\n=== empty milestone dir (ghost) → skipped, pre-planning ==="); - { + it("empty milestone dir (ghost) → skipped, pre-planning", async () => { const base = createFixtureBase(); try { - // M001: just a directory, no files at all — ghost milestone, skipped mkdirSync(join(base, ".sf", "milestones", "M001"), { recursive: true }); const state = await deriveState(base); - assertEq( - state.phase, - "pre-planning", - "phase is pre-planning for ghost milestone", - ); - assertEq( - state.activeMilestone, - null, - "activeMilestone is null (ghost skipped)", - ); - assertEq(state.registry.length, 0, "registry is empty (ghost skipped)"); + assert.strictEqual(state.phase, "pre-planning"); + assert.strictEqual(state.activeMilestone, null); + assert.strictEqual(state.registry.length, 0); } finally { cleanup(base); } - } + }); - // ─── Test 8: CONTEXT-DRAFT on non-first active milestone ────────────── - // M001 has no summary and no roadmap (active), M002 has CONTEXT-DRAFT - // M001 should be active (pre-planning), M002 should be pending - console.log("\n=== CONTEXT-DRAFT on non-active milestone → pending ==="); - { + it("CONTEXT-DRAFT on non-active milestone → pending", async () => { const base = createFixtureBase(); try { - // M001: has CONTEXT but no roadmap/summary → becomes active first mkdirSync(join(base, ".sf", "milestones", "M001"), { recursive: true }); writeFileSync( join(base, ".sf", "milestones", "M001", "M001-CONTEXT.md"), "# M001\n\nFirst milestone.", ); - // M002: has CONTEXT-DRAFT but isn't active (M001 is first) writeContextDraft(base, "M002", "# M002 Draft\n\nSeed."); const state = await deriveState(base); - assertEq( - state.phase, - "pre-planning", - "phase is pre-planning (M001 is active, not M002)", - ); - assertEq(state.activeMilestone?.id, "M001", "activeMilestone is M001"); - assertEq(state.registry[0]?.status, "active", "M001 is active"); - assertEq(state.registry[1]?.status, "pending", "M002 is pending"); + assert.strictEqual(state.phase, "pre-planning"); + assert.strictEqual(state.activeMilestone?.id, "M001"); + assert.strictEqual(state.registry[0]?.status, "active"); + assert.strictEqual(state.registry[1]?.status, "pending"); } finally { cleanup(base); } - } - - // ═══════════════════════════════════════════════════════════════════════════ - // Summary - // ═══════════════════════════════════════════════════════════════════════════ - - console.log(`\n${"═".repeat(60)}`); - console.log( - `Draft-aware state derivation tests: ${passed} passed, ${failed} failed`, - ); - console.log("═".repeat(60)); - - if (failed > 0) { - process.exit(1); - } -} - -main().catch((err) => { - console.error("Test suite error:", err); - process.exit(1); + }); }); diff --git a/src/resources/extensions/sf/tests/phases-merge-error-stops-auto.test.ts b/src/resources/extensions/sf/tests/phases-merge-error-stops-auto.test.ts index 34d2fa16b..2c16714d5 100644 --- a/src/resources/extensions/sf/tests/phases-merge-error-stops-auto.test.ts +++ b/src/resources/extensions/sf/tests/phases-merge-error-stops-auto.test.ts @@ -9,104 +9,99 @@ import { readFileSync } from "node:fs"; import { join } from "node:path"; -import { createTestContext } from "./test-helpers.ts"; - -const { assertTrue, report } = createTestContext(); +import { describe, it } from "vitest"; +import assert from "node:assert/strict"; const phasesPath = join(import.meta.dirname, "..", "auto", "phases.ts"); const phasesSrc = readFileSync(phasesPath, "utf-8"); -console.log("\n=== #2766: Non-MergeConflictError stops auto mode ==="); +describe("#2766: Non-MergeConflictError stops auto mode", () => { + it("phases.ts file exists and is readable", () => { + assert.ok( + phasesPath.length > 0 && phasesPath.endsWith("phases.ts"), + "phases.ts file exists and is readable", + ); + }); -// ── Test 1: phases.ts calls logError for non-conflict merge errors ────── + it("all mergeAndExit call sites have catch (mergeErr) blocks", () => { + const mergeErrCatchCount = [...phasesSrc.matchAll(/\} catch \(mergeErr\)/g)] + .length; + assert.ok( + mergeErrCatchCount >= 3, + `all mergeAndExit call sites have catch (mergeErr) blocks (found ${mergeErrCatchCount}, expected >= 3)`, + ); + }); -assertTrue( - phasesPath.length > 0 && phasesPath.endsWith("phases.ts"), - "phases.ts file exists and is readable", -); + it("every mergeErr catch block handles non-MergeConflictError", () => { + const catchPattern = /\} catch \(mergeErr\) \{/g; + let match: RegExpExecArray | null; + let blocksWithNonConflictHandling = 0; + let blocksTotal = 0; -// Count all mergeAndExit catch blocks by finding "} catch (mergeErr)" patterns -const _mergeErrCatches = [...phasesPath.matchAll(/\} catch \(mergeErr\)/g)]; -// Use the source itself for matching -const mergeErrCatchCount = [...phasesSrc.matchAll(/\} catch \(mergeErr\)/g)] - .length; -assertTrue( - mergeErrCatchCount >= 3, - `all mergeAndExit call sites have catch (mergeErr) blocks (found ${mergeErrCatchCount}, expected >= 3)`, -); + match = catchPattern.exec(phasesSrc); + while (match !== null) { + blocksTotal++; + const afterCatch = phasesSrc.slice(match.index, match.index + 1200); -// ── Test 2: Every mergeErr catch block handles non-MergeConflictError ─── + const hasInstanceofCheck = afterCatch.includes( + "instanceof MergeConflictError", + ); + const hasNonConflictStop = afterCatch.includes('reason: "merge-failed"'); + const hasStopAuto = afterCatch.includes("stopAuto"); + const hasLogError = afterCatch.includes("logError"); -// Find each catch block and verify it has the non-conflict error handling pattern -const catchPattern = /\} catch \(mergeErr\) \{/g; -let match: RegExpExecArray | null; -let blocksWithNonConflictHandling = 0; -let blocksTotal = 0; + if (hasInstanceofCheck && hasNonConflictStop && hasStopAuto && hasLogError) { + blocksWithNonConflictHandling++; + } + match = catchPattern.exec(phasesSrc); + } -match = catchPattern.exec(phasesSrc); -while (match !== null) { - blocksTotal++; - // Look at the ~800 chars after the catch to find both the MergeConflictError - // instanceof check AND the non-conflict handling - const afterCatch = phasesSrc.slice(match.index, match.index + 1200); + assert.strictEqual( + blocksWithNonConflictHandling, + blocksTotal, + `all ${blocksTotal} mergeAndExit catch blocks stop auto on non-conflict errors (${blocksWithNonConflictHandling}/${blocksTotal})`, + ); + assert.ok( + blocksTotal >= 3, + `expected at least 3 catch blocks, found ${blocksTotal}`, + ); + }); - const hasInstanceofCheck = afterCatch.includes( - "instanceof MergeConflictError", - ); - const hasNonConflictStop = afterCatch.includes('reason: "merge-failed"'); - const hasStopAuto = afterCatch.includes("stopAuto"); - const hasLogError = afterCatch.includes("logError"); + it("non-conflict handler returns break (does not continue)", () => { + const mergeFailedReasons = [...phasesSrc.matchAll(/reason: "merge-failed"/g)] + .length; + assert.ok( + mergeFailedReasons >= 3, + `all catch blocks return reason: "merge-failed" (found ${mergeFailedReasons}, expected >= 3)`, + ); + }); - if (hasInstanceofCheck && hasNonConflictStop && hasStopAuto && hasLogError) { - blocksWithNonConflictHandling++; - } - match = catchPattern.exec(phasesSrc); -} + it("non-conflict handler notifies user", () => { + const notifyErrorPattern = + /Merge failed:.*Resolve and run \/sf autonomous to resume/g; + const notifyCount = [...phasesSrc.matchAll(notifyErrorPattern)].length; + assert.ok( + notifyCount >= 3, + `all catch blocks notify user about merge failure (found ${notifyCount}, expected >= 3)`, + ); + }); -assertTrue( - blocksWithNonConflictHandling === blocksTotal && blocksTotal >= 3, - `all ${blocksTotal} mergeAndExit catch blocks stop auto on non-conflict errors (${blocksWithNonConflictHandling}/${blocksTotal})`, -); + it("logError replaces logWarning for non-conflict merge errors", () => { + const logWarningMergePattern = + /logWarning\(.*Milestone merge failed with non-conflict error/g; + const logWarningCount = [...phasesSrc.matchAll(logWarningMergePattern)].length; + assert.strictEqual( + logWarningCount, + 0, + "logWarning is no longer used for non-conflict merge errors (replaced by logError)", + ); -// ── Test 3: Non-conflict handler returns break (does not continue) ────── - -// Verify the pattern: after the MergeConflictError instanceof block, -// the non-conflict path returns { action: "break", reason: "merge-failed" } -const mergeFailedReasons = [...phasesSrc.matchAll(/reason: "merge-failed"/g)] - .length; -assertTrue( - mergeFailedReasons >= 3, - `all catch blocks return reason: "merge-failed" (found ${mergeFailedReasons}, expected >= 3)`, -); - -// ── Test 4: Non-conflict handler notifies user ────────────────────────── - -// Each non-conflict block should call ctx.ui.notify with error severity -const notifyErrorPattern = - /Merge failed:.*Resolve and run \/sf autonomous to resume/g; -const notifyCount = [...phasesSrc.matchAll(notifyErrorPattern)].length; -assertTrue( - notifyCount >= 3, - `all catch blocks notify user about merge failure (found ${notifyCount}, expected >= 3)`, -); - -// ── Test 5: logError replaces logWarning for non-conflict merge errors ── - -// The old code used logWarning — verify logError is used instead -const logWarningMergePattern = - /logWarning\(.*Milestone merge failed with non-conflict error/g; -const logWarningCount = [...phasesSrc.matchAll(logWarningMergePattern)].length; -assertTrue( - logWarningCount === 0, - "logWarning is no longer used for non-conflict merge errors (replaced by logError)", -); - -const logErrorMergePattern = - /logError\(.*Milestone merge failed with non-conflict error/g; -const logErrorCount = [...phasesSrc.matchAll(logErrorMergePattern)].length; -assertTrue( - logErrorCount >= 3, - `logError is used for non-conflict merge errors (found ${logErrorCount}, expected >= 3)`, -); - -report(); + const logErrorMergePattern = + /logError\(.*Milestone merge failed with non-conflict error/g; + const logErrorCount = [...phasesSrc.matchAll(logErrorMergePattern)].length; + assert.ok( + logErrorCount >= 3, + `logError is used for non-conflict merge errors (found ${logErrorCount}, expected >= 3)`, + ); + }); +}); diff --git a/src/resources/extensions/sf/tests/plan-quality-validator.test.ts b/src/resources/extensions/sf/tests/plan-quality-validator.test.ts index 0befb27b3..33cdcf5c9 100644 --- a/src/resources/extensions/sf/tests/plan-quality-validator.test.ts +++ b/src/resources/extensions/sf/tests/plan-quality-validator.test.ts @@ -1,17 +1,13 @@ +import { describe, it } from "vitest"; +import assert from "node:assert/strict"; import { validateSlicePlanContent, validateTaskPlanContent, } from "../observability-validator.ts"; -import { createTestContext } from "./test-helpers.ts"; -const { assertEq, assertTrue, report } = createTestContext(); -// ═══════════════════════════════════════════════════════════════════════════ -// validateTaskPlanContent — empty/missing Steps section -// ═══════════════════════════════════════════════════════════════════════════ - -console.log("\n=== validateTaskPlanContent: empty Steps section ==="); -{ - const content = `# T01: Some Task +describe("validateTaskPlanContent", () => { + it("empty Steps section produces empty_steps_section issue", () => { + const content = `# T01: Some Task ## Description @@ -24,31 +20,28 @@ Do something useful. - Run the tests and confirm output. `; - const issues = validateTaskPlanContent("T01-PLAN.md", content); - const stepsIssues = issues.filter((i) => i.ruleId === "empty_steps_section"); - assertTrue( - stepsIssues.length >= 1, - "empty Steps section produces empty_steps_section issue", - ); - if (stepsIssues.length > 0) { - assertEq( - stepsIssues[0].severity, - "warning", - "empty_steps_section severity is warning", + const issues = validateTaskPlanContent("T01-PLAN.md", content); + const stepsIssues = issues.filter((i) => i.ruleId === "empty_steps_section"); + assert.ok( + stepsIssues.length >= 1, + "empty Steps section produces empty_steps_section issue", ); - assertEq( - stepsIssues[0].scope, - "task-plan", - "empty_steps_section scope is task-plan", - ); - } -} + if (stepsIssues.length > 0) { + assert.strictEqual( + stepsIssues[0].severity, + "warning", + "empty_steps_section severity is warning", + ); + assert.strictEqual( + stepsIssues[0].scope, + "task-plan", + "empty_steps_section scope is task-plan", + ); + } + }); -console.log( - "\n=== validateTaskPlanContent: missing Steps section entirely ===", -); -{ - const content = `# T01: Some Task + it("missing Steps section entirely produces empty_steps_section issue", () => { + const content = `# T01: Some Task ## Description @@ -59,21 +52,16 @@ Do something useful. - Run the tests. `; - const issues = validateTaskPlanContent("T01-PLAN.md", content); - const stepsIssues = issues.filter((i) => i.ruleId === "empty_steps_section"); - assertTrue( - stepsIssues.length >= 1, - "missing Steps section produces empty_steps_section issue", - ); -} + const issues = validateTaskPlanContent("T01-PLAN.md", content); + const stepsIssues = issues.filter((i) => i.ruleId === "empty_steps_section"); + assert.ok( + stepsIssues.length >= 1, + "missing Steps section produces empty_steps_section issue", + ); + }); -// ═══════════════════════════════════════════════════════════════════════════ -// validateTaskPlanContent — placeholder-only Verification -// ═══════════════════════════════════════════════════════════════════════════ - -console.log("\n=== validateTaskPlanContent: placeholder-only Verification ==="); -{ - const content = `# T01: Some Task + it("placeholder-only Verification produces placeholder_verification issue", () => { + const content = `# T01: Some Task ## Steps @@ -86,33 +74,30 @@ console.log("\n=== validateTaskPlanContent: placeholder-only Verification ==="); - {{another placeholder}} `; - const issues = validateTaskPlanContent("T01-PLAN.md", content); - const verifyIssues = issues.filter( - (i) => i.ruleId === "placeholder_verification", - ); - assertTrue( - verifyIssues.length >= 1, - "placeholder-only Verification produces placeholder_verification issue", - ); - if (verifyIssues.length > 0) { - assertEq( - verifyIssues[0].severity, - "warning", - "placeholder_verification severity is warning", + const issues = validateTaskPlanContent("T01-PLAN.md", content); + const verifyIssues = issues.filter( + (i) => i.ruleId === "placeholder_verification", ); - assertEq( - verifyIssues[0].scope, - "task-plan", - "placeholder_verification scope is task-plan", + assert.ok( + verifyIssues.length >= 1, + "placeholder-only Verification produces placeholder_verification issue", ); - } -} + if (verifyIssues.length > 0) { + assert.strictEqual( + verifyIssues[0].severity, + "warning", + "placeholder_verification severity is warning", + ); + assert.strictEqual( + verifyIssues[0].scope, + "task-plan", + "placeholder_verification scope is task-plan", + ); + } + }); -console.log( - "\n=== validateTaskPlanContent: Verification with only template text ===", -); -{ - const content = `# T01: Some Task + it("template-text-only Verification produces placeholder_verification issue", () => { + const content = `# T01: Some Task ## Steps @@ -123,99 +108,18 @@ console.log( {{whatWasVerifiedAndHow — commands run, tests passed, behavior confirmed}} `; - const issues = validateTaskPlanContent("T01-PLAN.md", content); - const verifyIssues = issues.filter( - (i) => i.ruleId === "placeholder_verification", - ); - assertTrue( - verifyIssues.length >= 1, - "template-text-only Verification produces placeholder_verification issue", - ); -} - -// ═══════════════════════════════════════════════════════════════════════════ -// validateSlicePlanContent — empty inline task entries -// ═══════════════════════════════════════════════════════════════════════════ - -console.log("\n=== validateSlicePlanContent: empty inline task entries ==="); -{ - const content = `# S01: Some Slice - -**Goal:** Build the thing. -**Demo:** It works. - -## Tasks - -- [ ] **T01: First Task** \`est:20m\` - -- [ ] **T02: Second Task** \`est:15m\` - -## Verification - -- Run the tests. -`; - - const issues = validateSlicePlanContent("S01-PLAN.md", content); - const emptyTaskIssues = issues.filter((i) => i.ruleId === "empty_task_entry"); - assertTrue( - emptyTaskIssues.length >= 1, - "task entries with no description produce empty_task_entry issue", - ); - if (emptyTaskIssues.length > 0) { - assertEq( - emptyTaskIssues[0].severity, - "warning", - "empty_task_entry severity is warning", + const issues = validateTaskPlanContent("T01-PLAN.md", content); + const verifyIssues = issues.filter( + (i) => i.ruleId === "placeholder_verification", ); - assertEq( - emptyTaskIssues[0].scope, - "slice-plan", - "empty_task_entry scope is slice-plan", + assert.ok( + verifyIssues.length >= 1, + "template-text-only Verification produces placeholder_verification issue", ); - } -} + }); -console.log( - "\n=== validateSlicePlanContent: task entries with content are fine ===", -); -{ - const content = `# S01: Some Slice - -**Goal:** Build the thing. -**Demo:** It works. - -## Tasks - -- [ ] **T01: First Task** \`est:20m\` - - Why: Because it matters. - - Files: \`src/index.ts\` - - Do: Implement the feature. - -- [ ] **T02: Second Task** \`est:15m\` - - Why: Also important. - - Do: Add tests. - -## Verification - -- Run the tests. -`; - - const issues = validateSlicePlanContent("S01-PLAN.md", content); - const emptyTaskIssues = issues.filter((i) => i.ruleId === "empty_task_entry"); - assertEq( - emptyTaskIssues.length, - 0, - "task entries with description content produce no empty_task_entry issues", - ); -} - -// ═══════════════════════════════════════════════════════════════════════════ -// validateTaskPlanContent — scope_estimate over threshold -// ═══════════════════════════════════════════════════════════════════════════ - -console.log("\n=== validateTaskPlanContent: scope_estimate over threshold ==="); -{ - const content = `--- + it("scope_estimate over threshold produces scope issues", () => { + const content = `--- estimated_steps: 12 estimated_files: 15 --- @@ -233,49 +137,44 @@ estimated_files: 15 - Check it works. `; - const issues = validateTaskPlanContent("T01-PLAN.md", content); - const stepsOverIssues = issues.filter( - (i) => i.ruleId === "scope_estimate_steps_high", - ); - const filesOverIssues = issues.filter( - (i) => i.ruleId === "scope_estimate_files_high", - ); - assertTrue( - stepsOverIssues.length >= 1, - "estimated_steps=12 (>=10) produces scope_estimate_steps_high issue", - ); - assertTrue( - filesOverIssues.length >= 1, - "estimated_files=15 (>=12) produces scope_estimate_files_high issue", - ); - if (stepsOverIssues.length > 0) { - assertEq( - stepsOverIssues[0].severity, - "warning", - "scope_estimate_steps_high severity is warning", + const issues = validateTaskPlanContent("T01-PLAN.md", content); + const stepsOverIssues = issues.filter( + (i) => i.ruleId === "scope_estimate_steps_high", ); - assertEq( - stepsOverIssues[0].scope, - "task-plan", - "scope_estimate_steps_high scope is task-plan", + const filesOverIssues = issues.filter( + (i) => i.ruleId === "scope_estimate_files_high", ); - } - if (filesOverIssues.length > 0) { - assertEq( - filesOverIssues[0].severity, - "warning", - "scope_estimate_files_high severity is warning", + assert.ok( + stepsOverIssues.length >= 1, + "estimated_steps=12 (>=10) produces scope_estimate_steps_high issue", ); - } -} + assert.ok( + filesOverIssues.length >= 1, + "estimated_files=15 (>=12) produces scope_estimate_files_high issue", + ); + if (stepsOverIssues.length > 0) { + assert.strictEqual( + stepsOverIssues[0].severity, + "warning", + "scope_estimate_steps_high severity is warning", + ); + assert.strictEqual( + stepsOverIssues[0].scope, + "task-plan", + "scope_estimate_steps_high scope is task-plan", + ); + } + if (filesOverIssues.length > 0) { + assert.strictEqual( + filesOverIssues[0].severity, + "warning", + "scope_estimate_files_high severity is warning", + ); + } + }); -// ═══════════════════════════════════════════════════════════════════════════ -// validateTaskPlanContent — scope_estimate within limits -// ═══════════════════════════════════════════════════════════════════════════ - -console.log("\n=== validateTaskPlanContent: scope_estimate within limits ==="); -{ - const content = `--- + it("scope_estimate within limits produces no scope issues", () => { + const content = `--- estimated_steps: 4 estimated_files: 6 --- @@ -291,26 +190,21 @@ estimated_files: 6 - Verify it works. `; - const issues = validateTaskPlanContent("T01-PLAN.md", content); - const scopeIssues = issues.filter( - (i) => - i.ruleId === "scope_estimate_steps_high" || - i.ruleId === "scope_estimate_files_high", - ); - assertEq( - scopeIssues.length, - 0, - "scope_estimate within limits produces no scope issues", - ); -} + const issues = validateTaskPlanContent("T01-PLAN.md", content); + const scopeIssues = issues.filter( + (i) => + i.ruleId === "scope_estimate_steps_high" || + i.ruleId === "scope_estimate_files_high", + ); + assert.strictEqual( + scopeIssues.length, + 0, + "scope_estimate within limits produces no scope issues", + ); + }); -// ═══════════════════════════════════════════════════════════════════════════ -// validateTaskPlanContent — missing scope_estimate (no warning) -// ═══════════════════════════════════════════════════════════════════════════ - -console.log("\n=== validateTaskPlanContent: missing scope_estimate ==="); -{ - const content = `# T01: No Frontmatter Task + it("missing scope_estimate produces no scope issues", () => { + const content = `# T01: No Frontmatter Task ## Steps @@ -321,24 +215,21 @@ console.log("\n=== validateTaskPlanContent: missing scope_estimate ==="); - Verify it works. `; - const issues = validateTaskPlanContent("T01-PLAN.md", content); - const scopeIssues = issues.filter( - (i) => - i.ruleId === "scope_estimate_steps_high" || - i.ruleId === "scope_estimate_files_high", - ); - assertEq( - scopeIssues.length, - 0, - "missing scope_estimate produces no scope issues", - ); -} + const issues = validateTaskPlanContent("T01-PLAN.md", content); + const scopeIssues = issues.filter( + (i) => + i.ruleId === "scope_estimate_steps_high" || + i.ruleId === "scope_estimate_files_high", + ); + assert.strictEqual( + scopeIssues.length, + 0, + "missing scope_estimate produces no scope issues", + ); + }); -console.log( - "\n=== validateTaskPlanContent: frontmatter without scope keys ===", -); -{ - const content = `--- + it("frontmatter without scope keys produces no scope issues", () => { + const content = `--- id: T01 parent: S01 --- @@ -354,26 +245,21 @@ parent: S01 - Verify it works. `; - const issues = validateTaskPlanContent("T01-PLAN.md", content); - const scopeIssues = issues.filter( - (i) => - i.ruleId === "scope_estimate_steps_high" || - i.ruleId === "scope_estimate_files_high", - ); - assertEq( - scopeIssues.length, - 0, - "frontmatter without scope keys produces no scope issues", - ); -} + const issues = validateTaskPlanContent("T01-PLAN.md", content); + const scopeIssues = issues.filter( + (i) => + i.ruleId === "scope_estimate_steps_high" || + i.ruleId === "scope_estimate_files_high", + ); + assert.strictEqual( + scopeIssues.length, + 0, + "frontmatter without scope keys produces no scope issues", + ); + }); -// ═══════════════════════════════════════════════════════════════════════════ -// Clean plans — no false positives -// ═══════════════════════════════════════════════════════════════════════════ - -console.log("\n=== Clean task plan: no plan-quality issues ==="); -{ - const content = `--- + it("clean task plan produces no plan-quality issues", () => { + const content = `--- estimated_steps: 5 estimated_files: 3 --- @@ -409,24 +295,221 @@ A real task with real content. - Failure state exposed: exit code 1 + error message on invalid input `; - const issues = validateTaskPlanContent("T01-PLAN.md", content); - const planQualityIssues = issues.filter( - (i) => - i.ruleId === "empty_steps_section" || - i.ruleId === "placeholder_verification" || - i.ruleId === "scope_estimate_steps_high" || - i.ruleId === "scope_estimate_files_high", - ); - assertEq( - planQualityIssues.length, - 0, - "clean task plan produces no plan-quality issues", - ); -} + const issues = validateTaskPlanContent("T01-PLAN.md", content); + const planQualityIssues = issues.filter( + (i) => + i.ruleId === "empty_steps_section" || + i.ruleId === "placeholder_verification" || + i.ruleId === "scope_estimate_steps_high" || + i.ruleId === "scope_estimate_files_high", + ); + assert.strictEqual( + planQualityIssues.length, + 0, + "clean task plan produces no plan-quality issues", + ); + }); -console.log("\n=== Clean slice plan: no plan-quality issues ==="); -{ - const content = `# S01: Well-Formed Slice + it("Expected Output without file paths triggers missing_output_file_paths", () => { + const content = `# T01: Some Task + +## Description + +Do something. + +## Steps + +1. Do the thing + +## Verification + +- Check it works + +## Expected Output + +This task produces the main output. +`; + + const issues = validateTaskPlanContent("T01-PLAN.md", content); + const outputIssues = issues.filter( + (i) => i.ruleId === "missing_output_file_paths", + ); + assert.ok( + outputIssues.length >= 1, + "Expected Output without file paths triggers missing_output_file_paths", + ); + }); + + it("Expected Output with file paths does not trigger warning", () => { + const content = `# T01: Some Task + +## Description + +Do something. + +## Steps + +1. Do the thing + +## Verification + +- Check it works + +## Expected Output + +- \`src/types.ts\` — New type definitions +`; + + const issues = validateTaskPlanContent("T01-PLAN.md", content); + const outputIssues = issues.filter( + (i) => i.ruleId === "missing_output_file_paths", + ); + assert.strictEqual( + outputIssues.length, + 0, + "Expected Output with file paths does not trigger warning", + ); + }); + + it("Inputs without file paths triggers missing_input_file_paths with info severity", () => { + const content = `# T01: Some Task + +## Description + +Do something. + +## Steps + +1. Do the thing + +## Verification + +- Check it works + +## Inputs + +Prior task summary insights about the architecture. + +## Expected Output + +- \`src/output.ts\` — Output file +`; + + const issues = validateTaskPlanContent("T01-PLAN.md", content); + const inputIssues = issues.filter( + (i) => i.ruleId === "missing_input_file_paths", + ); + assert.ok( + inputIssues.length >= 1, + "Inputs without file paths triggers missing_input_file_paths", + ); + if (inputIssues.length > 0) { + assert.strictEqual( + inputIssues[0].severity, + "info", + "missing_input_file_paths is info severity (not warning)", + ); + } + }); + + it("missing Expected Output section triggers missing_output_file_paths", () => { + const content = `# T01: Some Task + +## Description + +Do something. + +## Steps + +1. Do the thing + +## Verification + +- Check it works +`; + + const issues = validateTaskPlanContent("T01-PLAN.md", content); + const outputIssues = issues.filter( + (i) => i.ruleId === "missing_output_file_paths", + ); + assert.ok( + outputIssues.length >= 1, + "Missing Expected Output section triggers missing_output_file_paths", + ); + }); +}); + +describe("validateSlicePlanContent", () => { + it("task entries with no description produce empty_task_entry issue", () => { + const content = `# S01: Some Slice + +**Goal:** Build the thing. +**Demo:** It works. + +## Tasks + +- [ ] **T01: First Task** \`est:20m\` + +- [ ] **T02: Second Task** \`est:15m\` + +## Verification + +- Run the tests. +`; + + const issues = validateSlicePlanContent("S01-PLAN.md", content); + const emptyTaskIssues = issues.filter((i) => i.ruleId === "empty_task_entry"); + assert.ok( + emptyTaskIssues.length >= 1, + "task entries with no description produce empty_task_entry issue", + ); + if (emptyTaskIssues.length > 0) { + assert.strictEqual( + emptyTaskIssues[0].severity, + "warning", + "empty_task_entry severity is warning", + ); + assert.strictEqual( + emptyTaskIssues[0].scope, + "slice-plan", + "empty_task_entry scope is slice-plan", + ); + } + }); + + it("task entries with description content produce no empty_task_entry issues", () => { + const content = `# S01: Some Slice + +**Goal:** Build the thing. +**Demo:** It works. + +## Tasks + +- [ ] **T01: First Task** \`est:20m\` + - Why: Because it matters. + - Files: \`src/index.ts\` + - Do: Implement the feature. + +- [ ] **T02: Second Task** \`est:15m\` + - Why: Also important. + - Do: Add tests. + +## Verification + +- Run the tests. +`; + + const issues = validateSlicePlanContent("S01-PLAN.md", content); + const emptyTaskIssues = issues.filter((i) => i.ruleId === "empty_task_entry"); + assert.strictEqual( + emptyTaskIssues.length, + 0, + "task entries with description content produce no empty_task_entry issues", + ); + }); + + it("clean slice plan produces no empty_task_entry issues", () => { + const content = `# S01: Well-Formed Slice **Goal:** Build a complete feature. **Demo:** Run the test suite and see all green. @@ -458,155 +541,14 @@ console.log("\n=== Clean slice plan: no plan-quality issues ==="); - Redaction constraints: none `; - const issues = validateSlicePlanContent("S01-PLAN.md", content); - const planQualityIssues = issues.filter( - (i) => i.ruleId === "empty_task_entry", - ); - assertEq( - planQualityIssues.length, - 0, - "clean slice plan produces no empty_task_entry issues", - ); -} - -// ═══════════════════════════════════════════════════════════════════════════ -// validateTaskPlanContent — missing output file paths -// ═══════════════════════════════════════════════════════════════════════════ - -console.log("\n=== validateTaskPlanContent: missing output file paths ==="); -{ - const content = `# T01: Some Task - -## Description - -Do something. - -## Steps - -1. Do the thing - -## Verification - -- Check it works - -## Expected Output - -This task produces the main output. -`; - - const issues = validateTaskPlanContent("T01-PLAN.md", content); - const outputIssues = issues.filter( - (i) => i.ruleId === "missing_output_file_paths", - ); - assertTrue( - outputIssues.length >= 1, - "Expected Output without file paths triggers missing_output_file_paths", - ); -} - -console.log("\n=== validateTaskPlanContent: valid output file paths ==="); -{ - const content = `# T01: Some Task - -## Description - -Do something. - -## Steps - -1. Do the thing - -## Verification - -- Check it works - -## Expected Output - -- \`src/types.ts\` — New type definitions -`; - - const issues = validateTaskPlanContent("T01-PLAN.md", content); - const outputIssues = issues.filter( - (i) => i.ruleId === "missing_output_file_paths", - ); - assertEq( - outputIssues.length, - 0, - "Expected Output with file paths does not trigger warning", - ); -} - -console.log( - "\n=== validateTaskPlanContent: missing input file paths (info severity) ===", -); -{ - const content = `# T01: Some Task - -## Description - -Do something. - -## Steps - -1. Do the thing - -## Verification - -- Check it works - -## Inputs - -Prior task summary insights about the architecture. - -## Expected Output - -- \`src/output.ts\` — Output file -`; - - const issues = validateTaskPlanContent("T01-PLAN.md", content); - const inputIssues = issues.filter( - (i) => i.ruleId === "missing_input_file_paths", - ); - assertTrue( - inputIssues.length >= 1, - "Inputs without file paths triggers missing_input_file_paths", - ); - if (inputIssues.length > 0) { - assertEq( - inputIssues[0].severity, - "info", - "missing_input_file_paths is info severity (not warning)", + const issues = validateSlicePlanContent("S01-PLAN.md", content); + const planQualityIssues = issues.filter( + (i) => i.ruleId === "empty_task_entry", ); - } -} - -console.log( - "\n=== validateTaskPlanContent: no Expected Output section at all ===", -); -{ - const content = `# T01: Some Task - -## Description - -Do something. - -## Steps - -1. Do the thing - -## Verification - -- Check it works -`; - - const issues = validateTaskPlanContent("T01-PLAN.md", content); - const outputIssues = issues.filter( - (i) => i.ruleId === "missing_output_file_paths", - ); - assertTrue( - outputIssues.length >= 1, - "Missing Expected Output section triggers missing_output_file_paths", - ); -} - -report(); + assert.strictEqual( + planQualityIssues.length, + 0, + "clean slice plan produces no empty_task_entry issues", + ); + }); +}); diff --git a/src/resources/extensions/sf/tests/visualizer-critical-path.test.ts b/src/resources/extensions/sf/tests/visualizer-critical-path.test.ts index 85c561e66..c6f3cf774 100644 --- a/src/resources/extensions/sf/tests/visualizer-critical-path.test.ts +++ b/src/resources/extensions/sf/tests/visualizer-critical-path.test.ts @@ -1,6 +1,7 @@ // Tests for critical path algorithm. // Tests computeCriticalPath with known DAG structures. +import { describe, it } from "vitest"; import assert from "node:assert/strict"; import type { VisualizerMilestone } from "../visualizer-data.js"; import { computeCriticalPath } from "../visualizer-data.js"; @@ -26,178 +27,154 @@ function makeSlice(id: string, done: boolean, depends: string[] = []) { }; } -// ─── Linear chain ─────────────────────────────────────────────────────────── +describe("computeCriticalPath", () => { + it("linear chain has critical path", () => { + // M001 -> M002 -> M003 + const milestones = [ + makeMs("M001", "complete", []), + makeMs( + "M002", + "active", + ["M001"], + [makeSlice("S01", true), makeSlice("S02", false, ["S01"])], + ), + makeMs("M003", "pending", ["M002"]), + ]; -console.log("\n=== Critical Path: Linear Chain ==="); + const cp = computeCriticalPath(milestones); + assert.ok(cp.milestonePath.length > 0, "linear chain has critical path"); + assert.ok(cp.milestonePath.includes("M002"), "M002 is on critical path"); + assert.ok(cp.milestonePath.includes("M003"), "M003 is on critical path"); + assert.deepStrictEqual( + cp.milestoneSlack.get("M002"), + 0, + "M002 has zero slack", + ); + assert.deepStrictEqual( + cp.milestoneSlack.get("M003"), + 0, + "M003 has zero slack", + ); + }); -{ - // M001 -> M002 -> M003 - const milestones = [ - makeMs("M001", "complete", []), - makeMs( - "M002", - "active", - ["M001"], - [makeSlice("S01", true), makeSlice("S02", false, ["S01"])], - ), - makeMs("M003", "pending", ["M002"]), - ]; + it("diamond DAG has critical path through heavier branch", () => { + // M001 -> M002 -> M004 + // M001 -> M003 -> M004 + // M002 has 3 incomplete slices, M003 has 1 incomplete slice + const milestones = [ + makeMs("M001", "complete", []), + makeMs( + "M002", + "active", + ["M001"], + [ + makeSlice("S01", false), + makeSlice("S02", false), + makeSlice("S03", false), + ], + ), + makeMs("M003", "pending", ["M001"], [makeSlice("S01", false)]), + makeMs("M004", "pending", ["M002", "M003"]), + ]; - const cp = computeCriticalPath(milestones); - assert.ok(cp.milestonePath.length > 0, "linear chain has critical path"); - assert.ok(cp.milestonePath.includes("M002"), "M002 is on critical path"); - assert.ok(cp.milestonePath.includes("M003"), "M003 is on critical path"); - assert.deepStrictEqual( - cp.milestoneSlack.get("M002"), - 0, - "M002 has zero slack", - ); - assert.deepStrictEqual( - cp.milestoneSlack.get("M003"), - 0, - "M003 has zero slack", - ); -} + const cp = computeCriticalPath(milestones); + assert.ok(cp.milestonePath.length >= 2, "diamond DAG has critical path"); + // M002 has weight 3 (3 incomplete), M003 has weight 1 + // Critical path should go through M002 (longer) + assert.ok( + cp.milestonePath.includes("M002"), + "M002 (heavier) is on critical path", + ); -// ─── Diamond DAG ──────────────────────────────────────────────────────────── + // M003 should have non-zero slack since it's lighter + const m003Slack = cp.milestoneSlack.get("M003") ?? -1; + assert.ok(m003Slack > 0, "M003 has positive slack (lighter branch)"); + }); -console.log("\n=== Critical Path: Diamond DAG ==="); + it("independent branches have at least one critical node", () => { + // M001 (no deps), M002 (no deps), M003 (no deps) + const milestones = [ + makeMs("M001", "active", [], [makeSlice("S01", false)]), + makeMs( + "M002", + "pending", + [], + [makeSlice("S01", false), makeSlice("S02", false)], + ), + makeMs("M003", "pending", [], [makeSlice("S01", false)]), + ]; -{ - // M001 -> M002 -> M004 - // M001 -> M003 -> M004 - // M002 has 3 incomplete slices, M003 has 1 incomplete slice - const milestones = [ - makeMs("M001", "complete", []), - makeMs( - "M002", - "active", - ["M001"], - [ - makeSlice("S01", false), - makeSlice("S02", false), - makeSlice("S03", false), - ], - ), - makeMs("M003", "pending", ["M001"], [makeSlice("S01", false)]), - makeMs("M004", "pending", ["M002", "M003"]), - ]; + const cp = computeCriticalPath(milestones); + assert.ok( + cp.milestonePath.length >= 1, + "independent branches have at least one critical node", + ); + // M002 has the most incomplete slices, should be critical + assert.ok( + cp.milestonePath.includes("M002"), + "M002 (longest) is on critical path", + ); + }); - const cp = computeCriticalPath(milestones); - assert.ok(cp.milestonePath.length >= 2, "diamond DAG has critical path"); - // M002 has weight 3 (3 incomplete), M003 has weight 1 - // Critical path should go through M002 (longer) - assert.ok( - cp.milestonePath.includes("M002"), - "M002 (heavier) is on critical path", - ); + it("slice-level critical path is computed correctly", () => { + // Active milestone with slice dependencies: S01 -> S02 -> S04, S01 -> S03 + const milestones = [ + makeMs( + "M001", + "active", + [], + [ + makeSlice("S01", true), + makeSlice("S02", false, ["S01"]), + makeSlice("S03", false, ["S01"]), + makeSlice("S04", false, ["S02"]), + ], + ), + ]; - // M003 should have non-zero slack since it's lighter - const m003Slack = cp.milestoneSlack.get("M003") ?? -1; - assert.ok(m003Slack > 0, "M003 has positive slack (lighter branch)"); -} + const cp = computeCriticalPath(milestones); + assert.ok(cp.slicePath.length > 0, "has slice-level critical path"); + assert.ok(cp.slicePath.includes("S02"), "S02 is on slice critical path"); + assert.ok(cp.slicePath.includes("S04"), "S04 is on slice critical path"); -// ─── Independent branches ─────────────────────────────────────────────────── + // S03 should have non-zero slack (it's a shorter branch) + const s03Slack = cp.sliceSlack.get("S03") ?? -1; + assert.ok(s03Slack > 0, "S03 has positive slack (shorter branch)"); + }); -console.log("\n=== Critical Path: Independent Branches ==="); + it("empty milestones produce empty path", () => { + const cp = computeCriticalPath([]); + assert.deepStrictEqual( + cp.milestonePath.length, + 0, + "empty milestones produce empty path", + ); + assert.deepStrictEqual( + cp.slicePath.length, + 0, + "empty milestones produce empty slice path", + ); + }); -{ - // M001 (no deps), M002 (no deps), M003 (no deps) - const milestones = [ - makeMs("M001", "active", [], [makeSlice("S01", false)]), - makeMs( - "M002", - "pending", - [], - [makeSlice("S01", false), makeSlice("S02", false)], - ), - makeMs("M003", "pending", [], [makeSlice("S01", false)]), - ]; + it("single milestone is its own critical path", () => { + const milestones = [ + makeMs( + "M001", + "active", + [], + [makeSlice("S01", false), makeSlice("S02", false)], + ), + ]; - const cp = computeCriticalPath(milestones); - assert.ok( - cp.milestonePath.length >= 1, - "independent branches have at least one critical node", - ); - // M002 has the most incomplete slices, should be critical - assert.ok( - cp.milestonePath.includes("M002"), - "M002 (longest) is on critical path", - ); -} - -// ─── Slice-level critical path ────────────────────────────────────────────── - -console.log("\n=== Critical Path: Slice-level ==="); - -{ - // Active milestone with slice dependencies: S01 -> S02 -> S04, S01 -> S03 - const milestones = [ - makeMs( + const cp = computeCriticalPath(milestones); + assert.ok( + cp.milestonePath.length === 1, + "single milestone is its own critical path", + ); + assert.deepStrictEqual( + cp.milestonePath[0], "M001", - "active", - [], - [ - makeSlice("S01", true), - makeSlice("S02", false, ["S01"]), - makeSlice("S03", false, ["S01"]), - makeSlice("S04", false, ["S02"]), - ], - ), - ]; - - const cp = computeCriticalPath(milestones); - assert.ok(cp.slicePath.length > 0, "has slice-level critical path"); - assert.ok(cp.slicePath.includes("S02"), "S02 is on slice critical path"); - assert.ok(cp.slicePath.includes("S04"), "S04 is on slice critical path"); - - // S03 should have non-zero slack (it's a shorter branch) - const s03Slack = cp.sliceSlack.get("S03") ?? -1; - assert.ok(s03Slack > 0, "S03 has positive slack (shorter branch)"); -} - -// ─── Empty milestones ─────────────────────────────────────────────────────── - -console.log("\n=== Critical Path: Empty ==="); - -{ - const cp = computeCriticalPath([]); - assert.deepStrictEqual( - cp.milestonePath.length, - 0, - "empty milestones produce empty path", - ); - assert.deepStrictEqual( - cp.slicePath.length, - 0, - "empty milestones produce empty slice path", - ); -} - -// ─── Single milestone ─────────────────────────────────────────────────────── - -console.log("\n=== Critical Path: Single Milestone ==="); - -{ - const milestones = [ - makeMs( - "M001", - "active", - [], - [makeSlice("S01", false), makeSlice("S02", false)], - ), - ]; - - const cp = computeCriticalPath(milestones); - assert.ok( - cp.milestonePath.length === 1, - "single milestone is its own critical path", - ); - assert.deepStrictEqual( - cp.milestonePath[0], - "M001", - "M001 is the critical node", - ); -} - -// ─── Report ───────────────────────────────────────────────────────────────── + "M001 is the critical node", + ); + }); +}); diff --git a/src/tests/search-provider-command.test.ts b/src/tests/search-provider-command.test.ts index 44e377863..9ee0b8ad1 100644 --- a/src/tests/search-provider-command.test.ts +++ b/src/tests/search-provider-command.test.ts @@ -20,6 +20,9 @@ import { test, afterEach } from 'vitest'; const SEARCH_ENV_KEYS = [ "TAVILY_API_KEY", + "MINIMAX_API_KEY", + "MINIMAX_CODE_PLAN_KEY", + "MINIMAX_CODING_API_KEY", "BRAVE_API_KEY", "SERPER_API_KEY", "EXA_API_KEY", @@ -243,27 +246,28 @@ test('direct arg "auto" sets preference and notifies', async (t) => { // 4. No arg — shows select UI, user picks one // ═══════════════════════════════════════════════════════════════════════════ -test("no arg shows select UI with 7 options, user picks brave", async () => { +test("no arg shows select UI with 8 options, user picks brave", async () => { const cmd = await loadCommand(); await withEnv( - { TAVILY_API_KEY: "tvly-test", BRAVE_API_KEY: "BSA-test" }, + { TAVILY_API_KEY: "tvly-test", BRAVE_API_KEY: "BSA-test", MINIMAX_API_KEY: undefined, MINIMAX_CODE_PLAN_KEY: undefined, MINIMAX_CODING_API_KEY: undefined }, async () => { const ctx = makeMockCtx("brave (key: ✓)"); await cmd.handler("", ctx); // Select UI shown assert.equal(ctx.ui.selectCalls.length, 1, "should show select UI"); - assert.equal(ctx.ui.selectCalls[0].options.length, 7); + assert.equal(ctx.ui.selectCalls[0].options.length, 8); // Options show key status assert.match(ctx.ui.selectCalls[0].options[0], /tavily \(key: ✓\)/); - assert.match(ctx.ui.selectCalls[0].options[1], /brave \(key: ✓\)/); - assert.match(ctx.ui.selectCalls[0].options[2], /serper \(key:/); - assert.match(ctx.ui.selectCalls[0].options[3], /exa \(key:/); - assert.match(ctx.ui.selectCalls[0].options[4], /ollama \(key:/); - assert.match(ctx.ui.selectCalls[0].options[5], /combosearch \(/); - assert.equal(ctx.ui.selectCalls[0].options[6], "auto"); + assert.match(ctx.ui.selectCalls[0].options[1], /minimax \(key:/); + assert.match(ctx.ui.selectCalls[0].options[2], /brave \(key: ✓\)/); + assert.match(ctx.ui.selectCalls[0].options[3], /serper \(key:/); + assert.match(ctx.ui.selectCalls[0].options[4], /exa \(key:/); + assert.match(ctx.ui.selectCalls[0].options[5], /ollama \(key:/); + assert.match(ctx.ui.selectCalls[0].options[6], /combosearch \(/); + assert.equal(ctx.ui.selectCalls[0].options[7], "auto"); // Title shows current preference assert.match(ctx.ui.selectCalls[0].title, /current:/); @@ -346,19 +350,20 @@ test('invalid arg "google" falls back to interactive select', async () => { // 7. Tab completion — all options when prefix is empty // ═══════════════════════════════════════════════════════════════════════════ -test("tab completion returns all 7 options when prefix is empty", async () => { +test("tab completion returns all 8 options when prefix is empty", async () => { const cmd = await loadCommand(); await withEnv( - { TAVILY_API_KEY: "tvly-test", BRAVE_API_KEY: "BSA-test" }, + { TAVILY_API_KEY: "tvly-test", BRAVE_API_KEY: "BSA-test", MINIMAX_API_KEY: undefined, MINIMAX_CODE_PLAN_KEY: undefined, MINIMAX_CODING_API_KEY: undefined }, () => { const items = cmd.getArgumentCompletions!(""); assert.ok(items, "completions should not be null"); - assert.equal(items!.length, 7); + assert.equal(items!.length, 8); const values = items!.map((i: any) => i.value); assert.deepEqual(values, [ "tavily", + "minimax", "brave", "serper", "exa", @@ -434,6 +439,9 @@ test('notify message shows "none" when no API keys available', async () => { { TAVILY_API_KEY: undefined, BRAVE_API_KEY: undefined, + MINIMAX_API_KEY: undefined, + MINIMAX_CODE_PLAN_KEY: undefined, + MINIMAX_CODING_API_KEY: undefined, OLLAMA_API_KEY: undefined, }, async () => { @@ -454,14 +462,14 @@ test("select options show key unavailability with ✗", async () => { const cmd = await loadCommand(); await withEnv( - { TAVILY_API_KEY: undefined, BRAVE_API_KEY: undefined }, + { TAVILY_API_KEY: undefined, BRAVE_API_KEY: undefined, MINIMAX_API_KEY: undefined, MINIMAX_CODE_PLAN_KEY: undefined, MINIMAX_CODING_API_KEY: undefined }, async () => { const ctx = makeMockCtx("auto"); await cmd.handler("", ctx); assert.equal(ctx.ui.selectCalls.length, 1); assert.match(ctx.ui.selectCalls[0].options[0], /tavily \(key: ✗\)/); - assert.match(ctx.ui.selectCalls[0].options[1], /brave \(key: ✗\)/); + assert.match(ctx.ui.selectCalls[0].options[2], /brave \(key: ✗\)/); }, ); }); diff --git a/web/lib/__tests__/dashboard-metrics-fallback.test.ts b/web/lib/__tests__/dashboard-metrics-fallback.test.ts index bd6e2a176..fefb2aff7 100644 --- a/web/lib/__tests__/dashboard-metrics-fallback.test.ts +++ b/web/lib/__tests__/dashboard-metrics-fallback.test.ts @@ -1,4 +1,4 @@ -import { describe } from 'vitest'; +import { describe, test } from 'vitest'; import assert from "node:assert/strict"; /** diff --git a/web/lib/__tests__/shutdown-gate.test.ts b/web/lib/__tests__/shutdown-gate.test.ts index e2506b54c..e465e0250 100644 --- a/web/lib/__tests__/shutdown-gate.test.ts +++ b/web/lib/__tests__/shutdown-gate.test.ts @@ -1,4 +1,4 @@ -import { describe, beforeEach, afterEach } from 'vitest'; +import { describe, test, beforeEach, afterEach } from 'vitest'; import assert from "node:assert/strict"; import {