import { test } from "node:test"; import assert from "node:assert/strict"; import { mkdtempSync, writeFileSync, mkdirSync, rmSync } from "node:fs"; import { join } from "node:path"; import { tmpdir } from "node:os"; import { docLabel, relativeLink, canonicalDocPath, parseMetadata, renderMetadata, renderBacklog, validateLinks, serializeTicketFile, } from "../src/utils.ts"; import type { DocMetadata, Ticket } from "../src/types.ts"; const META_BASE: DocMetadata = { purpose: "p", audience: "Both", sourceOfTruth: "Codebase (implementation is authoritative)", lastUpdated: "2026-01-01", dependsOn: [], referencedBy: [], }; function sampleTicket(overrides: Partial = {}): Ticket { return { id: "001", title: "Foo", status: "backlog", priority: "P2", dependencies: [], context: "", acceptanceCriteria: [], definitionOfDone: [], implementationNotes: [], relatedDocs: [], relatedFiles: [], createdAt: "2026-01-01T00:00:00.000Z", updatedAt: "2026-01-01T00:00:00.000Z", ...overrides, }; } // ── docLabel ─────────────────────────────────────────────────────────── test("docLabel strips directory and .md extension", () => { assert.equal(docLabel("docs/SPEC.md"), "SPEC"); assert.equal(docLabel("AGENTS.md"), "AGENTS"); assert.equal(docLabel("docs/tickets/"), "tickets"); assert.equal(docLabel("T-001-foo.md"), "T-001-foo"); }); // ── relativeLink ─────────────────────────────────────────────────────── test("relativeLink is computed from the source file, not hardcoded", () => { // The canonical examples from the spec: // docs/ARCHITECTURE.md -> docs/SPEC.md must be SPEC.md assert.equal(relativeLink("docs/ARCHITECTURE.md", "docs/SPEC.md"), "SPEC.md"); // README.md -> docs/SPEC.md stays docs/SPEC.md assert.equal(relativeLink("README.md", "docs/SPEC.md"), "docs/SPEC.md"); // Sibling docs inside docs/ never carry a docs/ prefix assert.equal(relativeLink("docs/API.md", "docs/SPEC.md"), "SPEC.md"); assert.equal(relativeLink("docs/SPEC.md", "docs/ARCHITECTURE.md"), "ARCHITECTURE.md"); // Docs leaving their directory climb with .. assert.equal(relativeLink("docs/SPEC.md", "AGENTS.md"), "../AGENTS.md"); // Root docs link to root docs without prefix assert.equal(relativeLink("AGENTS.md", "README.md"), "README.md"); // Directory targets assert.equal(relativeLink("docs/BACKLOG.md", "docs/tickets"), "tickets"); assert.equal(relativeLink("AGENTS.md", "docs/tickets"), "docs/tickets"); }); // ── canonicalDocPath ─────────────────────────────────────────────────── test("canonicalDocPath resolves hrefs relative to their containing file", () => { assert.equal(canonicalDocPath("docs/ARCHITECTURE.md", "SPEC.md"), "docs/SPEC.md"); assert.equal(canonicalDocPath("README.md", "docs/SPEC.md"), "docs/SPEC.md"); assert.equal(canonicalDocPath("docs/SPEC.md", "../AGENTS.md"), "AGENTS.md"); assert.equal( canonicalDocPath("docs/BACKLOG.md", "tickets/T-001.md"), "docs/tickets/T-001.md", ); }); test("relativeLink and canonicalDocPath round-trip", () => { const source = "docs/ARCHITECTURE.md"; const target = "docs/SPEC.md"; const href = relativeLink(source, target); assert.equal(canonicalDocPath(source, href), target); }); // ── parseMetadata / renderMetadata ───────────────────────────────────── test("renderMetadata renders links relative to the source document", () => { const rendered = renderMetadata( { ...META_BASE, dependsOn: ["docs/SPEC.md", "docs/API.md"], referencedBy: ["AGENTS.md", "docs/BACKLOG.md"], }, "docs/ARCHITECTURE.md", ); assert.match(rendered, /\[SPEC\]\(SPEC\.md\)/); assert.match(rendered, /\[API\]\(API\.md\)/); assert.match(rendered, /\[AGENTS\]\(\.\.\/AGENTS\.md\)/); assert.match(rendered, /\[BACKLOG\]\(BACKLOG\.md\)/); }); test("renderMetadata keeps docs/SPEC.md href for root-level documents", () => { const rendered = renderMetadata( { ...META_BASE, dependsOn: ["docs/SPEC.md"] }, "README.md", ); assert.match(rendered, /\[SPEC\]\(docs\/SPEC\.md\)/); }); test("parseMetadata canonicalizes hrefs and round-trips through renderMetadata", () => { const source = "docs/ARCHITECTURE.md"; const rendered = renderMetadata( { ...META_BASE, dependsOn: ["docs/SPEC.md"], referencedBy: ["AGENTS.md"] }, source, ); const parsed = parseMetadata(rendered, source); assert.deepEqual(parsed.dependsOn, ["docs/SPEC.md"]); assert.deepEqual(parsed.referencedBy, ["AGENTS.md"]); // Re-rendering the parsed metadata from a different file still resolves const fromRoot = renderMetadata(parsed, "README.md"); assert.match(fromRoot, /\[SPEC\]\(docs\/SPEC\.md\)/); assert.match(fromRoot, /\[AGENTS\]\(AGENTS\.md\)/); }); // ── renderBacklog ────────────────────────────────────────────────────── test("renderBacklog links everything relative to docs/BACKLOG.md", () => { const backlog = renderBacklog([ sampleTicket({ title: "Add dark mode" }), ]); assert.match(backlog, /\[ROADMAP\]\(ROADMAP\.md\)/); assert.match(backlog, /\[AGENTS\]\(\.\.\/AGENTS\.md\)/); assert.match(backlog, /\[`tickets\/`\]\(tickets\/\)/); assert.match(backlog, /\[T-001-add-dark-mode\.md\]\(tickets\/T-001-add-dark-mode\.md\)/); // No root-relative docs/ hrefs may leak into docs/BACKLOG.md assert.doesNotMatch(backlog, /\]\(docs\//); }); // ── serializeTicketFile ──────────────────────────────────────────────── test("serializeTicketFile renders relatedDocs relative to the ticket file", () => { const ticket = sampleTicket({ relatedDocs: ["docs/API.md", "docs/DESIGN.md"] }); const out = serializeTicketFile(ticket); assert.match(out, /\[API\]\(\.\.\/API\.md\)/); assert.match(out, /\[DESIGN\]\(\.\.\/DESIGN\.md\)/); }); test("serializeTicketFile preserves checkbox state and inline verification comments", () => { const ticket = sampleTicket({ acceptanceCriteria: [ { text: "pnpm dev serves a page at localhost:3000", checked: true, comment: "VERIFIED: served HTTP 200", }, { text: "pnpm typecheck passes", checked: false, comment: "" }, ], definitionOfDone: [{ text: "Test written first", checked: true, comment: "" }], }); const out = serializeTicketFile(ticket); // Checked boxes render as [x], comments stay inline on the same line. assert.match(out, /- \[x\] pnpm dev serves a page at localhost:3000 <\!-- VERIFIED: served HTTP 200 -->/); assert.match(out, /- \[ \] pnpm typecheck passes/); assert.match(out, /- \[x\] Test written first/); // No comment is stranded on its own line / as its own checkbox. assert.doesNotMatch(out, /- \[ \]