/** * docgraph_init tool: registration metadata, first-run scaffolding, idempotent * re-runs, --force body preservation, project-name override, backlog * regeneration, and hostile-filesystem failure behavior. */ import { test } from "node:test"; import assert from "node:assert/strict"; import { existsSync } from "node:fs"; import { join } from "node:path"; import { Text } from "@earendil-works/pi-tui"; import { registerDocgraphInit } from "../src/tools/doc-init.ts"; import { SCAFFOLDS } from "../src/tools/doc-init.ts"; import { writeTicket, readFileSafe } from "../src/utils.ts"; import { captureTool, makeTempRepo, cleanupTemp, writeFiles, readRepoFile, makeCtx, runTool, toolText, componentText, renderCall, renderResult, sampleTicket, } from "./helpers.ts"; function tool() { return captureTool(registerDocgraphInit); } test("docgraph_init registers descriptive metadata and a typed schema", () => { const t = tool(); assert.equal(t.name, "docgraph_init"); assert.equal(t.label, "Docgraph Init"); assert.match(t.description, /scaffold/i); assert.equal(typeof t.execute, "function"); assert.equal(typeof t.renderCall, "function"); assert.equal(typeof t.renderResult, "function"); const props = (t.parameters as unknown as { properties: Record }).properties; assert.ok(props.force, "force parameter expected"); assert.ok(props.projectName, "projectName parameter expected"); }); test("docgraph_init scaffolds every canonical document on first run", async () => { const cwd = makeTempRepo(); try { const { ctx, statusCalls } = makeCtx({ cwd }); const res = await runTool(tool(), {}, ctx); assert.match(toolText(res), /^Created:/); const created = (res.details as { created: string[] }).created; assert.equal(created.length, SCAFFOLDS.length); assert.deepEqual([...created].sort(), SCAFFOLDS.map((s) => s.path).sort()); // Every expected file exists with a title, metadata block, and dated stamp. for (const sc of SCAFFOLDS) { const content = readRepoFile(cwd, sc.path); assert.ok(content, `expected ${sc.path} to exist`); assert.match(content!, /^# /, `${sc.path} should start with a title`); assert.match(content!, /> \*\*Purpose:\*\*/, `${sc.path} should have a metadata block`); assert.match( content!, /> \*\*Last Updated:\*\* (\d{4}-\d{2}-\d{2})/, `${sc.path} should carry a dated Last Updated stamp`, ); } assert.ok(existsSync(join(cwd, "docs/tickets")), "docs/tickets dir expected"); assert.equal((res.details as { skipped: string[] }).skipped.length, 0); assert.deepEqual((res.details as { state: unknown }).state, { initialized: true, schemaVersion: 1, }); // The UI status reflects initialization. assert.deepEqual(statusCalls, [{ key: "docgraph", text: "Docgraph: initialized" }]); } finally { cleanupTemp(cwd); } }); test("docgraph_init writes the scaffolding markers isInitialized relies on", async () => { const cwd = makeTempRepo(); try { const { ctx } = makeCtx({ cwd }); await runTool(tool(), {}, ctx); const agents = readRepoFile(cwd, "AGENTS.md")!; assert.match(agents, /## Documentation Index/); assert.ok(readRepoFile(cwd, "README.md")); } finally { cleanupTemp(cwd); } }); test("docgraph_init is idempotent: re-running skips existing files", async () => { const cwd = makeTempRepo(); try { const t = tool(); const { ctx } = makeCtx({ cwd }); await runTool(t, {}, ctx); const again = await runTool(t, {}, ctx); assert.equal((again.details as { created: string[] }).created.length, 0); assert.equal((again.details as { skipped: string[] }).skipped.length, SCAFFOLDS.length); assert.match(toolText(again), /Skipped \(already exist\)/); } finally { cleanupTemp(cwd); } }); test("docgraph_init --force preserves existing bodies while refreshing metadata", async () => { const cwd = makeTempRepo(); try { writeFiles(cwd, { "README.md": "# Existing\n\n## Custom Section\n\nHand-written content\n", }); const t = tool(); const { ctx } = makeCtx({ cwd }); const res = await runTool(t, { force: true }, ctx); const created = (res.details as { created: string[] }).created; assert.ok(created.includes("README.md"), "README.md should be re-created under force"); const readme = readRepoFile(cwd, "README.md")!; assert.match(readme, /## Custom Section/, "existing body should be preserved"); assert.match(readme, /Hand-written content/); assert.match(readme, /> \*\*Last Updated:\*\* \d{4}-\d{2}-\d{2}/); } finally { cleanupTemp(cwd); } }); test("docgraph_init projectName overrides the README title only", async () => { const cwd = makeTempRepo(); try { const { ctx } = makeCtx({ cwd }); await runTool(tool(), { projectName: "My Awesome App" }, ctx); assert.match(readRepoFile(cwd, "README.md")!, /^# My Awesome App\b/); // Other scaffolds keep their default titles. assert.match(readRepoFile(cwd, "docs/SPEC.md")!, /^# Specification/); } finally { cleanupTemp(cwd); } }); test("docgraph_init regenerates docs/BACKLOG.md from existing tickets", async () => { const cwd = makeTempRepo(); try { writeTicket(sampleTicket({ title: "Already planned" }), cwd); const { ctx } = makeCtx({ cwd }); await runTool(tool(), {}, ctx); const backlog = readRepoFile(cwd, "docs/BACKLOG.md")!; assert.match(backlog, /T-001-already-planned\.md/); } finally { cleanupTemp(cwd); } }); test("docgraph_init registration and rendering produce human-readable output", async () => { const t = tool(); const call = componentText(renderCall(t, { force: false, projectName: undefined })); assert.match(call, /docgraph-init/); const okResult = { content: [{ type: "text", text: "Created: README.md" }], details: { action: "init", state: { initialized: true, schemaVersion: 1 }, created: ["README.md", "docs/SPEC.md"], skipped: [], errors: [], }, }; const okRender = componentText(renderResult(t, okResult as never)); assert.match(okRender, /Created 2 file\(s\)/); assert.match(okRender, /README\.md/); const errResult = { content: [{ type: "text", text: "Created: README.md. Errors: Failed to write X" }], details: { action: "init", state: { initialized: true, schemaVersion: 1 }, created: ["README.md"], skipped: [], errors: ["Failed to write X"], }, }; const errRender = componentText(renderResult(t, errResult as never)); assert.match(errRender, /Errors: Failed to write X/); const emptyResult = { content: [{ type: "text", text: "fallback" }], details: undefined }; const fallback = componentText(renderResult(t, emptyResult as never)); assert.match(fallback, /fallback/); }); test("docgraph_init returns Text components from its renderers", () => { const t = tool(); assert.ok(renderCall(t, {}) instanceof Text); assert.ok( renderResult(t, { content: [{ type: "text", text: "x" }], details: { action: "init" }, } as never) instanceof Text, ); }); test("docgraph_init fails loudly when the filesystem is hostile", async () => { const cwd = makeTempRepo(); try { // A regular file named "docs" blocks directory creation (ENOTDIR). writeFiles(cwd, { docs: "not a directory" }); const { ctx } = makeCtx({ cwd }); await assert.rejects(() => runTool(tool(), {}, ctx)); } finally { cleanupTemp(cwd); } }); test("docgraph_init never reports files it could not write as created", async () => { const cwd = makeTempRepo(); try { // Root-level scaffolds succeed even when docs/ is unusable... but the // ticket dir fails first, so init rejects. This pins the overall contract: // a failed run must not silently half-initialize. writeFiles(cwd, { docs: "not a directory" }); const { ctx } = makeCtx({ cwd }); await assert.rejects(() => runTool(tool(), {}, ctx)); // Nothing was written before the failure. assert.equal(readFileSafe("docs/SPEC.md", cwd), null); } finally { cleanupTemp(cwd); } });