import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import assert from "node:assert/strict"; import { execFileSync } from "node:child_process"; import { afterEach, describe, it } from "vitest"; import { checkDocsLinks, formatDocsLinkCheckResult } from "./docs-link-check"; const fixtures: string[] = []; function createFixture(files: Record): string { const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "docs-link-check-")); fixtures.push(rootDir); for (const [relativePath, content] of Object.entries(files)) { const absolutePath = path.join(rootDir, relativePath); fs.mkdirSync(path.dirname(absolutePath), { recursive: true }); fs.writeFileSync(absolutePath, content); } return rootDir; } afterEach(() => { while (fixtures.length > 0) { fs.rmSync(fixtures.pop() as string, { recursive: true, force: true }); } }); describe("docs-link-check", () => { it("passes for valid local Markdown links and anchors", () => { const rootDir = createFixture({ "README.md": "# Root\nSee [plugins](docs/plugins.md#plugin-boundaries).", "docs/plugins.md": "# Plugin Boundaries\nSee [feature inventory](feature-inventory.md).", "docs/feature-inventory.md": "# Feature Inventory\n", }); const result = checkDocsLinks(rootDir, ["README.md", "docs"]); const output = formatDocsLinkCheckResult(result); assert.deepEqual(result.failures, []); assert.match(output, /^PASS docs link check/); }); it("fails with file context for missing local paths", () => { const rootDir = createFixture({ "README.md": "# Root\nSee [missing](docs/missing.md).", }); const result = checkDocsLinks(rootDir, ["README.md", "docs"]); assert.match(result.failures.join("\n"), /README\.md:\d+:\d+: broken local link "docs\/missing\.md"/); }); it("fails with file context for missing local anchors", () => { const rootDir = createFixture({ "README.md": "# Root\nSee [plugins](docs/plugins.md#missing-anchor).", "docs/plugins.md": "# Plugin Boundaries\n", }); const result = checkDocsLinks(rootDir, ["README.md", "docs"]); assert.match(result.failures.join("\n"), /README\.md:\d+:\d+: broken local anchor "#missing-anchor"/); }); it("ignores external URLs and special link schemes", () => { const rootDir = createFixture({ "README.md": [ "# Root", "See [external](https://example.com/missing#anchor).", "Send [mail](mailto:help@example.com) or [call](tel:+15555550100).", "Jump to [local](#root).", ].join("\n"), }); const result = checkDocsLinks(rootDir, ["README.md"]); assert.deepEqual(result.failures, []); }); it("fails closed when a configured docs root is missing", () => { const rootDir = createFixture({ "README.md": "# Root\n", }); const result = checkDocsLinks(rootDir, ["README.md", "docs"]); assert.match(result.failures.join("\n"), /docs-roots: missing configured docs root docs/); }); it("fails closed when no Markdown files are checked", () => { const rootDir = createFixture({ "src/index.ts": "export {};", }); const result = checkDocsLinks(rootDir, ["docs"]); assert.match(result.failures.join("\n"), /docs-roots: checked 0 Markdown files/); }); it("exits non-zero from the CLI when local links are broken", () => { const rootDir = createFixture({ "README.md": "# Root\nSee [missing](docs/missing.md).", "AGENTS.md": "# Agents\n", "SECURITY.md": "# Security\n", "CHANGELOG.md": "# Changelog\n", "docs/index.md": "# Docs\n", "guides/index.md": "# Guides\n", "llm-docs/index.md": "# LLM Docs\n", }); let output = ""; assert.throws(() => { try { execFileSync("pnpm", ["exec", "tsx", path.resolve("scripts/docs-link-check.ts"), rootDir], { encoding: "utf8", stdio: "pipe" }); } catch (error) { if (error && typeof error === "object") { const failure = error as { stderr?: Buffer | string; stdout?: Buffer | string }; output = `${String(failure.stderr ?? "")}\n${String(failure.stdout ?? "")}`; } throw error; } }); assert.match(output, /broken local link "docs\/missing\.md"/); }); });