chore(template): clean-slate template snapshot from bb4a0c7
Curated, product-agnostic snapshot of the post-story-04 tree: demo content deleted, auth-only reference feature, web-next shell, all gates green. Product-specific docs, ADRs 027-029, PRDs/epics/archive, editor library traces, and product naming are curated out; generic template repairs (coverage provider devDeps, root test:coverage script, live lint fixes, root-only release-please) are kept. See TEMPLATE.md for provenance, curation list, and usage. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016j8z4VHjedXDTjEDNg7qHK
This commit is contained in:
162
scripts/work/decompose.test.mjs
Normal file
162
scripts/work/decompose.test.mjs
Normal file
@@ -0,0 +1,162 @@
|
||||
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 });
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user