/** * The derived backlog index: `renderBacklog` output shape/ordering/link * resolution and `syncBacklog` filesystem regeneration. */ import { test } from "node:test"; import assert from "node:assert/strict"; import { renderBacklog, syncBacklog, listTickets, KANBAN_ORDER, writeTicket, } from "../src/utils.ts"; import { makeTempRepo, cleanupTemp, writeFiles, readRepoFile, sampleTicket, } from "./helpers.ts"; // ── KANBAN_ORDER ────────────────────────────────────────────────────── test("KANBAN_ORDER defines the canonical column sequence", () => { assert.deepEqual(KANBAN_ORDER, [ "backlog", "ready", "in-progress", "review", "done", "blocked", ]); }); // ── renderBacklog ───────────────────────────────────────────────────── test("renderBacklog renders the empty state across every column", () => { const backlog = renderBacklog([]); assert.match(backlog, /_\(No tickets yet — ask the agent to create one\.\)_/); for (const _col of KANBAN_ORDER) { assert.match(backlog, /_\(No tickets in this column\.\)_/); } }); test("renderBacklog lists columns in canonical order with Title-cased headers", () => { const tickets = [sampleTicket({ title: "A" }), sampleTicket({ id: "002", title: "B" })]; const backlog = renderBacklog(tickets); const headers = backlog .split("\n") .filter((l) => /^## (Backlog|Ready|In Progress|Review|Done|Blocked)$/.test(l)); assert.deepEqual(headers, [ "## Backlog", "## Ready", "## In Progress", "## Review", "## Done", "## Blocked", ]); }); test("renderBacklog groups tickets under their status column", () => { const tickets = [ sampleTicket({ id: "001", title: "Do the thing", status: "in-progress" }), sampleTicket({ id: "002", title: "Later", status: "backlog" }), sampleTicket({ id: "003", title: "Shipped", status: "done" }), ]; const backlog = renderBacklog(tickets); // Index table lists every ticket. assert.match(backlog, /T-001-do-the-thing\.md/); assert.match(backlog, /T-002-later\.md/); assert.match(backlog, /T-003-shipped\.md/); const inProgressSection = backlog.split("## In Progress")[1]!.split("## Review")[0]!; assert.match(inProgressSection, /T-001-do-the-thing\.md/); assert.doesNotMatch(inProgressSection, /T-002-later\.md/); const backlogSection = backlog.split("## Backlog")[1]!.split("## Ready")[0]!; assert.match(backlogSection, /T-002-later\.md/); const doneSection = backlog.split("## Done")[1]!.split("## Blocked")[0]!; assert.match(doneSection, /T-003-shipped\.md/); }); test("renderBacklog computes every href relative to docs/BACKLOG.md", () => { const backlog = renderBacklog([ sampleTicket({ title: "Add dark mode" }), ]); // Sibling docs inside docs/ never carry a docs/ prefix. assert.match(backlog, /\[ROADMAP\]\(ROADMAP\.md\)/); // Repo-root docs climb one level. assert.match(backlog, /\[AGENTS\]\(\.\.\/AGENTS\.md\)/); // The tickets directory and files resolve relative to docs/. assert.match(backlog, /\[`tickets\/`\]\(tickets\/\)/); assert.match(backlog, /\[T-001-add-dark-mode\.md\]\(tickets\/T-001-add-dark-mode\.md\)/); assert.doesNotMatch(backlog, /\]\(docs\//); }); test("renderBacklog escapes pipe characters in titles", () => { const backlog = renderBacklog([sampleTicket({ title: "A | B" })]); assert.match(backlog, /A \\\| B/); }); test("renderBacklog stamps a fresh Last Updated date", () => { const backlog = renderBacklog([]); const today = new Date().toISOString().slice(0, 10); assert.match(backlog, new RegExp(`> \\*\\*Last Updated:\\*\\* ${today}`)); }); // ── syncBacklog ─────────────────────────────────────────────────────── test("syncBacklog writes a backlog file deriving from the ticket directory", () => { const cwd = makeTempRepo(); try { writeTicket(sampleTicket({ title: "Wire up API" }), cwd); assert.equal(syncBacklog(cwd), true); const file = readRepoFile(cwd, "docs/BACKLOG.md"); assert.ok(file); assert.match(file!, /\[T-001-wire-up-api\.md\]\(tickets\/T-001-wire-up-api\.md\)/); } finally { cleanupTemp(cwd); } }); test("syncBacklog replaces any stale backlog content when tickets change", () => { const cwd = makeTempRepo(); try { writeFiles(cwd, { "docs/BACKLOG.md": "# Backlog\n\nStale content", }); writeTicket(sampleTicket({ title: "Fresh ticket" }), cwd); assert.equal(syncBacklog(cwd), true); const file = readRepoFile(cwd, "docs/BACKLOG.md"); assert.ok(file); assert.doesNotMatch(file!, /Stale content/); assert.match(file!, /T-001-fresh-ticket\.md/); } finally { cleanupTemp(cwd); } }); test("syncBacklog parses back what it wrote (round trip)", () => { const cwd = makeTempRepo(); try { writeTicket(sampleTicket({ id: "001", title: "One" }), cwd); writeTicket(sampleTicket({ id: "002", title: "Two", status: "review" }), cwd); assert.equal(syncBacklog(cwd), true); const tickets = listTickets(cwd); assert.equal(tickets.length, 2); assert.deepEqual( tickets.map((t) => [t.id, t.status]).sort(), [["001", "backlog"], ["002", "review"]], ); } finally { cleanupTemp(cwd); } });