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 { parseFrontmatter, serializeFrontmatter, flipPrdStatus, findPrdPath, } from "./prd-ship.mjs"; describe("parseFrontmatter", () => { test("parses scalar keys from a basic frontmatter block", () => { const text = `--- id: test-1 title: Hello status: approved --- body `; const { frontmatter, body } = parseFrontmatter(text); assert.equal(frontmatter.id, "test-1"); assert.equal(frontmatter.title, "Hello"); assert.equal(frontmatter.status, "approved"); assert.match(body, /body/); }); test("parses YAML list values (indented '- item')", () => { const text = `--- id: test-2 shipping-commits: - abc123 - def456 --- body `; const { frontmatter } = parseFrontmatter(text); assert.deepEqual(frontmatter["shipping-commits"], ["abc123", "def456"]); }); test("returns empty frontmatter when none present", () => { const { frontmatter, body } = parseFrontmatter("just body content\n"); assert.deepEqual(frontmatter, {}); assert.equal(body, "just body content\n"); }); }); describe("flipPrdStatus", () => { const baseText = `--- id: test-prd title: Test PRD type: prd status: approved created: 2026-01-01 --- # body `; test("flips approved -> shipped with today's date", () => { const result = flipPrdStatus(baseText, { shippedDate: "2026-05-13" }); const { frontmatter } = parseFrontmatter(result); assert.equal(frontmatter.status, "shipped"); assert.equal(frontmatter.shipped, "2026-05-13"); }); test("flips in-review -> shipped", () => { const text = baseText.replace("status: approved", "status: in-review"); const result = flipPrdStatus(text, { shippedDate: "2026-05-13" }); const { frontmatter } = parseFrontmatter(result); assert.equal(frontmatter.status, "shipped"); }); test("preserves body", () => { const result = flipPrdStatus(baseText, { shippedDate: "2026-05-13" }); assert.match(result, /\n# body\n/); }); test("preserves other frontmatter keys", () => { const result = flipPrdStatus(baseText, { shippedDate: "2026-05-13" }); const { frontmatter } = parseFrontmatter(result); assert.equal(frontmatter.id, "test-prd"); assert.equal(frontmatter.title, "Test PRD"); assert.equal(frontmatter.created, "2026-01-01"); }); test("adds shipping-commits when supplied", () => { const result = flipPrdStatus(baseText, { shippedDate: "2026-05-13", commits: ["abc123", "def456"], }); const { frontmatter } = parseFrontmatter(result); assert.deepEqual(frontmatter["shipping-commits"], ["abc123", "def456"]); }); test("refuses to flip draft (must go through human review)", () => { const text = baseText.replace("status: approved", "status: draft"); assert.throws(() => flipPrdStatus(text), /still draft/); }); test("refuses to flip already-shipped (idempotent fail-soft)", () => { const text = baseText.replace("status: approved", "status: shipped"); assert.throws(() => flipPrdStatus(text), /already marked shipped/); }); test("refuses unexpected statuses", () => { const text = baseText.replace("status: approved", "status: cancelled"); assert.throws(() => flipPrdStatus(text), /Unexpected PRD status/); }); test("refuses files without frontmatter", () => { assert.throws( () => flipPrdStatus("no frontmatter here"), /no parseable frontmatter/, ); }); }); describe("findPrdPath", () => { test("finds a PRD by its id field", () => { const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "prd-find-")); try { const prdsDir = path.join(tmpRoot, "prds"); fs.mkdirSync(prdsDir, { recursive: true }); fs.writeFileSync( path.join(prdsDir, "2026-05-13-something.prd.md"), `--- id: 2026-05-13-something status: approved --- body `, ); const found = findPrdPath(tmpRoot, "2026-05-13-something"); assert.ok(found); assert.match(found, /something\.prd\.md$/); } finally { fs.rmSync(tmpRoot, { recursive: true, force: true }); } }); test("returns null when no PRD matches", () => { const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "prd-find2-")); try { fs.mkdirSync(path.join(tmpRoot, "prds"), { recursive: true }); const found = findPrdPath(tmpRoot, "nonexistent"); assert.equal(found, null); } finally { fs.rmSync(tmpRoot, { recursive: true, force: true }); } }); }); describe("serializeFrontmatter", () => { test("preserves key order from the original", () => { const orig = `id: x title: y status: approved`; const result = serializeFrontmatter(orig, { id: "x", title: "y", status: "shipped", }); const lines = result.split("\n"); assert.equal(lines[0], "id: x"); assert.equal(lines[1], "title: y"); assert.equal(lines[2], "status: shipped"); }); test("appends new keys at the end", () => { const orig = `id: x status: approved`; const result = serializeFrontmatter(orig, { id: "x", status: "shipped", shipped: "2026-05-13", }); assert.match(result, /shipped: 2026-05-13\n?$/); }); test("emits array values as YAML lists", () => { const result = serializeFrontmatter("", { "shipping-commits": ["abc", "def"], }); assert.match(result, /shipping-commits:\n {2}- abc\n {2}- def/); }); });