singularity-forge/src/resources/extensions/sf/tests/markdown-renderer.test.ts

1759 lines
50 KiB
TypeScript

import assert from "node:assert/strict";
import * as fs from "node:fs";
import * as os from "node:os";
import * as path from "node:path";
import { test } from "node:test";
import { clearParseCache, parseSummary, parseTaskPlanFile } from "../files.ts";
import {
detectStaleRenders,
renderAllFromDb,
renderPlanCheckboxes,
renderPlanFromDb,
renderRoadmapCheckboxes,
renderSliceSummary,
renderTaskPlanFromDb,
renderTaskSummary,
repairStaleRenders,
} from "../markdown-renderer.ts";
import { parsePlan, parseRoadmap } from "../parsers-legacy.ts";
import { _clearSfRootCache, clearPathCache } from "../paths.ts";
import {
_getAdapter,
closeDatabase,
getAllMilestones,
getArtifact,
getMilestoneSlices,
insertArtifact,
insertMilestone,
insertSlice,
insertTask,
openDatabase,
} from "../sf-db.ts";
import { invalidateStateCache } from "../state.ts";
// ═══════════════════════════════════════════════════════════════════════════
// Helpers
// ═══════════════════════════════════════════════════════════════════════════
function makeTmpDir(): string {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "sf-renderer-"));
fs.mkdirSync(path.join(dir, ".sf"), { recursive: true });
return dir;
}
function cleanupDir(dir: string): void {
try {
fs.rmSync(dir, { recursive: true, force: true });
} catch {
/* swallow */
}
}
function clearAllCaches(): void {
clearParseCache();
clearPathCache();
_clearSfRootCache();
invalidateStateCache();
}
/**
* Create on-disk directory structure for a milestone/slice/task tree
* so that path resolvers work correctly.
*/
function scaffoldDirs(tmpDir: string, mid: string, sliceIds: string[]): void {
const msDir = path.join(tmpDir, ".sf", "milestones", mid);
fs.mkdirSync(msDir, { recursive: true });
for (const sid of sliceIds) {
const sliceDir = path.join(msDir, "slices", sid);
fs.mkdirSync(path.join(sliceDir, "tasks"), { recursive: true });
}
}
// ─── Fixture: Roadmap Template ────────────────────────────────────────────
function makeRoadmapContent(
slices: Array<{ id: string; title: string; done: boolean }>,
): string {
const lines: string[] = [];
lines.push("# M001 Roadmap");
lines.push("");
lines.push("**Vision:** Test milestone");
lines.push("");
lines.push("## Slices");
lines.push("");
for (const s of slices) {
const checkbox = s.done ? "[x]" : "[ ]";
lines.push(
`- ${checkbox} **${s.id}: ${s.title}** \`risk:medium\` \`depends:[]\``,
);
}
lines.push("");
return lines.join("\n");
}
// ─── Fixture: Plan Template ───────────────────────────────────────────────
function makePlanContent(
sliceId: string,
tasks: Array<{ id: string; title: string; done: boolean }>,
): string {
const lines: string[] = [];
lines.push(`# ${sliceId}: Test Slice`);
lines.push("");
lines.push("**Goal:** Test slice goal");
lines.push("**Demo:** Test demo");
lines.push("");
lines.push("## Must-Haves");
lines.push("");
lines.push("- Everything works");
lines.push("");
lines.push("## Tasks");
lines.push("");
for (const t of tasks) {
const checkbox = t.done ? "[x]" : "[ ]";
lines.push(`- ${checkbox} **${t.id}: ${t.title}** \`est:1h\``);
}
lines.push("");
return lines.join("\n");
}
// ─── Fixture: Task Summary Template ───────────────────────────────────────
function makeTaskSummaryContent(taskId: string): string {
return [
"---",
`id: ${taskId}`,
"parent: S01",
"milestone: M001",
"duration: 45m",
"verification_result: all-pass",
`completed_at: ${new Date().toISOString()}`,
"blocker_discovered: false",
"provides: []",
"requires: []",
"affects: []",
"key_files:",
" - src/test.ts",
"key_decisions: []",
"patterns_established: []",
"drill_down_paths: []",
"observability_surfaces: []",
"---",
"",
`# ${taskId}: Test Task Summary`,
"",
"**Implemented test functionality**",
"",
"## What Happened",
"",
"Built the test feature.",
"",
"## Deviations",
"",
"None.",
"",
"## Files Created/Modified",
"",
"- `src/test.ts` — main implementation",
"",
"## Verification Evidence",
"",
"| Command | Exit | Verdict | Duration |",
"|---------|------|---------|----------|",
"| `npm test` | 0 | ✅ pass | 2.1s |",
"",
].join("\n");
}
// ═══════════════════════════════════════════════════════════════════════════
// DB Accessor Tests
// ═══════════════════════════════════════════════════════════════════════════
test("── markdown-renderer: DB accessor basics ──", () => {
openDatabase(":memory:");
// getAllMilestones — empty
const empty = getAllMilestones();
assert.deepStrictEqual(
empty.length,
0,
"getAllMilestones returns empty when no milestones",
);
// Insert and retrieve
insertMilestone({ id: "M001", title: "Test MS", status: "active" });
insertMilestone({ id: "M002", title: "Second MS", status: "active" });
const all = getAllMilestones();
assert.deepStrictEqual(
all.length,
2,
"getAllMilestones returns 2 milestones",
);
assert.deepStrictEqual(all[0].id, "M001", "first milestone is M001");
assert.deepStrictEqual(all[1].id, "M002", "second milestone is M002");
assert.deepStrictEqual(all[0].title, "Test MS", "milestone title correct");
assert.deepStrictEqual(all[0].status, "active", "milestone status correct");
// getMilestoneSlices — empty
const noSlices = getMilestoneSlices("M001");
assert.deepStrictEqual(
noSlices.length,
0,
"getMilestoneSlices returns empty when no slices",
);
// Insert slices and retrieve
insertSlice({
id: "S01",
milestoneId: "M001",
title: "Slice 1",
status: "complete",
});
insertSlice({
id: "S02",
milestoneId: "M001",
title: "Slice 2",
status: "pending",
});
insertSlice({
id: "S01",
milestoneId: "M002",
title: "M2 Slice",
status: "pending",
});
const m1Slices = getMilestoneSlices("M001");
assert.deepStrictEqual(m1Slices.length, 2, "M001 has 2 slices");
assert.deepStrictEqual(m1Slices[0].id, "S01", "first slice is S01");
assert.deepStrictEqual(
m1Slices[0].status,
"complete",
"S01 status is complete",
);
assert.deepStrictEqual(m1Slices[1].id, "S02", "second slice is S02");
assert.deepStrictEqual(
m1Slices[1].status,
"pending",
"S02 status is pending",
);
const m2Slices = getMilestoneSlices("M002");
assert.deepStrictEqual(m2Slices.length, 1, "M002 has 1 slice");
closeDatabase();
});
test("── markdown-renderer: getArtifact accessor ──", () => {
openDatabase(":memory:");
// Not found
const missing = getArtifact("nonexistent/path");
assert.deepStrictEqual(
missing,
null,
"getArtifact returns null for missing path",
);
// Insert and retrieve
insertArtifact({
path: "milestones/M001/M001-ROADMAP.md",
artifact_type: "ROADMAP",
milestone_id: "M001",
slice_id: null,
task_id: null,
full_content: "# Roadmap content",
});
const found = getArtifact("milestones/M001/M001-ROADMAP.md");
assert.ok(found !== null, "getArtifact returns non-null for existing path");
assert.deepStrictEqual(
found!.artifact_type,
"ROADMAP",
"artifact type correct",
);
assert.deepStrictEqual(found!.milestone_id, "M001", "milestone_id correct");
assert.deepStrictEqual(
found!.full_content,
"# Roadmap content",
"content correct",
);
closeDatabase();
});
// ═══════════════════════════════════════════════════════════════════════════
// Roadmap Checkbox Round-Trip
// ═══════════════════════════════════════════════════════════════════════════
test("── markdown-renderer: renderRoadmapCheckboxes round-trip ──", async () => {
const tmpDir = makeTmpDir();
const dbPath = path.join(tmpDir, ".sf", "sf.db");
openDatabase(dbPath);
clearAllCaches();
try {
scaffoldDirs(tmpDir, "M001", ["S01", "S02"]);
// Seed DB with milestone and slices
insertMilestone({ id: "M001", title: "Test", status: "active" });
insertSlice({
id: "S01",
milestoneId: "M001",
title: "Core setup",
status: "complete",
});
insertSlice({
id: "S02",
milestoneId: "M001",
title: "Rendering",
status: "pending",
});
// Write a roadmap file on disk with BOTH slices unchecked
const roadmapContent = makeRoadmapContent([
{ id: "S01", title: "Core setup", done: false },
{ id: "S02", title: "Rendering", done: false },
]);
const roadmapPath = path.join(
tmpDir,
".sf",
"milestones",
"M001",
"M001-ROADMAP.md",
);
fs.writeFileSync(roadmapPath, roadmapContent);
clearAllCaches();
// Render — should set S01 [x] and leave S02 [ ]
const ok = await renderRoadmapCheckboxes(tmpDir, "M001");
assert.ok(ok, "renderRoadmapCheckboxes returns true");
// Read rendered file and parse
const rendered = fs.readFileSync(roadmapPath, "utf-8");
clearAllCaches();
const parsed = parseRoadmap(rendered);
assert.deepStrictEqual(
parsed.slices.length,
2,
"roadmap has 2 slices after render",
);
const s01 = parsed.slices.find((s) => s.id === "S01");
const s02 = parsed.slices.find((s) => s.id === "S02");
assert.ok(!!s01, "S01 found in parsed roadmap");
assert.ok(!!s02, "S02 found in parsed roadmap");
assert.ok(s01!.done, "S01 is checked (done) after render");
assert.ok(!s02!.done, "S02 is unchecked (pending) after render");
// Verify artifact stored in DB
const artifact = getArtifact("milestones/M001/M001-ROADMAP.md");
assert.ok(artifact !== null, "roadmap artifact stored in DB after render");
assert.ok(
artifact!.full_content.includes("[x] **S01:"),
"DB artifact has S01 checked",
);
assert.ok(
artifact!.full_content.includes("[ ] **S02:"),
"DB artifact has S02 unchecked",
);
} finally {
closeDatabase();
cleanupDir(tmpDir);
}
});
test("── markdown-renderer: renderRoadmapCheckboxes bidirectional ──", async () => {
const tmpDir = makeTmpDir();
const dbPath = path.join(tmpDir, ".sf", "sf.db");
openDatabase(dbPath);
clearAllCaches();
try {
scaffoldDirs(tmpDir, "M001", ["S01", "S02"]);
insertMilestone({ id: "M001", title: "Test", status: "active" });
// S01 is PENDING in DB, but checked on disk — should be unchecked
insertSlice({
id: "S01",
milestoneId: "M001",
title: "Core setup",
status: "pending",
});
insertSlice({
id: "S02",
milestoneId: "M001",
title: "Rendering",
status: "complete",
});
// Write roadmap with S01 checked and S02 unchecked (opposite of DB state)
const roadmapContent = makeRoadmapContent([
{ id: "S01", title: "Core setup", done: true },
{ id: "S02", title: "Rendering", done: false },
]);
const roadmapPath = path.join(
tmpDir,
".sf",
"milestones",
"M001",
"M001-ROADMAP.md",
);
fs.writeFileSync(roadmapPath, roadmapContent);
clearAllCaches();
const ok = await renderRoadmapCheckboxes(tmpDir, "M001");
assert.ok(ok, "bidirectional render returns true");
const rendered = fs.readFileSync(roadmapPath, "utf-8");
clearAllCaches();
const parsed = parseRoadmap(rendered);
const s01 = parsed.slices.find((s) => s.id === "S01");
const s02 = parsed.slices.find((s) => s.id === "S02");
assert.ok(
!s01!.done,
"S01 unchecked (DB says pending, was checked on disk)",
);
assert.ok(
s02!.done,
"S02 checked (DB says complete, was unchecked on disk)",
);
} finally {
closeDatabase();
cleanupDir(tmpDir);
}
});
// ═══════════════════════════════════════════════════════════════════════════
// Plan Checkbox Round-Trip
// ═══════════════════════════════════════════════════════════════════════════
test("── markdown-renderer: renderPlanCheckboxes round-trip ──", async () => {
const tmpDir = makeTmpDir();
const dbPath = path.join(tmpDir, ".sf", "sf.db");
openDatabase(dbPath);
clearAllCaches();
try {
scaffoldDirs(tmpDir, "M001", ["S01"]);
insertMilestone({ id: "M001", title: "Test", status: "active" });
insertSlice({
id: "S01",
milestoneId: "M001",
title: "Slice",
status: "pending",
});
insertTask({
id: "T01",
sliceId: "S01",
milestoneId: "M001",
title: "First task",
status: "done",
});
insertTask({
id: "T02",
sliceId: "S01",
milestoneId: "M001",
title: "Second task",
status: "done",
});
insertTask({
id: "T03",
sliceId: "S01",
milestoneId: "M001",
title: "Third task",
status: "pending",
});
// Write plan with all tasks unchecked
const planContent = makePlanContent("S01", [
{ id: "T01", title: "First task", done: false },
{ id: "T02", title: "Second task", done: false },
{ id: "T03", title: "Third task", done: false },
]);
const planPath = path.join(
tmpDir,
".sf",
"milestones",
"M001",
"slices",
"S01",
"S01-PLAN.md",
);
fs.writeFileSync(planPath, planContent);
clearAllCaches();
const ok = await renderPlanCheckboxes(tmpDir, "M001", "S01");
assert.ok(ok, "renderPlanCheckboxes returns true");
const rendered = fs.readFileSync(planPath, "utf-8");
clearAllCaches();
const parsed = parsePlan(rendered);
assert.deepStrictEqual(
parsed.tasks.length,
3,
"plan has 3 tasks after render",
);
const t01 = parsed.tasks.find((t) => t.id === "T01");
const t02 = parsed.tasks.find((t) => t.id === "T02");
const t03 = parsed.tasks.find((t) => t.id === "T03");
assert.ok(t01!.done, "T01 checked (done in DB)");
assert.ok(t02!.done, "T02 checked (done in DB)");
assert.ok(!t03!.done, "T03 unchecked (pending in DB)");
} finally {
closeDatabase();
cleanupDir(tmpDir);
}
});
test("── markdown-renderer: renderPlanCheckboxes bidirectional ──", async () => {
const tmpDir = makeTmpDir();
const dbPath = path.join(tmpDir, ".sf", "sf.db");
openDatabase(dbPath);
clearAllCaches();
try {
scaffoldDirs(tmpDir, "M001", ["S01"]);
insertMilestone({ id: "M001", title: "Test", status: "active" });
insertSlice({
id: "S01",
milestoneId: "M001",
title: "Slice",
status: "pending",
});
// T01 pending in DB but checked on disk
insertTask({
id: "T01",
sliceId: "S01",
milestoneId: "M001",
title: "First task",
status: "pending",
});
insertTask({
id: "T02",
sliceId: "S01",
milestoneId: "M001",
title: "Second task",
status: "done",
});
const planContent = makePlanContent("S01", [
{ id: "T01", title: "First task", done: true }, // checked but DB says pending
{ id: "T02", title: "Second task", done: false }, // unchecked but DB says done
]);
const planPath = path.join(
tmpDir,
".sf",
"milestones",
"M001",
"slices",
"S01",
"S01-PLAN.md",
);
fs.writeFileSync(planPath, planContent);
clearAllCaches();
const ok = await renderPlanCheckboxes(tmpDir, "M001", "S01");
assert.ok(ok, "bidirectional plan render returns true");
const rendered = fs.readFileSync(planPath, "utf-8");
clearAllCaches();
const parsed = parsePlan(rendered);
const t01 = parsed.tasks.find((t) => t.id === "T01");
const t02 = parsed.tasks.find((t) => t.id === "T02");
assert.ok(!t01!.done, "T01 unchecked (DB says pending, was checked)");
assert.ok(t02!.done, "T02 checked (DB says done, was unchecked)");
} finally {
closeDatabase();
cleanupDir(tmpDir);
}
});
test("── markdown-renderer: renderPlanFromDb creates parse-compatible slice plan + task plan files ──", async () => {
const tmpDir = makeTmpDir();
const dbPath = path.join(tmpDir, ".sf", "sf.db");
openDatabase(dbPath);
clearAllCaches();
try {
scaffoldDirs(tmpDir, "M001", ["S02"]);
insertMilestone({ id: "M001", title: "Milestone", status: "active" });
insertSlice({
id: "S02",
milestoneId: "M001",
title: "DB-backed planning",
status: "pending",
demo: "Rendered plans exist on disk.",
planning: {
goal: "Render slice plans from DB state.",
successCriteria:
"- Slice plan stays parse-compatible\n- Task plan files are regenerated",
proofLevel: "integration",
integrationClosure: "Wires DB planning rows to markdown artifacts.",
observabilityImpact:
"- Run renderer contract tests\n- Inspect stale-render diagnostics on mismatch",
},
});
insertTask({
id: "T01",
sliceId: "S02",
milestoneId: "M001",
title: "Render slice plan",
status: "pending",
planning: {
description: "Implement the DB-backed slice plan renderer.",
estimate: "45m",
files: ["src/resources/extensions/sf/markdown-renderer.ts"],
verify: "node --test markdown-renderer.test.ts",
inputs: ["src/resources/extensions/sf/markdown-renderer.ts"],
expectedOutput: [
"src/resources/extensions/sf/tests/markdown-renderer.test.ts",
],
observabilityImpact: "Renderer tests cover stale render failure paths.",
},
});
insertTask({
id: "T02",
sliceId: "S02",
milestoneId: "M001",
title: "Render task plan",
status: "pending",
planning: {
description: "Emit the task plan file with conservative frontmatter.",
estimate: "30m",
files: ["src/resources/extensions/sf/files.ts"],
verify: "node --test auto-recovery.test.ts",
inputs: ["src/resources/extensions/sf/files.ts"],
expectedOutput: [
"src/resources/extensions/sf/tests/auto-recovery.test.ts",
],
observabilityImpact:
"Missing task-plan files fail recovery verification.",
},
});
const rendered = await renderPlanFromDb(tmpDir, "M001", "S02");
assert.ok(fs.existsSync(rendered.planPath), "slice plan written to disk");
assert.strictEqual(
rendered.taskPlanPaths.length,
2,
"task plan paths returned for each task",
);
assert.ok(
rendered.taskPlanPaths.every((p) => fs.existsSync(p)),
"all task plan files written to disk",
);
const planContent = fs.readFileSync(rendered.planPath, "utf-8");
clearAllCaches();
const parsedPlan = parsePlan(planContent);
assert.strictEqual(
parsedPlan.id,
"S02",
"rendered slice plan parses with correct slice id",
);
assert.strictEqual(
parsedPlan.goal,
"Render slice plans from DB state.",
"rendered slice plan preserves goal",
);
assert.strictEqual(
parsedPlan.demo,
"Rendered plans exist on disk.",
"rendered slice plan preserves demo",
);
assert.strictEqual(
parsedPlan.mustHaves.length,
2,
"rendered slice plan exposes must-haves",
);
assert.strictEqual(
parsedPlan.tasks.length,
2,
"rendered slice plan exposes all tasks",
);
assert.strictEqual(
parsedPlan.tasks[0].id,
"T01",
"first task parses correctly",
);
assert.ok(
parsedPlan.tasks[0].description.includes("DB-backed slice plan renderer"),
"task description preserved in slice plan",
);
assert.strictEqual(
parsedPlan.tasks[0].files?.[0],
"src/resources/extensions/sf/markdown-renderer.ts",
"files list preserved in slice plan",
);
assert.strictEqual(
parsedPlan.tasks[0].verify,
"node --test markdown-renderer.test.ts",
"verify line preserved in slice plan",
);
const planArtifact = getArtifact("milestones/M001/slices/S02/S02-PLAN.md");
assert.ok(planArtifact !== null, "slice plan artifact stored in DB");
assert.ok(
planArtifact!.full_content.includes("## Tasks"),
"stored plan artifact contains task section",
);
const taskPlanPath = path.join(
tmpDir,
".sf",
"milestones",
"M001",
"slices",
"S02",
"tasks",
"T01-PLAN.md",
);
const taskPlanContent = fs.readFileSync(taskPlanPath, "utf-8");
const taskPlanFile = parseTaskPlanFile(taskPlanContent);
assert.strictEqual(
taskPlanFile.frontmatter.estimated_steps,
1,
"task plan frontmatter exposes estimated_steps",
);
assert.strictEqual(
taskPlanFile.frontmatter.estimated_files,
1,
"task plan frontmatter exposes estimated_files",
);
assert.strictEqual(
taskPlanFile.frontmatter.skills_used.length,
0,
"task plan frontmatter uses conservative empty skills list",
);
assert.match(
taskPlanContent,
/^# T01: Render slice plan/m,
"task plan renders task heading",
);
assert.match(
taskPlanContent,
/^## Inputs$/m,
"task plan renders Inputs section",
);
assert.match(
taskPlanContent,
/^## Expected Output$/m,
"task plan renders Expected Output section",
);
assert.match(
taskPlanContent,
/^## Verification$/m,
"task plan renders Verification section",
);
const taskArtifact = getArtifact(
"milestones/M001/slices/S02/tasks/T01-PLAN.md",
);
assert.ok(taskArtifact !== null, "task plan artifact stored in DB");
assert.ok(
taskArtifact!.full_content.includes("skills_used: []"),
"stored task plan artifact preserves conservative skills_used",
);
} finally {
closeDatabase();
cleanupDir(tmpDir);
}
});
test("── markdown-renderer: renderTaskPlanFromDb throws for missing task ──", async () => {
const tmpDir = makeTmpDir();
const dbPath = path.join(tmpDir, ".sf", "sf.db");
openDatabase(dbPath);
clearAllCaches();
try {
scaffoldDirs(tmpDir, "M001", ["S02"]);
insertMilestone({ id: "M001", title: "Milestone", status: "active" });
insertSlice({
id: "S02",
milestoneId: "M001",
title: "Slice",
status: "pending",
});
let threw = false;
try {
await renderTaskPlanFromDb(tmpDir, "M001", "S02", "T99");
} catch (error) {
threw = true;
assert.match(
String((error as Error).message),
/task M001\/S02\/T99 not found/,
"renderTaskPlanFromDb should fail clearly when task row is missing",
);
}
assert.ok(
threw,
"renderTaskPlanFromDb throws when the task row is missing",
);
} finally {
closeDatabase();
cleanupDir(tmpDir);
}
});
// ═══════════════════════════════════════════════════════════════════════════
// Task Summary Rendering
// ═══════════════════════════════════════════════════════════════════════════
test("── markdown-renderer: renderTaskSummary round-trip ──", async () => {
const tmpDir = makeTmpDir();
const dbPath = path.join(tmpDir, ".sf", "sf.db");
openDatabase(dbPath);
clearAllCaches();
try {
scaffoldDirs(tmpDir, "M001", ["S01"]);
insertMilestone({ id: "M001", title: "Test", status: "active" });
insertSlice({
id: "S01",
milestoneId: "M001",
title: "Slice",
status: "pending",
});
const summaryContent = makeTaskSummaryContent("T01");
insertTask({
id: "T01",
sliceId: "S01",
milestoneId: "M001",
title: "Test Task",
status: "done",
fullSummaryMd: summaryContent,
});
const ok = await renderTaskSummary(tmpDir, "M001", "S01", "T01");
assert.ok(ok, "renderTaskSummary returns true");
// Verify file exists on disk
const summaryPath = path.join(
tmpDir,
".sf",
"milestones",
"M001",
"slices",
"S01",
"tasks",
"T01-SUMMARY.md",
);
assert.ok(fs.existsSync(summaryPath), "T01-SUMMARY.md written to disk");
// Parse and verify
const rendered = fs.readFileSync(summaryPath, "utf-8");
clearAllCaches();
const parsed = parseSummary(rendered);
assert.deepStrictEqual(
parsed.frontmatter.id,
"T01",
"parsed summary has correct id",
);
assert.deepStrictEqual(
parsed.frontmatter.parent,
"S01",
"parsed summary has correct parent",
);
assert.deepStrictEqual(
parsed.frontmatter.milestone,
"M001",
"parsed summary has correct milestone",
);
assert.deepStrictEqual(
parsed.frontmatter.duration,
"45m",
"parsed summary has correct duration",
);
assert.ok(
parsed.title.includes("T01"),
"parsed summary title contains task ID",
);
assert.ok(
parsed.whatHappened.includes("Built the test feature"),
"whatHappened content preserved",
);
} finally {
closeDatabase();
cleanupDir(tmpDir);
}
});
test("── markdown-renderer: renderTaskSummary skips empty ──", async () => {
const tmpDir = makeTmpDir();
const dbPath = path.join(tmpDir, ".sf", "sf.db");
openDatabase(dbPath);
clearAllCaches();
try {
scaffoldDirs(tmpDir, "M001", ["S01"]);
insertMilestone({ id: "M001", title: "Test", status: "active" });
insertSlice({
id: "S01",
milestoneId: "M001",
title: "Slice",
status: "pending",
});
insertTask({
id: "T01",
sliceId: "S01",
milestoneId: "M001",
title: "Task without summary",
status: "pending",
fullSummaryMd: "", // empty summary
});
const ok = await renderTaskSummary(tmpDir, "M001", "S01", "T01");
assert.ok(!ok, "renderTaskSummary returns false for empty summary");
} finally {
closeDatabase();
cleanupDir(tmpDir);
}
});
// ═══════════════════════════════════════════════════════════════════════════
// Slice Summary Rendering
// ═══════════════════════════════════════════════════════════════════════════
test("── markdown-renderer: renderSliceSummary round-trip ──", async () => {
const tmpDir = makeTmpDir();
const dbPath = path.join(tmpDir, ".sf", "sf.db");
openDatabase(dbPath);
clearAllCaches();
try {
scaffoldDirs(tmpDir, "M001", ["S01"]);
insertMilestone({ id: "M001", title: "Test", status: "active" });
insertSlice({
id: "S01",
milestoneId: "M001",
title: "Slice",
status: "complete",
});
// Update slice with summary and UAT content
// Since insertSlice uses INSERT OR IGNORE, we need to set the content via raw adapter
const db = await import("../sf-db.ts");
const adapter = db._getAdapter()!;
adapter
.prepare(
`UPDATE slices SET full_summary_md = :sm, full_uat_md = :um WHERE milestone_id = 'M001' AND id = 'S01'`,
)
.run({
":sm":
"---\nid: S01\nparent: M001\nmilestone: M001\nduration: 2h\nverification_result: all-pass\ncompleted_at: 2025-01-01\nblocker_discovered: false\nprovides: []\nrequires: []\naffects: []\nkey_files:\n - src/index.ts\nkey_decisions: []\npatterns_established: []\ndrill_down_paths: []\nobservability_surfaces: []\n---\n\n# S01: Test Slice Summary\n\n**Completed core functionality**\n\n## What Happened\n\nBuilt the slice.\n\n## Deviations\n\nNone.\n",
":um":
"# S01 UAT\n\n## UAT Type\n\n- UAT mode: artifact-driven\n\n## Checks\n\n- All tests pass\n",
});
const ok = await renderSliceSummary(tmpDir, "M001", "S01");
assert.ok(ok, "renderSliceSummary returns true");
// Verify SUMMARY file
const summaryPath = path.join(
tmpDir,
".sf",
"milestones",
"M001",
"slices",
"S01",
"S01-SUMMARY.md",
);
assert.ok(fs.existsSync(summaryPath), "S01-SUMMARY.md written to disk");
const summaryContent = fs.readFileSync(summaryPath, "utf-8");
assert.ok(
summaryContent.includes("Test Slice Summary"),
"summary content correct",
);
// Verify UAT file
const uatPath = path.join(
tmpDir,
".sf",
"milestones",
"M001",
"slices",
"S01",
"S01-UAT.md",
);
assert.ok(fs.existsSync(uatPath), "S01-UAT.md written to disk");
const uatContent = fs.readFileSync(uatPath, "utf-8");
assert.ok(uatContent.includes("artifact-driven"), "UAT content correct");
} finally {
closeDatabase();
cleanupDir(tmpDir);
}
});
// ═══════════════════════════════════════════════════════════════════════════
// renderAllFromDb
// ═══════════════════════════════════════════════════════════════════════════
test("── markdown-renderer: renderAllFromDb produces all files ──", async () => {
const tmpDir = makeTmpDir();
const dbPath = path.join(tmpDir, ".sf", "sf.db");
openDatabase(dbPath);
clearAllCaches();
try {
// Setup: 2 milestones, M001 has 2 slices with tasks, M002 has 1 slice
scaffoldDirs(tmpDir, "M001", ["S01", "S02"]);
scaffoldDirs(tmpDir, "M002", ["S01"]);
insertMilestone({ id: "M001", title: "First", status: "active" });
insertMilestone({ id: "M002", title: "Second", status: "active" });
insertSlice({
id: "S01",
milestoneId: "M001",
title: "Core",
status: "complete",
});
insertSlice({
id: "S02",
milestoneId: "M001",
title: "Render",
status: "pending",
});
insertSlice({
id: "S01",
milestoneId: "M002",
title: "Future",
status: "pending",
});
insertTask({
id: "T01",
sliceId: "S01",
milestoneId: "M001",
title: "DB",
status: "done",
fullSummaryMd: makeTaskSummaryContent("T01"),
});
insertTask({
id: "T01",
sliceId: "S02",
milestoneId: "M001",
title: "Renderer",
status: "pending",
});
insertTask({
id: "T01",
sliceId: "S01",
milestoneId: "M002",
title: "Future task",
status: "pending",
});
// Write roadmap and plan files on disk
const roadmap1 = makeRoadmapContent([
{ id: "S01", title: "Core", done: false },
{ id: "S02", title: "Render", done: false },
]);
fs.writeFileSync(
path.join(tmpDir, ".sf", "milestones", "M001", "M001-ROADMAP.md"),
roadmap1,
);
const roadmap2 = makeRoadmapContent([
{ id: "S01", title: "Future", done: false },
]);
fs.writeFileSync(
path.join(tmpDir, ".sf", "milestones", "M002", "M002-ROADMAP.md"),
roadmap2,
);
const plan1 = makePlanContent("S01", [
{ id: "T01", title: "DB", done: false },
]);
fs.writeFileSync(
path.join(
tmpDir,
".sf",
"milestones",
"M001",
"slices",
"S01",
"S01-PLAN.md",
),
plan1,
);
const plan2 = makePlanContent("S02", [
{ id: "T01", title: "Renderer", done: false },
]);
fs.writeFileSync(
path.join(
tmpDir,
".sf",
"milestones",
"M001",
"slices",
"S02",
"S02-PLAN.md",
),
plan2,
);
const plan3 = makePlanContent("S01", [
{ id: "T01", title: "Future task", done: false },
]);
fs.writeFileSync(
path.join(
tmpDir,
".sf",
"milestones",
"M002",
"slices",
"S01",
"S01-PLAN.md",
),
plan3,
);
clearAllCaches();
const result = await renderAllFromDb(tmpDir);
assert.ok(result.rendered > 0, "renderAllFromDb rendered some files");
assert.deepStrictEqual(
result.errors.length,
0,
"renderAllFromDb had no errors",
);
// Verify M001 roadmap has S01 checked
const m1Roadmap = fs.readFileSync(
path.join(tmpDir, ".sf", "milestones", "M001", "M001-ROADMAP.md"),
"utf-8",
);
clearAllCaches();
const parsed1 = parseRoadmap(m1Roadmap);
const s01 = parsed1.slices.find((s) => s.id === "S01");
assert.ok(s01!.done, "M001 S01 checked after renderAll");
// Verify M001/S01 plan has T01 checked
const m1s1Plan = fs.readFileSync(
path.join(
tmpDir,
".sf",
"milestones",
"M001",
"slices",
"S01",
"S01-PLAN.md",
),
"utf-8",
);
clearAllCaches();
const parsedPlan = parsePlan(m1s1Plan);
assert.ok(parsedPlan.tasks[0].done, "M001/S01 T01 checked after renderAll");
// Verify task summary written
const taskSummaryPath = path.join(
tmpDir,
".sf",
"milestones",
"M001",
"slices",
"S01",
"tasks",
"T01-SUMMARY.md",
);
assert.ok(
fs.existsSync(taskSummaryPath),
"T01 summary written by renderAll",
);
} finally {
closeDatabase();
cleanupDir(tmpDir);
}
});
// ═══════════════════════════════════════════════════════════════════════════
// Graceful Degradation (Disk Fallback)
// ═══════════════════════════════════════════════════════════════════════════
test("── markdown-renderer: graceful fallback reads from disk when artifact not in DB ──", async () => {
const tmpDir = makeTmpDir();
const dbPath = path.join(tmpDir, ".sf", "sf.db");
openDatabase(dbPath);
clearAllCaches();
try {
scaffoldDirs(tmpDir, "M001", ["S01"]);
insertMilestone({ id: "M001", title: "Test", status: "active" });
insertSlice({
id: "S01",
milestoneId: "M001",
title: "Core",
status: "complete",
});
// Write roadmap to disk but NOT in artifacts DB
const roadmapContent = makeRoadmapContent([
{ id: "S01", title: "Core", done: false },
]);
const roadmapPath = path.join(
tmpDir,
".sf",
"milestones",
"M001",
"M001-ROADMAP.md",
);
fs.writeFileSync(roadmapPath, roadmapContent);
clearAllCaches();
// Verify no artifact in DB
const before = getArtifact("milestones/M001/M001-ROADMAP.md");
assert.deepStrictEqual(before, null, "artifact not in DB before render");
// Render — should read from disk, store in DB
const ok = await renderRoadmapCheckboxes(tmpDir, "M001");
assert.ok(ok, "render succeeds with disk fallback");
// Verify artifact now in DB (stored after reading from disk)
const after = getArtifact("milestones/M001/M001-ROADMAP.md");
assert.ok(
after !== null,
"artifact stored in DB after disk fallback render",
);
assert.ok(
after!.full_content.includes("[x] **S01:"),
"DB artifact reflects rendered state",
);
} finally {
closeDatabase();
cleanupDir(tmpDir);
}
});
// ═══════════════════════════════════════════════════════════════════════════
// stderr warnings (graceful degradation diagnostics)
// ═══════════════════════════════════════════════════════════════════════════
test("── markdown-renderer: stderr warning on missing content ──", async () => {
openDatabase(":memory:");
// No milestone/slices in DB, no files on disk — should return false and emit stderr
insertMilestone({ id: "M001", title: "Test", status: "active" });
// No slices inserted — should warn about no slices
const ok = await renderRoadmapCheckboxes("/nonexistent/path", "M001");
assert.ok(!ok, "returns false when no slices in DB");
closeDatabase();
});
// ═══════════════════════════════════════════════════════════════════════════
// Stale Detection — Plan Checkbox Mismatch
// ═══════════════════════════════════════════════════════════════════════════
test("── markdown-renderer: detectStaleRenders finds plan checkbox mismatch ──", () => {
const tmpDir = makeTmpDir();
const dbPath = path.join(tmpDir, ".sf", "sf.db");
openDatabase(dbPath);
clearAllCaches();
try {
scaffoldDirs(tmpDir, "M001", ["S01"]);
insertMilestone({ id: "M001", title: "Test", status: "active" });
insertSlice({
id: "S01",
milestoneId: "M001",
title: "Slice",
status: "pending",
});
// T01 is done, T02 is also done in DB
insertTask({
id: "T01",
sliceId: "S01",
milestoneId: "M001",
title: "First task",
status: "done",
});
insertTask({
id: "T02",
sliceId: "S01",
milestoneId: "M001",
title: "Second task",
status: "done",
});
// Write plan with T01 checked but T02 unchecked
// T01 matches DB (done + checked) but T02 is stale (done but unchecked)
const planContent = makePlanContent("S01", [
{ id: "T01", title: "First task", done: true },
{ id: "T02", title: "Second task", done: false },
]);
const planPath = path.join(
tmpDir,
".sf",
"milestones",
"M001",
"slices",
"S01",
"S01-PLAN.md",
);
fs.writeFileSync(planPath, planContent);
clearAllCaches();
// Render T01 to sync it, but leave T02 out of sync
// Actually, the plan was written with T01 already checked.
// The stale detection should find T02 as stale.
const stale = detectStaleRenders(tmpDir);
assert.ok(stale.length > 0, "detectStaleRenders should find stale entries");
const t02Stale = stale.find((s) => s.reason.includes("T02"));
assert.ok(
!!t02Stale,
"should detect T02 as stale (done in DB, unchecked in plan)",
);
assert.ok(
t02Stale!.reason.includes("done in DB but unchecked"),
"reason should explain the mismatch",
);
// T01 should NOT be stale — it's checked and done
const t01Stale = stale.find((s) => s.reason.includes("T01"));
assert.deepStrictEqual(
t01Stale,
undefined,
"T01 should not be stale (done and checked)",
);
} finally {
closeDatabase();
cleanupDir(tmpDir);
}
});
// ═══════════════════════════════════════════════════════════════════════════
// Stale Repair — Plan Checkbox
// ═══════════════════════════════════════════════════════════════════════════
test("── markdown-renderer: repairStaleRenders fixes plan and second detect returns empty ──", async () => {
const tmpDir = makeTmpDir();
const dbPath = path.join(tmpDir, ".sf", "sf.db");
openDatabase(dbPath);
clearAllCaches();
try {
scaffoldDirs(tmpDir, "M001", ["S01"]);
insertMilestone({ id: "M001", title: "Test", status: "active" });
insertSlice({
id: "S01",
milestoneId: "M001",
title: "Slice",
status: "pending",
});
insertTask({
id: "T01",
sliceId: "S01",
milestoneId: "M001",
title: "First task",
status: "done",
});
insertTask({
id: "T02",
sliceId: "S01",
milestoneId: "M001",
title: "Second task",
status: "done",
});
// Write plan with both tasks unchecked (both are stale since DB says done)
const planContent = makePlanContent("S01", [
{ id: "T01", title: "First task", done: false },
{ id: "T02", title: "Second task", done: false },
]);
const planPath = path.join(
tmpDir,
".sf",
"milestones",
"M001",
"slices",
"S01",
"S01-PLAN.md",
);
fs.writeFileSync(planPath, planContent);
clearAllCaches();
// Verify stale before repair
const staleBefore = detectStaleRenders(tmpDir);
assert.ok(
staleBefore.length > 0,
"should have stale entries before repair",
);
// Repair
const repaired = await repairStaleRenders(tmpDir);
assert.ok(repaired > 0, "repairStaleRenders should repair at least 1 file");
// After repair, detect again — should be empty
clearAllCaches();
const staleAfter = detectStaleRenders(tmpDir);
assert.deepStrictEqual(
staleAfter.length,
0,
"detectStaleRenders should return empty after repair",
);
// Verify the plan file was actually updated
const repairedContent = fs.readFileSync(planPath, "utf-8");
assert.ok(
repairedContent.includes("[x] **T01:"),
"T01 should be checked after repair",
);
assert.ok(
repairedContent.includes("[x] **T02:"),
"T02 should be checked after repair",
);
} finally {
closeDatabase();
cleanupDir(tmpDir);
}
});
// ═══════════════════════════════════════════════════════════════════════════
// Stale Detection — Roadmap Checkbox Mismatch
// ═══════════════════════════════════════════════════════════════════════════
test("── markdown-renderer: detectStaleRenders finds roadmap checkbox mismatch ──", () => {
const tmpDir = makeTmpDir();
const dbPath = path.join(tmpDir, ".sf", "sf.db");
openDatabase(dbPath);
clearAllCaches();
try {
scaffoldDirs(tmpDir, "M001", ["S01", "S02"]);
insertMilestone({ id: "M001", title: "Test", status: "active" });
insertSlice({
id: "S01",
milestoneId: "M001",
title: "Core",
status: "complete",
});
insertSlice({
id: "S02",
milestoneId: "M001",
title: "Render",
status: "pending",
});
// Write roadmap with both slices unchecked (S01 is stale — complete in DB but unchecked)
const roadmapContent = makeRoadmapContent([
{ id: "S01", title: "Core", done: false },
{ id: "S02", title: "Render", done: false },
]);
const roadmapPath = path.join(
tmpDir,
".sf",
"milestones",
"M001",
"M001-ROADMAP.md",
);
fs.writeFileSync(roadmapPath, roadmapContent);
clearAllCaches();
const stale = detectStaleRenders(tmpDir);
const s01Stale = stale.find((s) => s.reason.includes("S01"));
assert.ok(
!!s01Stale,
"should detect S01 as stale (complete in DB, unchecked in roadmap)",
);
const s02Stale = stale.find((s) => s.reason.includes("S02"));
assert.deepStrictEqual(
s02Stale,
undefined,
"S02 should not be stale (pending and unchecked — matches)",
);
} finally {
closeDatabase();
cleanupDir(tmpDir);
}
});
// ═══════════════════════════════════════════════════════════════════════════
// Stale Detection — Missing Task Summary
// ═══════════════════════════════════════════════════════════════════════════
test("── markdown-renderer: detectStaleRenders finds missing task summary ──", () => {
const tmpDir = makeTmpDir();
const dbPath = path.join(tmpDir, ".sf", "sf.db");
openDatabase(dbPath);
clearAllCaches();
try {
scaffoldDirs(tmpDir, "M001", ["S01"]);
insertMilestone({ id: "M001", title: "Test", status: "active" });
insertSlice({
id: "S01",
milestoneId: "M001",
title: "Slice",
status: "pending",
});
// Task is done with full_summary_md, but no SUMMARY.md on disk
const summaryContent = makeTaskSummaryContent("T01");
insertTask({
id: "T01",
sliceId: "S01",
milestoneId: "M001",
title: "Task",
status: "done",
fullSummaryMd: summaryContent,
});
// Also write a plan so plan detection doesn't trigger (T01 is done but not checked)
// We need a plan file so task plan detection works — but we specifically want to test
// the missing summary case, so write plan with T01 checked
const planContent = makePlanContent("S01", [
{ id: "T01", title: "Task", done: true },
]);
const planPath = path.join(
tmpDir,
".sf",
"milestones",
"M001",
"slices",
"S01",
"S01-PLAN.md",
);
fs.writeFileSync(planPath, planContent);
clearAllCaches();
const stale = detectStaleRenders(tmpDir);
const summaryStale = stale.find((s) =>
s.reason.includes("SUMMARY.md missing"),
);
assert.ok(!!summaryStale, "should detect missing T01-SUMMARY.md");
assert.ok(
summaryStale!.reason.includes("T01"),
"reason should mention T01",
);
} finally {
closeDatabase();
cleanupDir(tmpDir);
}
});
// ═══════════════════════════════════════════════════════════════════════════
// Stale Repair — Missing Task Summary
// ═══════════════════════════════════════════════════════════════════════════
test("── markdown-renderer: repairStaleRenders writes missing task summary ──", async () => {
const tmpDir = makeTmpDir();
const dbPath = path.join(tmpDir, ".sf", "sf.db");
openDatabase(dbPath);
clearAllCaches();
try {
scaffoldDirs(tmpDir, "M001", ["S01"]);
insertMilestone({ id: "M001", title: "Test", status: "active" });
insertSlice({
id: "S01",
milestoneId: "M001",
title: "Slice",
status: "pending",
});
const summaryContent = makeTaskSummaryContent("T01");
insertTask({
id: "T01",
sliceId: "S01",
milestoneId: "M001",
title: "Task",
status: "done",
fullSummaryMd: summaryContent,
});
// Write plan with T01 checked so plan detection doesn't trigger
const planContent = makePlanContent("S01", [
{ id: "T01", title: "Task", done: true },
]);
const planPath = path.join(
tmpDir,
".sf",
"milestones",
"M001",
"slices",
"S01",
"S01-PLAN.md",
);
fs.writeFileSync(planPath, planContent);
clearAllCaches();
// Repair
const repaired = await repairStaleRenders(tmpDir);
assert.ok(repaired > 0, "should repair missing summary");
// Verify file written
const summaryPath = path.join(
tmpDir,
".sf",
"milestones",
"M001",
"slices",
"S01",
"tasks",
"T01-SUMMARY.md",
);
assert.ok(
fs.existsSync(summaryPath),
"T01-SUMMARY.md should exist after repair",
);
// Second detect should be empty
clearAllCaches();
const staleAfter = detectStaleRenders(tmpDir);
const summaryStale = staleAfter.find(
(s) =>
s.reason.includes("SUMMARY.md missing") && s.reason.includes("T01"),
);
assert.deepStrictEqual(
summaryStale,
undefined,
"missing summary should be fixed after repair",
);
} finally {
closeDatabase();
cleanupDir(tmpDir);
}
});
// ═══════════════════════════════════════════════════════════════════════════
// Stale Repair — Idempotency
// ═══════════════════════════════════════════════════════════════════════════
test("── markdown-renderer: repairStaleRenders idempotency — fully synced returns 0 ──", async () => {
const tmpDir = makeTmpDir();
const dbPath = path.join(tmpDir, ".sf", "sf.db");
openDatabase(dbPath);
clearAllCaches();
try {
scaffoldDirs(tmpDir, "M001", ["S01"]);
insertMilestone({ id: "M001", title: "Test", status: "active" });
insertSlice({
id: "S01",
milestoneId: "M001",
title: "Slice",
status: "pending",
});
insertTask({
id: "T01",
sliceId: "S01",
milestoneId: "M001",
title: "Task",
status: "done",
});
// Write plan with T01 checked — matches DB
const planContent = makePlanContent("S01", [
{ id: "T01", title: "Task", done: true },
]);
const planPath = path.join(
tmpDir,
".sf",
"milestones",
"M001",
"slices",
"S01",
"S01-PLAN.md",
);
fs.writeFileSync(planPath, planContent);
clearAllCaches();
// No stale entries when everything is in sync (no summary to check since no fullSummaryMd)
const repaired = await repairStaleRenders(tmpDir);
assert.deepStrictEqual(
repaired,
0,
"repairStaleRenders should return 0 on fully synced project",
);
} finally {
closeDatabase();
cleanupDir(tmpDir);
}
});
// ═══════════════════════════════════════════════════════════════════════════
// Stale Detection — Missing Slice Summary + UAT
// ═══════════════════════════════════════════════════════════════════════════
test("── markdown-renderer: detectStaleRenders finds missing slice summary and UAT ──", () => {
const tmpDir = makeTmpDir();
const dbPath = path.join(tmpDir, ".sf", "sf.db");
openDatabase(dbPath);
clearAllCaches();
try {
scaffoldDirs(tmpDir, "M001", ["S01"]);
insertMilestone({ id: "M001", title: "Test", status: "active" });
insertSlice({
id: "S01",
milestoneId: "M001",
title: "Slice",
status: "pending",
});
// Update slice to complete with content via raw adapter
const adapter = _getAdapter()!;
adapter
.prepare(
`UPDATE slices SET status = 'complete', full_summary_md = :sm, full_uat_md = :um WHERE milestone_id = 'M001' AND id = 'S01'`,
)
.run({
":sm":
"---\nid: S01\nparent: M001\nmilestone: M001\n---\n\n# S01: Summary\n\nDone.\n",
":um": "# S01 UAT\n\nAll pass.\n",
});
clearAllCaches();
const stale = detectStaleRenders(tmpDir);
const summaryStale = stale.find(
(s) =>
s.reason.includes("SUMMARY.md missing") && s.reason.includes("S01"),
);
const uatStale = stale.find(
(s) => s.reason.includes("UAT.md missing") && s.reason.includes("S01"),
);
assert.ok(!!summaryStale, "should detect missing S01-SUMMARY.md");
assert.ok(!!uatStale, "should detect missing S01-UAT.md");
} finally {
closeDatabase();
cleanupDir(tmpDir);
}
});
// ═══════════════════════════════════════════════════════════════════════════