128 lines
4.2 KiB
TypeScript
128 lines
4.2 KiB
TypeScript
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, string>): 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"/);
|
|
});
|
|
});
|