/** * docgraph_read tool: registration, initialization guard, missing-file handling, * metadata/body extraction, and long-body truncation. */ import { test } from "node:test"; import assert from "node:assert/strict"; import { writeFileSync, mkdirSync } from "node:fs"; import { join } from "node:path"; import { Text } from "@earendil-works/pi-tui"; import { registerDocgraphRead } from "../src/tools/doc-read.ts"; import { captureTool, makeTempRepo, cleanupTemp, writeFiles, makeCtx, runTool, toolText, componentText, renderCall, renderResult, } from "./helpers.ts"; function tool() { return captureTool(registerDocgraphRead); } const SPEC = [ "> **Purpose:** Functional specification", ">", "> **Audience:** Both", ">", "> **Source of Truth:** Codebase (implementation is authoritative)", ">", "> **Last Updated:** 2026-01-01", ">", "> **Depends On:** [AGENTS](../AGENTS.md)", ">", "> **Referenced By:** [ARCHITECTURE](ARCHITECTURE.md)", "", "## Product Overview", "", "The system does things.", "## Goals", "", "Be excellent.", ].join("\n"); test("docgraph_read registers descriptive metadata and requires a path", () => { const t = tool(); assert.equal(t.name, "docgraph_read"); assert.equal(t.label, "Docgraph Read"); assert.match(t.description, /metadata/); const props = (t.parameters as unknown as { properties: Record }).properties; assert.ok(props.path, "path parameter required"); }); test("docgraph_read refuses to run before initialization", async () => { const cwd = makeTempRepo(); try { const { ctx } = makeCtx({ cwd }); const res = await runTool(tool(), { path: "docs/SPEC.md" }, ctx); assert.match(toolText(res), /Documentation not initialized\. Run `docgraph_init` first\./); assert.equal((res.details as { error: string }).error, "not_initialized"); } finally { cleanupTemp(cwd); } }); test("docgraph_read reports a missing document with not_found details", async () => { const cwd = makeTempRepo(); try { const { ctx } = makeCtx({ cwd, state: { initialized: true, schemaVersion: 1 } }); const res = await runTool(tool(), { path: "docs/NOPE.md" }, ctx); assert.match(toolText(res), /Document not found: docs\/NOPE\.md/); const details = res.details as { error: string; path: string }; assert.equal(details.error, "not_found"); assert.equal(details.path, "docs/NOPE.md"); } finally { cleanupTemp(cwd); } }); test("docgraph_read parses metadata and returns the body minus the metadata block", async () => { const cwd = makeTempRepo(); try { mkdirSync(join(cwd, "docs"), { recursive: true }); writeFileSync(join(cwd, "docs/SPEC.md"), SPEC, "utf-8"); // Satisfy the initialization guard via filesystem markers. writeFiles(cwd, { "AGENTS.md": "## Documentation Index", "README.md": "# Project", }); const { ctx } = makeCtx({ cwd }); const res = await runTool(tool(), { path: "docs/SPEC.md" }, ctx); const text = toolText(res); assert.match(text, /## Metadata for `docs\/SPEC\.md`/); assert.match(text, /- \*\*Purpose:\*\* Functional specification/); assert.match(text, /- \*\*Depends On:\*\* AGENTS\.md/); assert.match(text, /- \*\*Referenced By:\*\* docs\/ARCHITECTURE\.md/); assert.match(text, /## Body/); assert.match(text, /## Product Overview/); assert.match(text, /The system does things\./); assert.doesNotMatch(text, /> \*\*Purpose:\*\*/, "raw block should not leak into output"); const details = res.details as { path: string; metadata: { purpose: string; audience: string; lastUpdated: string }; bodyLength: number; }; assert.equal(details.path, "docs/SPEC.md"); assert.equal(details.metadata.purpose, "Functional specification"); assert.equal(details.metadata.audience, "Both"); assert.equal(details.metadata.lastUpdated, "2026-01-01"); assert.ok(details.bodyLength > 0, "bodyLength should be reported"); } finally { cleanupTemp(cwd); } }); test("docgraph_read truncates bodies longer than 4000 characters", async () => { const cwd = makeTempRepo(); try { mkdirSync(join(cwd, "docs"), { recursive: true }); const bigBody = "lorem ipsum ".repeat(600); // ~7200 chars writeFileSync( join(cwd, "docs/SPEC.md"), `> **Purpose:** p\n\n## Body\n\n${bigBody}`, "utf-8", ); writeFiles(cwd, { "AGENTS.md": "## Documentation Index", "README.md": "# Project", }); const { ctx } = makeCtx({ cwd }); const res = await runTool(tool(), { path: "docs/SPEC.md" }, ctx); const text = toolText(res); assert.match(text, /\.\.\. \(truncated\)/); assert.ok((res.details as { bodyLength: number }).bodyLength > 4000); } finally { cleanupTemp(cwd); } }); test("docgraph_read renderers include the path and first line", () => { const t = tool(); const call = componentText(renderCall(t, { path: "docs/SPEC.md" })); assert.match(call, /docgraph-read/); assert.match(call, /docs\/SPEC\.md/); const result = { content: [{ type: "text", text: "## Metadata for `docs/SPEC.md`\n- **Purpose:** p" }], }; const rendered = componentText(renderResult(t, result as never)); assert.match(rendered, /## Metadata for `docs\/SPEC\.md`/); assert.ok(renderCall(t, { path: "x.md" }) instanceof Text); });