singularity-forge/src/resources/extensions/sf/tests/plan-milestone.test.ts
Mikael Hugo d73a73d7f3 chore: node 24 native APIs, import.meta.dirname, parsers rename, dep updates
- Replace fileURLToPath(import.meta.url) with import.meta.dirname across
  scripts and extensions
- Rename parsers-legacy.ts → parsers.ts
- Remove deleted plan/spec docs (cicd-pipeline)
- Update package.json engines and deps across workspace packages
- Update web/package-lock.json

💘 Generated with Crush

Assisted-by: GLM-5.1 via Crush <crush@charm.land>
2026-05-02 06:18:25 +02:00

608 lines
17 KiB
TypeScript

import assert from "node:assert/strict";
import {
existsSync,
mkdirSync,
mkdtempSync,
readFileSync,
rmSync,
writeFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { test } from 'vitest';
import { parseRoadmap } from "../parsers.ts";
import {
closeDatabase,
getMilestone,
getMilestoneSlices,
getSlice,
insertMilestone,
openDatabase,
updateSliceStatus,
} from "../sf-db.ts";
import {
handlePlanMilestone,
type PlanMilestoneParams,
} from "../tools/plan-milestone.ts";
function makeTmpBase(): string {
const base = mkdtempSync(join(tmpdir(), "sf-plan-milestone-"));
mkdirSync(join(base, ".sf", "milestones", "M001"), { recursive: true });
return base;
}
function cleanup(base: string): void {
try {
closeDatabase();
} catch {
/* noop */
}
try {
rmSync(base, { recursive: true, force: true });
} catch {
/* noop */
}
}
function validParams(): PlanMilestoneParams {
return {
milestoneId: "M001",
title: "DB-backed planning",
vision: "Make planning write through the database.",
successCriteria: ["Planning persists", "Roadmap renders from DB"],
keyRisks: [
{
risk: "Renderer mismatch",
whyItMatters: "Rendered roadmap may stop round-tripping.",
},
],
proofStrategy: [
{
riskOrUnknown: "Render correctness",
retireIn: "S01",
whatWillBeProven: "ROADMAP output matches DB state.",
},
],
verificationContract: "Contract verification text",
verificationIntegration: "Integration verification text",
verificationOperational: "Operational verification text",
verificationUat: "UAT verification text",
definitionOfDone: ["Tests pass", "Tool reruns cleanly"],
requirementCoverage: "Covers R015.",
boundaryMapMarkdown:
"| From | To | Produces | Consumes |\n|------|----|----------|----------|\n| S01 | terminal | roadmap | nothing |",
visionMeeting: {
trigger:
"Top-level roadmap spans multiple user, business, and delivery concerns.",
pm: "Primary product move is making DB-backed planning the real source of truth.",
userAdvocate:
"Users need planning artifacts that stay coherent after repeated planning turns.",
customerPanel:
"Power users care about fidelity, maintainers care about consistency, and new adopters care about understandable roadmap output.",
business:
"The system needs a credible planning surface that can scale to more guided automation.",
researcher:
"Comparable planning systems treat requirements, roadmap, and state as a connected contract rather than separate notes.",
deliveryLead:
"Keep the first milestone narrow: DB-backed write path, projection, and prompt migration.",
partner:
"The proposed roadmap is small enough to land and strong enough to prove the architecture shift.",
combatant:
"Do not overbuild planning metadata before the DB-backed write path is working.",
architect:
"Persist the meeting and render it into the roadmap so the state machine can reason over it.",
moderator:
"Weighted synthesis: preserve the narrow DB-backed milestone, but require explicit stakeholder and market reasoning before execution.",
weightedSynthesis:
"User trust, product coherence, and business viability all point to a small but fully real DB-backed planning milestone. The strongest cut is avoiding speculative extra automation in this milestone.",
confidenceByArea:
"- User need: high\n- Architecture fit: high\n- Comparable-system fit: medium\n- Sequencing confidence: high",
recommendedRoute: "planning",
},
slices: [
{
sliceId: "S01",
title: "Tool wiring",
risk: "medium",
depends: [],
demo: "The tool writes roadmap state.",
goal: "Wire the handler.",
successCriteria: "Handler persists state and renders markdown.",
proofLevel: "integration",
integrationClosure: "Downstream callers read rendered roadmap output.",
observabilityImpact: "Tests expose render and validation failures.",
},
{
sliceId: "S02",
title: "Prompt migration",
risk: "low",
depends: ["S01"],
demo: "Prompts call the tool.",
goal: "Migrate prompts to DB-backed path.",
successCriteria: "Prompt contracts reference the new tool.",
proofLevel: "integration",
integrationClosure: "Prompt tests cover the new planning route.",
observabilityImpact: "Prompt and rogue-write failures become explicit.",
},
],
};
}
test("handlePlanMilestone writes milestone and slice planning state and renders roadmap", async () => {
const base = makeTmpBase();
const dbPath = join(base, ".sf", "sf.db");
openDatabase(dbPath);
try {
const result = await handlePlanMilestone(validParams(), base);
assert.ok(
!("error" in result),
`unexpected error: ${"error" in result ? result.error : ""}`,
);
const milestone = getMilestone("M001");
assert.ok(milestone, "milestone should exist");
assert.equal(
milestone?.vision,
"Make planning write through the database.",
);
assert.deepEqual(milestone?.success_criteria, [
"Planning persists",
"Roadmap renders from DB",
]);
assert.equal(
milestone?.verification_contract,
"Contract verification text",
);
assert.equal(milestone?.vision_meeting?.recommendedRoute, "planning");
assert.match(
milestone?.vision_meeting?.customerPanel ?? "",
/Power users care about fidelity/,
);
const slices = getMilestoneSlices("M001");
assert.equal(slices.length, 2);
assert.equal(slices[0]?.id, "S01");
assert.equal(slices[0]?.goal, "Wire the handler.");
assert.equal(slices[1]?.depends[0], "S01");
const roadmapPath = join(
base,
".sf",
"milestones",
"M001",
"M001-ROADMAP.md",
);
assert.ok(existsSync(roadmapPath), "roadmap should be rendered to disk");
const roadmap = readFileSync(roadmapPath, "utf-8");
assert.match(roadmap, /# M001: DB-backed planning/);
assert.match(roadmap, /## Vision/);
assert.match(roadmap, /Make planning write through the database\./);
assert.match(roadmap, /## Vision Alignment Meeting/);
assert.match(roadmap, /### Customer Panel/);
assert.match(roadmap, /### Weighted Synthesis/);
assert.match(roadmap, /## Slice Overview/);
assert.match(roadmap, /\| S01 \| Tool wiring \| medium \|/);
assert.match(roadmap, /\| S02 \| Prompt migration \| low \| S01 \|/);
} finally {
cleanup(base);
}
});
test("handlePlanMilestone persists weighted roadmap draft even when the meeting routes back to research", async () => {
const base = makeTmpBase();
const dbPath = join(base, ".sf", "sf.db");
openDatabase(dbPath);
try {
const baseVisionMeeting = validParams().visionMeeting!;
const result = await handlePlanMilestone(
{
...validParams(),
visionMeeting: {
...baseVisionMeeting,
moderator:
"Weighted synthesis says comparable-product expectations are still too fuzzy for a final roadmap.",
confidenceByArea:
"- User need: high\n- Architecture fit: medium\n- Comparable-system fit: low\n- Sequencing confidence: low",
recommendedRoute: "researching",
},
},
base,
);
assert.ok(
!("error" in result),
`unexpected error: ${"error" in result ? result.error : ""}`,
);
const milestone = getMilestone("M001");
assert.equal(milestone?.vision_meeting?.recommendedRoute, "researching");
const roadmapPath = join(
base,
".sf",
"milestones",
"M001",
"M001-ROADMAP.md",
);
const roadmap = readFileSync(roadmapPath, "utf-8");
assert.match(roadmap, /### Recommended Route/);
assert.match(roadmap, /researching/);
} finally {
cleanup(base);
}
});
test("handlePlanMilestone rejects invalid payloads", async () => {
const base = makeTmpBase();
const dbPath = join(base, ".sf", "sf.db");
openDatabase(dbPath);
try {
const params = validParams();
const result = await handlePlanMilestone({ ...params, slices: [] }, base);
assert.ok("error" in result);
assert.match(
result.error,
/validation failed: slices must be a non-empty array/,
);
} finally {
cleanup(base);
}
});
test("handlePlanMilestone rejects leaked JSON fields in nested planning text", async () => {
const base = makeTmpBase();
const dbPath = join(base, ".sf", "sf.db");
openDatabase(dbPath);
try {
const params = validParams();
const result = await handlePlanMilestone(
{
...params,
slices: [
{
...params.slices![0]!,
proofLevel:
'Contract proof.", "integrationClosure": "leaked field"',
},
],
},
base,
);
assert.ok("error" in result);
assert.match(
result.error,
/validation failed: slices\[0\]\.proofLevel appears to contain leaked JSON fields/,
);
} finally {
cleanup(base);
}
});
test("handlePlanMilestone normalizes escaped newlines in planning arrays", async () => {
const base = makeTmpBase();
const dbPath = join(base, ".sf", "sf.db");
openDatabase(dbPath);
try {
const result = await handlePlanMilestone(
{
...validParams(),
successCriteria: ["First criterion\\nSecond criterion"],
},
base,
);
assert.ok(
!("error" in result),
`unexpected error: ${"error" in result ? result.error : ""}`,
);
const milestone = getMilestone("M001");
assert.deepEqual(milestone?.success_criteria, [
"First criterion\nSecond criterion",
]);
} finally {
cleanup(base);
}
});
test("handlePlanMilestone scaffolds common milestone slices from templateId", async () => {
const base = makeTmpBase();
const dbPath = join(base, ".sf", "sf.db");
openDatabase(dbPath);
try {
const params = validParams();
const result = await handlePlanMilestone(
{
milestoneId: params.milestoneId,
title: params.title,
vision: params.vision,
templateId: "bugfix",
successCriteria: params.successCriteria,
},
base,
);
assert.ok(
!("error" in result),
`unexpected error: ${"error" in result ? result.error : ""}`,
);
const slices = getMilestoneSlices("M001");
assert.equal(slices.length, 3);
assert.equal(slices[0]?.id, "S01");
assert.equal(slices[1]?.depends[0], "S01");
assert.match(slices[0]?.goal ?? "", /Capture the failing boundary/i);
const roadmapPath = join(
base,
".sf",
"milestones",
"M001",
"M001-ROADMAP.md",
);
const roadmap = readFileSync(roadmapPath, "utf-8");
assert.match(roadmap, /Reproduce and bound the failure/);
assert.match(roadmap, /Verify and guard against regression/);
} finally {
cleanup(base);
}
});
test("handlePlanMilestone surfaces render failures and does not clear parse-visible state on failure", async () => {
const base = makeTmpBase();
const dbPath = join(base, ".sf", "sf.db");
openDatabase(dbPath);
try {
const fallbackRoadmapPath = join(
base,
".sf",
"milestones",
"MISSING",
"MISSING-ROADMAP.md",
);
mkdirSync(fallbackRoadmapPath, { recursive: true });
const result = await handlePlanMilestone(
{ ...validParams(), milestoneId: "MISSING" },
base,
);
assert.ok("error" in result);
assert.match(result.error, /render failed:/);
const existingRoadmapPath = join(
base,
".sf",
"milestones",
"M001",
"M001-ROADMAP.md",
);
writeFileSync(
existingRoadmapPath,
"# M001: Cached roadmap\n\n**Vision:** old value\n\n## Slices\n\n",
"utf-8",
);
const cachedAfter = parseRoadmap(
readFileSync(existingRoadmapPath, "utf-8"),
);
assert.equal(cachedAfter.vision, "old value");
} finally {
cleanup(base);
}
});
test("handlePlanMilestone clears parse-visible roadmap state after successful render", async () => {
const base = makeTmpBase();
const dbPath = join(base, ".sf", "sf.db");
openDatabase(dbPath);
try {
const roadmapPath = join(
base,
".sf",
"milestones",
"M001",
"M001-ROADMAP.md",
);
writeFileSync(
roadmapPath,
"# M001: Cached roadmap\n\n**Vision:** old value\n\n## Slices\n\n",
"utf-8",
);
const cachedBefore = parseRoadmap(readFileSync(roadmapPath, "utf-8"));
assert.equal(cachedBefore.vision, "old value");
const result = await handlePlanMilestone(validParams(), base);
assert.ok(!("error" in result));
const contentAfter = readFileSync(roadmapPath, "utf-8");
assert.match(contentAfter, /Make planning write through the database\./);
assert.match(contentAfter, /S01/);
assert.match(contentAfter, /S02/);
} finally {
cleanup(base);
}
});
test("handlePlanMilestone reruns idempotently and updates existing planning state", async () => {
const base = makeTmpBase();
const dbPath = join(base, ".sf", "sf.db");
openDatabase(dbPath);
try {
const first = await handlePlanMilestone(validParams(), base);
assert.ok(!("error" in first));
const baseParams = validParams();
const second = await handlePlanMilestone(
{
...baseParams,
vision: "Updated vision",
slices: [
{
...baseParams.slices![0]!,
goal: "Updated goal",
observabilityImpact: "Updated observability",
},
baseParams.slices![1]!,
],
},
base,
);
assert.ok(!("error" in second));
const milestone = getMilestone("M001");
assert.equal(milestone?.vision, "Updated vision");
const slices = getMilestoneSlices("M001");
assert.equal(slices.length, 2);
assert.equal(slices[0]?.goal, "Updated goal");
assert.equal(slices[0]?.observability_impact, "Updated observability");
} finally {
cleanup(base);
}
});
test("handlePlanMilestone preserves completed slice status on re-plan (#2558)", async () => {
const base = makeTmpBase();
const dbPath = join(base, ".sf", "sf.db");
openDatabase(dbPath);
try {
// Initial plan — both slices start as "pending"
const first = await handlePlanMilestone(validParams(), base);
assert.ok(
!("error" in first),
`unexpected error: ${"error" in first ? first.error : ""}`,
);
const _baseParams = validParams();
// Mark S01 as complete (simulates work done in a worktree)
updateSliceStatus("M001", "S01", "complete", new Date().toISOString());
const s01Before = getSlice("M001", "S01");
assert.equal(
s01Before?.status,
"complete",
"S01 should be complete before re-plan",
);
// Re-plan the same milestone — S01 must stay "complete", S02 stays "pending"
const second = await handlePlanMilestone(validParams(), base);
assert.ok(
!("error" in second),
`unexpected error: ${"error" in second ? second.error : ""}`,
);
const s01After = getSlice("M001", "S01");
assert.equal(
s01After?.status,
"complete",
"S01 status must be preserved as complete after re-plan",
);
const s02After = getSlice("M001", "S02");
assert.equal(s02After?.status, "pending", "S02 should remain pending");
} finally {
cleanup(base);
}
});
test("plan-milestone re-plan preserves completed status and updates slice fields (#2558)", async () => {
const base = makeTmpBase();
const dbPath = join(base, ".sf", "sf.db");
openDatabase(dbPath);
try {
// Initial plan — both slices start as "pending"
const first = await handlePlanMilestone(validParams(), base);
assert.ok(
!("error" in first),
`unexpected error: ${"error" in first ? first.error : ""}`,
);
const baseParams = validParams();
// Mark S01 as complete (simulates work done in worktree, then reconciled)
updateSliceStatus("M001", "S01", "complete", new Date().toISOString());
assert.equal(getSlice("M001", "S01")?.status, "complete");
// Re-plan with updated title for S01.
// The handler must:
// 1. NOT downgrade S01 from "complete" to "pending"
// 2. Update S01's non-status fields (title, risk, depends, demo)
// 3. Keep S02 as "pending"
const updatedParams = {
...baseParams,
slices: [
{ ...baseParams.slices![0]!, title: "Updated S01 title", risk: "high" },
baseParams.slices![1]!,
],
};
const second = await handlePlanMilestone(updatedParams, base);
assert.ok(
!("error" in second),
`unexpected error: ${"error" in second ? second.error : ""}`,
);
const s01After = getSlice("M001", "S01");
assert.equal(
s01After?.status,
"complete",
"completed slice status must survive re-plan",
);
assert.equal(
s01After?.title,
"Updated S01 title",
"title should update on re-plan",
);
assert.equal(s01After?.risk, "high", "risk should update on re-plan");
const s02After = getSlice("M001", "S02");
assert.equal(s02After?.status, "pending", "pending slice stays pending");
} finally {
cleanup(base);
}
});
test("handlePlanMilestone promotes pre-existing queued milestone to active (#3022)", async () => {
const base = makeTmpBase();
const dbPath = join(base, ".sf", "sf.db");
openDatabase(dbPath);
try {
// Simulate ensureMilestoneDbRow: pre-create row with status "queued"
// (this is what sf_milestone_generate_id does)
insertMilestone({ id: "M001", status: "queued" });
const before = getMilestone("M001");
assert.equal(
before?.status,
"queued",
"pre-condition: milestone should start as queued",
);
// Now plan the milestone — status should be promoted to "active"
const result = await handlePlanMilestone(validParams(), base);
assert.ok(
!("error" in result),
`unexpected error: ${"error" in result ? result.error : ""}`,
);
const after = getMilestone("M001");
assert.equal(
after?.status,
"active",
"milestone status should be promoted from queued to active",
);
assert.equal(
after?.title,
"DB-backed planning",
"milestone title should be set",
);
} finally {
cleanup(base);
}
});