/** * docgraph_ticket_create tool: registration, initialization guard, ticket file * creation with the mandatory TDD Definition of Done, ID sequencing, backlog * sync, and relative link rendering for related docs. */ import { test } from "node:test"; import assert from "node:assert/strict"; import { Text } from "@earendil-works/pi-tui"; import { registerDocgraphTicketCreate } from "../src/tools/ticket-create.ts"; import { readTicket, listTickets } from "../src/utils.ts"; import { captureTool, makeTempRepo, cleanupTemp, readRepoFile, makeCtx, runTool, toolText, componentText, renderCall, renderResult, ISO_TIMESTAMP_RE, } from "./helpers.ts"; function tool() { return captureTool(registerDocgraphTicketCreate); } test("docgraph_ticket_create registers a typed schema with priority enum", () => { const t = tool(); assert.equal(t.name, "docgraph_ticket_create"); assert.equal(t.label, "Docgraph Ticket Create"); const parameters = t.parameters as unknown as { properties: Record; }; assert.ok(parameters.properties.title); assert.deepEqual(parameters.properties.priority!.enum, ["P0", "P1", "P2", "P3"]); assert.ok(parameters.properties.acceptanceCriteria, "acceptanceCriteria expected"); }); test("docgraph_ticket_create refuses to run before initialization", async () => { const cwd = makeTempRepo(); try { const { ctx } = makeCtx({ cwd }); const res = await runTool(tool(), { title: "X", priority: "P2" }, ctx); assert.match(toolText(res), /not initialized/i); assert.equal((res.details as { error: string }).error, "not_initialized"); } finally { cleanupTemp(cwd); } }); test("docgraph_ticket_create writes a ticket file with the mandatory TDD checklist", async () => { const cwd = makeTempRepo(); try { const { ctx } = makeCtx({ cwd, state: { initialized: true, schemaVersion: 1 } }); const res = await runTool(tool(), { title: "Add dark mode", priority: "P2" }, ctx); const text = toolText(res); assert.match(text, /Created ticket T-001: \*\*Add dark mode\*\* \(P2\)/); assert.match(text, /docs\/tickets\/T-001-add-dark-mode\.md/); const details = res.details as { ticket: { id: string; status: string; definitionOfDone: Array<{ text: string; checked: boolean; comment: string }>; createdAt: string; updatedAt: string; }; success: boolean; backlogSynced: boolean; }; assert.equal(details.success, true); assert.equal(details.backlogSynced, true); assert.equal(details.ticket.id, "001"); assert.equal(details.ticket.status, "backlog"); assert.equal(details.ticket.definitionOfDone.length, 5); assert.match(details.ticket.definitionOfDone[0]!.text, /Test written first/); assert.match(details.ticket.definitionOfDone[4]!.text, /no regressions/); assert.ok(details.ticket.definitionOfDone.every((i) => !i.checked)); assert.ok(details.ticket.definitionOfDone.every((i) => i.comment === "")); assert.match(details.ticket.createdAt, ISO_TIMESTAMP_RE); assert.equal(details.ticket.createdAt, details.ticket.updatedAt); // The file on disk re-parses to the same ticket. const file = readRepoFile(cwd, "docs/tickets/T-001-add-dark-mode.md")!; assert.match(file, /\*\*ID:\*\* 001/); assert.match(file, /## Definition of Done/); assert.match(file, /- \[ \] Test written first/); assert.equal(readTicket("001", cwd)?.title, "Add dark mode"); // BACKLOG.md was regenerated to index the ticket. const backlog = readRepoFile(cwd, "docs/BACKLOG.md")!; assert.match(backlog, /T-001-add-dark-mode\.md/); } finally { cleanupTemp(cwd); } }); test("docgraph_ticket_create appends custom Definition of Done items", async () => { const cwd = makeTempRepo(); try { const { ctx } = makeCtx({ cwd, state: { initialized: true, schemaVersion: 1 } }); const res = await runTool( tool(), { title: "Ship staging", priority: "P1", definitionOfDone: ["Deploy to staging and smoke test"], }, ctx, ); const details = res.details as { ticket: { definitionOfDone: Array<{ text: string; checked: boolean; comment: string }> }; }; assert.equal(details.ticket.definitionOfDone.length, 6); assert.ok( details.ticket.definitionOfDone.some((i) => i.text === "Deploy to staging and smoke test"), ); } finally { cleanupTemp(cwd); } }); test("docgraph_ticket_create allocates sequential ids", async () => { const cwd = makeTempRepo(); try { const t = tool(); const { ctx } = makeCtx({ cwd, state: { initialized: true, schemaVersion: 1 } }); const first = await runTool(t, { title: "One", priority: "P3" }, ctx); const second = await runTool(t, { title: "Two", priority: "P3" }, ctx); assert.equal((first.details as { ticket: { id: string } }).ticket.id, "001"); assert.equal((second.details as { ticket: { id: string } }).ticket.id, "002"); assert.equal(listTickets(cwd).length, 2); } finally { cleanupTemp(cwd); } }); test("docgraph_ticket_create persists full ticket payloads including related docs as relative links", async () => { const cwd = makeTempRepo(); try { const { ctx } = makeCtx({ cwd, state: { initialized: true, schemaVersion: 1 } }); await runTool( tool(), { title: "Auth wiring", priority: "P0", estimate: "1d", dependencies: ["002"], context: "Needs the session store.", acceptanceCriteria: ["Login succeeds", "Logout clears tokens"], relatedDocs: ["docs/API.md", "docs/DESIGN.md"], relatedFiles: ["src/auth.ts", "src/session.ts"], }, ctx, ); const file = readRepoFile(cwd, "docs/tickets/T-001-auth-wiring.md")!; // Related docs render relative to docs/tickets/ → climb one level. assert.match(file, /\[API\]\(\.\.\/API\.md\)/); assert.match(file, /\[DESIGN\]\(\.\.\/DESIGN\.md\)/); assert.match(file, /- `src\/auth\.ts`/); assert.match(file, /\*\*Dependencies:\*\* 002/); assert.match(file, /\*\*Estimate:\*\* 1d/); assert.match(file, /- \[ \] Login succeeds/); const loaded = readTicket("001", cwd)!; assert.equal(loaded.context, "Needs the session store."); assert.deepEqual(loaded.relatedFiles, ["src/auth.ts", "src/session.ts"]); } finally { cleanupTemp(cwd); } }); test("docgraph_ticket_create falls back to the plain filename for un-slugifiable titles", async () => { const cwd = makeTempRepo(); try { const { ctx } = makeCtx({ cwd, state: { initialized: true, schemaVersion: 1 } }); const res = await runTool(tool(), { title: "!!!", priority: "P2" }, ctx); const text = toolText(res); assert.match(text, /docs\/tickets\/T-001\.md/); assert.ok(readRepoFile(cwd, "docs/tickets/T-001.md")); } finally { cleanupTemp(cwd); } }); test("docgraph_ticket_create renderers show the title", () => { const t = tool(); const call = componentText(renderCall(t, { title: "Add dark mode", priority: "P2" })); assert.match(call, /docgraph-ticket-create/); assert.match(call, /Add dark mode/); const ok = { content: [{ type: "text", text: "Created ticket T-001: **X** (P2)" }] }; assert.match(componentText(renderResult(t, ok as never)), /Created ticket/); const fail = { content: [{ type: "text", text: "Failed to create ticket T-001" }] }; assert.match(componentText(renderResult(t, fail as never)), /Failed/); assert.ok(renderCall(t, { title: "x", priority: "P2" }) instanceof Text); });