195 lines
8.4 KiB
TypeScript
195 lines
8.4 KiB
TypeScript
import { test } from "node:test";
|
|
import assert from "node:assert";
|
|
import { execFileSync } from "node:child_process";
|
|
import path from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import {
|
|
resolveApiDocPathToRoute,
|
|
extractDocApiPaths,
|
|
findStaleDocApiRefs,
|
|
collectRouteFiles,
|
|
KNOWN_STALE_DOC_REFS,
|
|
} from "../../scripts/check/check-docs-symbols.mjs";
|
|
import { reportStaleEntries } from "../../scripts/check/lib/allowlist.mjs";
|
|
|
|
// Tipos explícitos (não `any`) para as exports do .mjs — mantém o test em 0 warnings de
|
|
// no-explicit-any (catraca 3482) usando `as <Type>`.
|
|
const resolve = resolveApiDocPathToRoute as (apiPath: string, routeFiles: Set<string>) => boolean;
|
|
const extract = extractDocApiPaths as (src: string) => string[];
|
|
const findStale = findStaleDocApiRefs as (
|
|
docPathsByFile: { file: string; paths: string[] }[],
|
|
routeFiles: Set<string>,
|
|
allowlist: Set<string>
|
|
) => string[];
|
|
const collect = collectRouteFiles as () => Set<string>;
|
|
const allowlist = KNOWN_STALE_DOC_REFS as Set<string>;
|
|
|
|
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
const GATE = path.resolve(here, "../../scripts/check/check-docs-symbols.mjs");
|
|
const REPO = path.resolve(here, "../..");
|
|
|
|
// --- resolveApiDocPathToRoute --------------------------------------------------------
|
|
|
|
test("resolves a static doc path to a real route", () => {
|
|
const files = new Set(["src/app/api/usage/route.ts"]);
|
|
assert.equal(resolve("/api/usage", files), true);
|
|
});
|
|
|
|
test("resolves a {param} doc segment against a real [param] dynamic segment", () => {
|
|
const files = new Set(["src/app/api/providers/[id]/models/route.ts"]);
|
|
assert.equal(resolve("/api/providers/{providerId}/models", files), true);
|
|
});
|
|
|
|
test("resolves a :param (Express-style) doc segment too", () => {
|
|
const files = new Set(["src/app/api/shadow/[id]/route.ts"]);
|
|
assert.equal(resolve("/api/shadow/:id", files), true);
|
|
});
|
|
|
|
test("resolves a doc path that is a prefix of a deeper route (family reference)", () => {
|
|
const files = new Set(["src/app/api/auth/login/route.ts"]);
|
|
assert.equal(resolve("/api/auth", files), true);
|
|
});
|
|
|
|
test("resolves into a catch-all route segment", () => {
|
|
const files = new Set(["src/app/api/mcp/[...transport]/route.ts"]);
|
|
assert.equal(resolve("/api/mcp/sse/extra", files), true);
|
|
});
|
|
|
|
test("flags a hallucinated route with no matching file", () => {
|
|
const files = new Set(["src/app/api/usage/route.ts"]);
|
|
assert.equal(resolve("/api/shadow/metrics", files), false);
|
|
});
|
|
|
|
test("does NOT match when the doc path is deeper than the real route", () => {
|
|
const files = new Set(["src/app/api/acp/agents/route.ts"]);
|
|
assert.equal(resolve("/api/acp/agents/refresh", files), false);
|
|
});
|
|
|
|
test("static segment mismatch is not absorbed by an unrelated dynamic route", () => {
|
|
const files = new Set(["src/app/api/plugins/[name]/activate/route.ts"]);
|
|
// /api/plugins/{id}/enable: dynamic seg ok, but "enable" != "activate"
|
|
assert.equal(resolve("/api/plugins/{id}/enable", files), false);
|
|
});
|
|
|
|
// --- extractDocApiPaths --------------------------------------------------------------
|
|
|
|
test("extracts an /api path from a code fence", () => {
|
|
const md = "```\nGET /api/cache/stats\n```";
|
|
assert.deepEqual(extract(md), ["/api/cache/stats"]);
|
|
});
|
|
|
|
test("extracts an /api path from inline code and strips trailing prose punctuation", () => {
|
|
const md = "Call `/api/usage`, then check the result.";
|
|
assert.deepEqual(extract(md), ["/api/usage"]);
|
|
});
|
|
|
|
test("keeps balanced [param] / {param} segments intact", () => {
|
|
const md = "DELETE | `/api/shadow/[id]` | and `/api/tools/agent-bridge/agents/{id}/state`";
|
|
assert.deepEqual(extract(md), ["/api/shadow/[id]", "/api/tools/agent-bridge/agents/{id}/state"]);
|
|
});
|
|
|
|
test("does NOT capture a source-file path tail (src/lib/api/..., @/app/api/...)", () => {
|
|
const md = "see `src/lib/api/requireManagementAuth.ts` and `@/app/api/oauth/route.ts`";
|
|
assert.deepEqual(extract(md), []);
|
|
});
|
|
|
|
test("ignores file references ending in .ts even when URL-shaped", () => {
|
|
// file-ref filter lives in findStaleDocApiRefs; extract still returns it, but the
|
|
// capture must not include the leading file-path context.
|
|
const md = "endpoint is /api/cache/route.ts in the tree";
|
|
assert.deepEqual(extract(md), ["/api/cache/route.ts"]);
|
|
});
|
|
|
|
test("drops a trailing markdown-emphasis underscore (table italics)", () => {
|
|
const md = "| _500 no POST /api/cli-tools/config_ | _Zod faltando_ |";
|
|
assert.deepEqual(extract(md), ["/api/cli-tools/config"]);
|
|
});
|
|
|
|
test("discards a segment with an unbalanced bracket (regex truncation in prose)", () => {
|
|
// greedy regex captured "/api/mcp/{status" — drop the dangling segment, keep prefix.
|
|
const md = "the `/api/mcp/{status` field (prose ran on)";
|
|
assert.deepEqual(extract(md), ["/api/mcp"]);
|
|
});
|
|
|
|
// --- findStaleDocApiRefs -------------------------------------------------------------
|
|
|
|
const routes = new Set([
|
|
"src/app/api/usage/route.ts",
|
|
"src/app/api/providers/[id]/models/route.ts",
|
|
]);
|
|
|
|
test("passes a doc path that resolves to a real route", () => {
|
|
const docs = [{ file: "docs/x.md", paths: ["/api/usage"] }];
|
|
assert.deepEqual(findStale(docs, routes, new Set()), []);
|
|
});
|
|
|
|
test("flags a hallucinated doc path as 'file → path'", () => {
|
|
const docs = [{ file: "docs/x.md", paths: ["/api/ghost"] }];
|
|
assert.deepEqual(findStale(docs, routes, new Set()), ["docs/x.md → /api/ghost"]);
|
|
});
|
|
|
|
test("allowlisted stale path is frozen (not flagged)", () => {
|
|
const docs = [{ file: "docs/x.md", paths: ["/api/ghost"] }];
|
|
assert.deepEqual(findStale(docs, routes, new Set(["/api/ghost"])), []);
|
|
});
|
|
|
|
test("IGNORE swallows the OpenAI-compat /api/v1 proxy surface", () => {
|
|
const docs = [{ file: "docs/x.md", paths: ["/api/v1/chat/completions"] }];
|
|
assert.deepEqual(findStale(docs, routes, new Set()), []);
|
|
});
|
|
|
|
test("IGNORE swallows obvious example/placeholder paths", () => {
|
|
const docs = [{ file: "docs/x.md", paths: ["/api/your-route/here", "/api/example/foo"] }];
|
|
assert.deepEqual(findStale(docs, routes, new Set()), []);
|
|
});
|
|
|
|
test("file-ref paths (.ts / /route) are skipped, not flagged", () => {
|
|
const docs = [{ file: "docs/x.md", paths: ["/api/cache/route.ts", "/api/oauth/route"] }];
|
|
assert.deepEqual(findStale(docs, routes, new Set()), []);
|
|
});
|
|
|
|
// --- live gate smoke -----------------------------------------------------------------
|
|
|
|
test("collectRouteFiles finds the real route tree (non-empty, all route.ts)", () => {
|
|
const files = collect();
|
|
assert.ok(files.size > 100, "expected the full route tree");
|
|
for (const f of files) assert.match(f, /route\.tsx?$/);
|
|
});
|
|
|
|
test("KNOWN_STALE_DOC_REFS is a frozen, documented allowlist (/api/ paths; may be empty)", () => {
|
|
// The allowlist legitimately empties once every previously-stale ref is fixed — v3.8.46
|
|
// removed the last two entries (/api/chat, /api/settings/tunnels) because the gate's
|
|
// stale-enforcement flagged them as no longer suppressing a live miss. The structural
|
|
// invariant is only that any present entry is an /api/ path (not a minimum count).
|
|
for (const p of allowlist) assert.match(p, /^\/api\//);
|
|
});
|
|
|
|
// NOTE: the compression research docs that used to live in docs/research/compression/
|
|
// moved to the isolated, gitignored _tasks/research/compression/ repo, so the gate no
|
|
// longer scans them. The integration smoke stays skipped to keep this unit suite hermetic
|
|
// (it asserts the pure helpers, not the live filesystem walk).
|
|
|
|
// --- stale-allowlist enforcement (6A.3) ---
|
|
|
|
test("stale-enforcement: allowlist entry with no live miss is reported as stale", () => {
|
|
// Simulate an allowlist path whose doc was corrected (route now exists or path removed).
|
|
const liveMissPaths: string[] = ["/api/discovery/results"]; // one live miss remains
|
|
const stale = (reportStaleEntries as (a: Set<string>, l: string[], g: string) => string[])(
|
|
new Set(["/api/discovery/results", "/api/ghost-fixed"]),
|
|
liveMissPaths,
|
|
"check-docs-symbols"
|
|
);
|
|
assert.deepEqual(stale, ["/api/ghost-fixed"]);
|
|
});
|
|
|
|
test("stale-enforcement: all current KNOWN_STALE_DOC_REFS entries look like /api/ paths", () => {
|
|
// Structural invariant: every allowlist entry must be an /api/ path, not a file path
|
|
// or a prose snippet. Live staleness is enforced at gate runtime by assertNoStale().
|
|
const al = allowlist as Set<string>;
|
|
// May be empty once all stale refs are fixed (v3.8.46); the invariant is structural —
|
|
// any present entry is an /api/ path — not a minimum count.
|
|
for (const entry of al) {
|
|
assert.match(entry, /^\/api\//, `every allowlist entry must start with /api/: ${entry}`);
|
|
}
|
|
});
|