diff --git a/src/resources/extensions/sf/tests/doctor-plan-dir-normalization.test.mjs b/src/resources/extensions/sf/tests/doctor-plan-dir-normalization.test.mjs index 986842db4..0f2d70612 100644 --- a/src/resources/extensions/sf/tests/doctor-plan-dir-normalization.test.mjs +++ b/src/resources/extensions/sf/tests/doctor-plan-dir-normalization.test.mjs @@ -13,12 +13,7 @@ import { checkEngineHealth, normalizeLegacyPlanSlugDirectories, } from "../doctor-engine-checks.js"; -import { - closeDatabase, - insertMilestone, - insertSlice, - openDatabase, -} from "../sf-db.js"; +import { closeDatabase, openDatabase } from "../sf-db.js"; const tmpDirs = []; @@ -133,43 +128,4 @@ describe("doctor plan directory normalization", () => { false, ); }); - - test("checkEngineHealth_when_planning_records_lack_purpose_reports_db_purpose_errors", async () => { - const project = makeProject(); - const dbPath = join(project, ".sf", "sf.db"); - assert.equal(openDatabase(dbPath), true); - insertMilestone({ - id: "M001", - title: "Missing purpose milestone", - status: "active", - planning: { vision: " " }, - }); - insertSlice({ - milestoneId: "M001", - id: "S01", - title: "Missing purpose slice", - status: "active", - sequence: 1, - planning: { goal: " " }, - }); - closeDatabase(); - const issues = []; - - await checkEngineHealth(project, issues, [], () => false); - - assert.deepEqual( - issues - .filter((issue) => - [ - "db_milestone_missing_vision", - "db_slice_missing_goal", - ].includes(issue.code), - ) - .map((issue) => [issue.code, issue.scope, issue.unitId]), - [ - ["db_milestone_missing_vision", "milestone", "M001"], - ["db_slice_missing_goal", "slice", "M001/S01"], - ], - ); - }); }); diff --git a/src/resources/extensions/sf/tests/doctor-purpose-gate.test.mjs b/src/resources/extensions/sf/tests/doctor-purpose-gate.test.mjs index 5558c4d0b..6f2c12989 100644 --- a/src/resources/extensions/sf/tests/doctor-purpose-gate.test.mjs +++ b/src/resources/extensions/sf/tests/doctor-purpose-gate.test.mjs @@ -1,56 +1,86 @@ +/** + * doctor-purpose-gate.test.mjs — doctor catches DB planning records that lack purpose. + * + * Purpose: prove that DB-first planning cannot advance through autonomous + * health checks when milestone vision or slice goal fields are blank. + */ import assert from "node:assert/strict"; -import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { mkdirSync, mkdtempSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, test } from "vitest"; import { checkEngineHealth } from "../doctor-engine-checks.js"; -import { closeDatabase, openDatabase } from "../sf-db.js"; +import { + closeDatabase, + insertMilestone, + insertSlice, + openDatabase, +} from "../sf-db.js"; const tmpDirs = []; -afterEach(() => { - closeDatabase(); - while (tmpDirs.length > 0) { - const dir = tmpDirs.pop(); - if (dir) rmSync(dir, { recursive: true, force: true }); - } -}); - function makeProject() { - const dir = mkdtempSync(join(tmpdir(), "sf-purpose-gate-")); - tmpDirs.push(dir); - mkdirSync(join(dir, ".sf"), { recursive: true }); - writeFileSync(join(dir, "package.json"), JSON.stringify({ name: "singularity-forge" })); - openDatabase(join(dir, ".sf", "sf.db")); - return dir; + const dir = mkdtempSync(join(tmpdir(), "sf-purpose-gate-")); + tmpDirs.push(dir); + mkdirSync(join(dir, ".sf"), { recursive: true }); + openDatabase(join(dir, ".sf", "sf.db")); + return dir; } -test("checkEngineHealth_flags_milestones_without_vision", async () => { - const dir = makeProject(); - const db = new (await import("node:sqlite")).DatabaseSync(join(dir, ".sf", "sf.db")); - db.prepare('INSERT INTO milestones (id, title, status, depends_on, vision, success_criteria, key_risks, proof_strategy, verification_contract, verification_integration, verification_operational, verification_uat, definition_of_done, requirement_coverage, boundary_map_markdown, vision_meeting_json, product_research_json) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)') - .run("M001", "Test", "active", "", "", "", "", "", "", "", "", "", "", "", "", "", ""); - - const issues = []; - await checkEngineHealth(dir, issues, [], () => false); - - const missingVision = issues.find(i => i.code === "db_milestone_missing_vision"); - assert.ok(missingVision, "expected db_milestone_missing_vision issue"); - assert.equal(missingVision.scope, "milestone"); +afterEach(() => { + closeDatabase(); + while (tmpDirs.length > 0) { + const dir = tmpDirs.pop(); + if (dir) rmSync(dir, { recursive: true, force: true }); + } }); -test("checkEngineHealth_flags_slices_without_goal", async () => { - const dir = makeProject(); - const db = new (await import("node:sqlite")).DatabaseSync(join(dir, ".sf", "sf.db")); - db.prepare('INSERT INTO milestones (id, title, status, depends_on, vision, success_criteria, key_risks, proof_strategy, verification_contract, verification_integration, verification_operational, verification_uat, definition_of_done, requirement_coverage, boundary_map_markdown, vision_meeting_json, product_research_json) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)') - .run("M001", "Test", "active", "", "vision text", "", "", "", "", "", "", "", "", "", "", "", ""); - db.prepare('INSERT INTO slices (milestone_id, id, title, status, goal, sequence) VALUES (?, ?, ?, ?, ?, ?)') - .run("M001", "S01", "Slice", "pending", "", 1); - - const issues = []; - await checkEngineHealth(dir, issues, [], () => false); - - const missingGoal = issues.find(i => i.code === "db_slice_missing_goal"); - assert.ok(missingGoal, "expected db_slice_missing_goal issue"); - assert.equal(missingGoal.scope, "slice"); +test("checkEngineHealth_when_milestone_vision_is_blank_reports_purpose_error", async () => { + const dir = makeProject(); + insertMilestone({ + id: "M001", + title: "Missing purpose milestone", + status: "active", + planning: { vision: " " }, + }); + closeDatabase(); + const issues = []; + + await checkEngineHealth(dir, issues, [], () => false); + + assert.deepEqual( + issues + .filter((issue) => issue.code === "db_milestone_missing_vision") + .map((issue) => [issue.scope, issue.unitId]), + [["milestone", "M001"]], + ); +}); + +test("checkEngineHealth_when_slice_goal_is_blank_reports_purpose_error", async () => { + const dir = makeProject(); + insertMilestone({ + id: "M001", + title: "Purposeful milestone", + status: "active", + planning: { vision: "Protect DB-backed planning purpose gates." }, + }); + insertSlice({ + milestoneId: "M001", + id: "S01", + title: "Missing purpose slice", + status: "active", + sequence: 1, + planning: { goal: " " }, + }); + closeDatabase(); + const issues = []; + + await checkEngineHealth(dir, issues, [], () => false); + + assert.deepEqual( + issues + .filter((issue) => issue.code === "db_slice_missing_goal") + .map((issue) => [issue.scope, issue.unitId]), + [["slice", "M001/S01"]], + ); });