/** * Slash commands (/docgraph:init, /docgraph:sync, /docgraph:tickets, /docgraph:graph): * registration, initialization gating, and notification behavior for both * initialized and uninitialized repositories. */ import { test } from "node:test"; import assert from "node:assert/strict"; import { registerAllCommands } from "../src/commands/index.ts"; import { writeTicket } from "../src/utils.ts"; import { mockPi, makeTempRepo, cleanupTemp, makeCtx, sampleTicket } from "./helpers.ts"; const COMMAND_NAMES = ["docgraph:tickets", "docgraph:init", "docgraph:sync", "docgraph:graph"]; function registeredCommands() { const { pi, commands } = mockPi(); registerAllCommands(pi); return commands; } function findCommand(commands: ReturnType, name: string) { const cmd = commands.find((c) => c.name === name); assert.ok(cmd, `command ${name} should be registered`); return cmd!; } test("registerAllCommands registers all four slash commands with descriptions", () => { const commands = registeredCommands(); assert.deepEqual( commands.map((c) => c.name).sort(), [...COMMAND_NAMES].sort(), ); for (const cmd of commands) { assert.ok(cmd.description && cmd.description.length > 0, `${cmd.name} needs a description`); assert.equal(typeof cmd.handler, "function", `${cmd.name} needs a handler`); } }); test("/docgraph:tickets warns when docgraph is not initialized", async () => { const cwd = makeTempRepo(); try { const { ctx, notifyCalls } = makeCtx({ cwd }); await findCommand(registeredCommands(), "docgraph:tickets").handler({}, ctx); assert.equal(notifyCalls.length, 1); assert.equal(notifyCalls[0]!.type, "warning"); assert.match(notifyCalls[0]!.message, /not initialized/i); } finally { cleanupTemp(cwd); } }); test("/docgraph:tickets announces an empty board", async () => { const cwd = makeTempRepo(); try { const { ctx, notifyCalls } = makeCtx({ cwd, state: { initialized: true, schemaVersion: 1 } }); await findCommand(registeredCommands(), "docgraph:tickets").handler({}, ctx); assert.equal(notifyCalls.length, 1); assert.equal(notifyCalls[0]!.type, "info"); assert.match(notifyCalls[0]!.message, /No tickets yet\. Ask the agent to create one!/); } finally { cleanupTemp(cwd); } }); test("/docgraph:tickets renders a grouped board when tickets exist", async () => { const cwd = makeTempRepo(); try { writeTicket(sampleTicket({ id: "001", title: "Dark mode", status: "in-progress" }), cwd); writeTicket(sampleTicket({ id: "002", title: "Housekeeping", priority: "P3" }), cwd); const { ctx, notifyCalls } = makeCtx({ cwd, state: { initialized: true, schemaVersion: 1 } }); await findCommand(registeredCommands(), "docgraph:tickets").handler({}, ctx); assert.equal(notifyCalls.length, 1); const message = notifyCalls[0]!.message; assert.match(message, /--- Ticket Board \(2 total\) ---/); assert.match(message, /\[IN-PROGRESS\]/); assert.match(message, /T-001 P2 Dark mode/); assert.match(message, /\[BACKLOG\]/); assert.match(message, /T-002 P3 Housekeeping/); } finally { cleanupTemp(cwd); } }); test("/docgraph:init announces initialization state", async () => { const notInit = makeTempRepo(); try { const { ctx, notifyCalls } = makeCtx({ cwd: notInit }); await findCommand(registeredCommands(), "docgraph:init").handler({}, ctx); assert.equal(notifyCalls[0]!.type, "warning"); assert.match(notifyCalls[0]!.message, /Docgraph not initialized\. Ask the agent to run the docgraph_init tool\./); } finally { cleanupTemp(notInit); } const init = makeTempRepo(); try { const { ctx, notifyCalls } = makeCtx({ cwd: init, state: { initialized: true, schemaVersion: 1 } }); await findCommand(registeredCommands(), "docgraph:init").handler({}, ctx); assert.equal(notifyCalls[0]!.type, "info"); assert.match(notifyCalls[0]!.message, /Docgraph already initialized\. Use \/docgraph:sync/); } finally { cleanupTemp(init); } }); test("/docgraph:sync warns when uninitialized and delegates when initialized", async () => { const notInit = makeTempRepo(); try { const { ctx, notifyCalls } = makeCtx({ cwd: notInit }); await findCommand(registeredCommands(), "docgraph:sync").handler({}, ctx); assert.equal(notifyCalls[0]!.type, "warning"); } finally { cleanupTemp(notInit); } const init = makeTempRepo(); try { const { ctx, notifyCalls } = makeCtx({ cwd: init, state: { initialized: true, schemaVersion: 1 } }); await findCommand(registeredCommands(), "docgraph:sync").handler({}, ctx); assert.equal(notifyCalls[0]!.type, "info"); assert.match(notifyCalls[0]!.message, /Ask the agent to run docgraph_sync/); } finally { cleanupTemp(init); } }); test("/docgraph:graph is gated on initialization and renders the doc tree", async () => { const notInit = makeTempRepo(); try { const { ctx, notifyCalls } = makeCtx({ cwd: notInit }); await findCommand(registeredCommands(), "docgraph:graph").handler({}, ctx); assert.equal(notifyCalls[0]!.type, "warning"); assert.match(notifyCalls[0]!.message, /not initialized/i); } finally { cleanupTemp(notInit); } const init = makeTempRepo(); try { const { ctx, notifyCalls } = makeCtx({ cwd: init, state: { initialized: true, schemaVersion: 1 } }); await findCommand(registeredCommands(), "docgraph:graph").handler({}, ctx); assert.equal(notifyCalls[0]!.type, "info"); const message = notifyCalls[0]!.message; assert.match(message, /Documentation Graph:/); assert.match(message, /README\.md ─── Human entry point/); assert.match(message, /docs\/tickets\/ ─── Individual tickets/); } finally { cleanupTemp(init); } });