import { test, describe } from "node:test"; import assert from "node:assert/strict"; import fs from "node:fs"; import path from "node:path"; import os from "node:os"; import { validatePrdForDecompose, runCli } from "./decompose.mjs"; function writePrd(dir, id, status) { const file = path.join(dir, `${id}.prd.md`); fs.writeFileSync( file, `--- id: ${id} title: Test PRD type: prd status: ${status} author: tester created: 2026-05-13 --- body content `, ); return file; } function setupRepo() { const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "decompose-")); fs.mkdirSync(path.join(tmp, "prds"), { recursive: true }); return tmp; } describe("validatePrdForDecompose", () => { test("accepts an approved PRD", () => { const tmp = setupRepo(); try { const file = writePrd(path.join(tmp, "prds"), "test-1", "approved"); const result = validatePrdForDecompose(file); assert.equal(result.frontmatter.status, "approved"); assert.ok(result.text.includes("body content")); } finally { fs.rmSync(tmp, { recursive: true, force: true }); } }); test("rejects draft (must go through human review)", () => { const tmp = setupRepo(); try { const file = writePrd(path.join(tmp, "prds"), "test-2", "draft"); assert.throws( () => validatePrdForDecompose(file), /status is "draft".*Flip status to "approved"/, ); } finally { fs.rmSync(tmp, { recursive: true, force: true }); } }); test("rejects in-review", () => { const tmp = setupRepo(); try { const file = writePrd(path.join(tmp, "prds"), "test-3", "in-review"); assert.throws( () => validatePrdForDecompose(file), /status is "in-review"/, ); } finally { fs.rmSync(tmp, { recursive: true, force: true }); } }); test("rejects shipped (epic already exists)", () => { const tmp = setupRepo(); try { const file = writePrd(path.join(tmp, "prds"), "test-4", "shipped"); assert.throws(() => validatePrdForDecompose(file), /status is "shipped"/); } finally { fs.rmSync(tmp, { recursive: true, force: true }); } }); test("rejects unknown status", () => { const tmp = setupRepo(); try { const file = writePrd(path.join(tmp, "prds"), "test-5", "cancelled"); assert.throws( () => validatePrdForDecompose(file), /Unexpected PRD status "cancelled"/, ); } finally { fs.rmSync(tmp, { recursive: true, force: true }); } }); test("rejects missing file", () => { assert.throws( () => validatePrdForDecompose("/nonexistent/path.prd.md"), /PRD file not found/, ); }); }); describe("runCli (print mode)", () => { test("returns 1 + writes error when PRD id not found", async () => { const tmp = setupRepo(); const errors = []; const origError = console.error; console.error = (m) => errors.push(m); try { const code = await runCli(["nonexistent-id"], { repoRoot: tmp, workRoot: tmp, }); assert.equal(code, 1); assert.ok(errors.some((e) => /not found under docs\/work\/prds/.test(e))); } finally { console.error = origError; fs.rmSync(tmp, { recursive: true, force: true }); } }); test("returns 1 + writes error when PRD is draft", async () => { const tmp = setupRepo(); const errors = []; const origError = console.error; console.error = (m) => errors.push(m); try { writePrd(path.join(tmp, "prds"), "draft-prd", "draft"); const code = await runCli(["draft-prd"], { repoRoot: tmp, workRoot: tmp, }); assert.equal(code, 1); assert.ok(errors.some((e) => /Cannot decompose/.test(e))); } finally { console.error = origError; fs.rmSync(tmp, { recursive: true, force: true }); } }); test("prints plan when PRD is approved + returns 0", async () => { const tmp = setupRepo(); const logs = []; const origLog = console.log; console.log = (m) => logs.push(m); try { writePrd(path.join(tmp, "prds"), "approved-prd", "approved"); const code = await runCli(["approved-prd"], { repoRoot: tmp, workRoot: tmp, }); assert.equal(code, 0); const all = logs.join("\n"); assert.match(all, /Decompose plan/); assert.match(all, /approved-prd/); assert.match(all, /eligible/); } finally { console.log = origLog; fs.rmSync(tmp, { recursive: true, force: true }); } }); });