test: include vitest test import
This commit is contained in:
parent
df03312fa5
commit
0e769dbf13
9 changed files with 673 additions and 862 deletions
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<T>(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<void> {
|
||||
// ─── 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<void> {
|
|||
|
||||
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<void> {
|
|||
|
||||
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<void> {
|
|||
|
||||
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<void> {
|
|||
"# 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<void> {
|
|||
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<void> {
|
|||
|
||||
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<void> {
|
|||
"# 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<void> {
|
|||
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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)`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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: ✗\)/);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { describe } from 'vitest';
|
||||
import { describe, test } from 'vitest';
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { describe, beforeEach, afterEach } from 'vitest';
|
||||
import { describe, test, beforeEach, afterEach } from 'vitest';
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue