import * as fs from "node:fs"; import * as os from "node:os"; import * as path from "node:path"; import { describe, expect, it } from "vitest"; import { createResearchMapReceipt, writeResearchMapReceipt } from "./research-map-receipt.js"; const phase = { name: "Add receipt primitive", goal: "Map research into a bounded receipt and tests", acceptanceCriteria: ["receipt is typed", "forbidden implementation scope is blocked"], verification: ["npm run -s test -- src/fable5/research-map-receipt.test.ts"], }; describe("research map receipts", () => { it("creates ready bounded research plans", () => { const receipt = createResearchMapReceipt({ now: new Date("2026-07-01T00:00:00.000Z"), taskLabel: "ast-context", task: "Map AST context research into Fable receipt primitives", constraints: ["advisory-only", "no production mutation"], existingPrimitives: ["plan-receipt", "context-workspace-receipt", "factory-deploy", "rsi-reconcile"], recommendedPlan: [phase], skipped: ["deploy changes", "dashboard", "vendored tools", "new model architecture"], }); expect(receipt).toMatchObject({ schema: "fable.research_map.receipt.v1", decision: "ready", reasons: [], deployAttempted: false, }); expect(receipt.skipped).toContain("deploy changes"); expect(receipt.existingPrimitives).toContain("plan-receipt"); }); it("blocks missing task metadata and phases", () => { const receipt = createResearchMapReceipt({ taskLabel: "", task: "", recommendedPlan: [] }); expect(receipt.decision).toBe("blocked"); expect(receipt.reasons).toContain("missing task label"); expect(receipt.reasons).toContain("missing task"); expect(receipt.reasons).toContain("missing recommended phases"); }); it("requires acceptance criteria and verification", () => { const receipt = createResearchMapReceipt({ taskLabel: "bad", task: "Bad map", recommendedPlan: [{ name: "x", goal: "y", acceptanceCriteria: [], verification: [] }], }); expect(receipt.decision).toBe("blocked"); expect(receipt.reasons).toContain("each phase needs acceptance criteria"); expect(receipt.reasons).toContain("each phase needs verification steps"); }); it("blocks forbidden implementation scope in recommended phases", () => { const receipt = createResearchMapReceipt({ taskLabel: "overbuild", task: "Map research safely", recommendedPlan: [{ ...phase, goal: "Build a new model architecture and dashboard" }], }); expect(receipt.decision).toBe("blocked"); expect(receipt.reasons[0]).toContain("forbidden scope"); expect(receipt.reasons[0]).toContain("dashboard"); expect(receipt.reasons[0]).toContain("new model architecture"); }); it("does not block forbidden terms when they are explicitly skipped", () => { const receipt = createResearchMapReceipt({ taskLabel: "bounded", task: "Map research safely", recommendedPlan: [phase], skipped: ["deploy", "dashboard", "vendor", "new model architecture"], }); expect(receipt.decision).toBe("ready"); }); it("writes receipts", () => { const dir = fs.mkdtempSync(path.join(os.tmpdir(), "research-map-")); const file = path.join(dir, "receipt.json"); writeResearchMapReceipt(file, createResearchMapReceipt({ taskLabel: "x", task: "y", recommendedPlan: [phase] })); expect(JSON.parse(fs.readFileSync(file, "utf-8"))).toMatchObject({ schema: "fable.research_map.receipt.v1" }); fs.rmSync(dir, { recursive: true, force: true }); }); });