Ctrl K
ComunidadGuías…

scripts/testing/verification-docs.test.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
// Verification recipes are followed by hand, so nothing else notices when a
// command, file, route or link they cite stops existing. This reads every
// recipe under docs/verification and checks each citation against the tree.
import { existsSync, readdirSync, readFileSync } from "node:fs";
import { join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { describe, expect, it } from "vitest";

import { HELP } from "../control-omb.ts";

const ROOT = fileURLToPath(new URL("../..", import.meta.url));
const DOCS = join(ROOT, "docs", "verification");
const recipes = readdirSync(DOCS).filter((name) => name.endsWith(".md")).sort();
const text = (recipe: string) => readFileSync(join(DOCS, recipe), "utf8");
// Paths inside external links belong to other repositories.
const withoutUrls = (markdown: string) => markdown.replace(/https?:\/\/\S+/g, " ");
const cited = (pattern: RegExp) => {
  const hits: string[] = [];
  for (const recipe of recipes) {
    for (const match of withoutUrls(text(recipe)).matchAll(pattern)) hits.push(`${recipe}: ${match[1]!}`);
  }
  return hits;
};
const target = (hit: string) => hit.slice(hit.indexOf(": ") + 2);

// The verbs `help` prints, minus the shell line under "isolated fixture".
const helpVerbs = new Set([...HELP.matchAll(/^ {2}([a-z][\w-]*)(?= |$)/gm)].map((match) => match[1]!).filter((verb) => verb !== "node"));
helpVerbs.add("help");

// Routes are registered in server/index.ts and in the modules under
// server/routes; tests there quote paths without registering them.
const ROUTES_DIR = join(ROOT, "server", "routes");
// Including subfolders and the factories that live beside their feature.
const routeModules = readdirSync(ROUTES_DIR, { recursive: true, encoding: "utf8" })
  .filter((name) => name.endsWith(".ts") && !name.endsWith(".test.ts")).sort()
  .map((name) => join(ROUTES_DIR, name));
const featureRouteModules = ["data/routes.ts", "workspace-backup-http.ts", "cloud-move-http.ts"].map((name) => join(ROOT, "server", name));
const serverSource = [join(ROOT, "server", "index.ts"), ...routeModules, ...featureRouteModules]
  .map((file) => readFileSync(file, "utf8")).join("\n");
const hooksSource = readFileSync(join(ROOT, "server", "webhook-ingress.ts"), "utf8");
const hostedSource = readFileSync(join(ROOT, "enterprise", "server", "workspace-access.ts"), "utf8");
// Only constants named in the public handler's accepted-path guard are routes.
// Outbound post("/api/handoff/...") calls belong to the identity service, not us.
const hostedConstants = new Map([...hostedSource.matchAll(/const ([A-Z_]+) = "(\/api\/[^"\n]+)";/g)].map(match => [match[1]!, match[2]!]));
const hostedPublicRoutes = new Set((hostedSource.match(/!\[([^\]]+)\]\.includes\(path\)/)?.[1] ?? "")
  .split(",").map(name => hostedConstants.get(name.trim())).filter((path): path is string => path !== undefined));
// server/index.ts matches routes two ways: `path === "/api/x"` and
// `path.match(/^\/api\/x\/(a|b)$/)`. Collect the regex form too.
const routePatterns = [...serverSource.matchAll(/\/(\^\\\/api\\\/(?:\[(?:[^\]\\]|\\.)*\]|[^/\\\n[]|\\.)*)\/[a-z]*/g)].map((match) => new RegExp(match[1]!));
const PLACEHOLDER = /^(ID|:[\w-]+|<[^>]+>|\{[^}]+\})$/;

const slug = (heading: string) => heading.trim().toLowerCase().replace(/[^\w\- ]/g, "").replace(/\s+/g, "-");

describe("docs/verification recipes cite things that exist", () => {
  it("use control:omb verbs that help lists", () => {
    const used = cited(/pnpm control:omb ([a-z][\w-]*)/g);
    expect(used.length).toBeGreaterThan(0);
    expect(used.filter((hit) => !helpVerbs.has(target(hit)))).toEqual([]);
    // the launcher form may also name `launch`, which pnpm cannot run
    const direct = cited(/scripts\/control-omb\.ts ([a-z][\w-]*)/g);
    expect(direct.filter((hit) => target(hit) !== "launch" && !helpVerbs.has(target(hit)))).toEqual([]);
  });

  it("cite source files that exist", () => {
    const refs = cited(/(?<![\w/.-])((?:scripts|server|src|shared|electron|companion|enterprise)\/[\w./-]+\.(?:ts|tsx|mjs|cjs|py))(?![\w/])/g);
    expect(refs.length).toBeGreaterThan(50);
    expect([...new Set(refs.filter((hit) => !existsSync(join(ROOT, target(hit)))))]).toEqual([]);
  });

  it("cite test files that exist", () => {
    const refs = cited(/(?<![\w/.-])([\w./-]+\.test\.(?:ts|mjs))(?![\w/])/g);
    expect(refs.length).toBeGreaterThan(20);
    expect([...new Set(refs.filter((hit) => !existsSync(join(ROOT, target(hit)))))]).toEqual([]);
  });

  it("cite API routes the server registers", () => {
    const refs = cited(/(?<!\w)(\/(?:api|hooks)\/[\w./:{}<>-]+)/g);
    expect(refs.length).toBeGreaterThan(5);
    const registered = (route: string) => {
      const path = route.replace(/[.,;:]+$/, "");
      if (hostedPublicRoutes.has(path)) return true;
      const source = path.startsWith("/hooks/") ? hooksSource : serverSource;
      const segments = path.split("/").slice(1);
      const literal = segments.findIndex((segment) => PLACEHOLDER.test(segment));
      if (literal === -1) return source.includes(`"${path}"`) || routePatterns.some((pattern) => pattern.test(path));
      // a placeholder path is known by its first two segments
      const prefix = `/${segments.slice(0, Math.min(2, literal)).join("/")}`;
      return source.includes(`"${prefix}`) || source.includes(`${prefix}/`)
        || routePatterns.some((pattern) => pattern.test(segments.map((segment) => PLACEHOLDER.test(segment) ? "x-1" : segment).join("/").replace(/^/, "/")));
    };
    expect([...new Set(refs.filter((hit) => !registered(target(hit))))]).toEqual([]);
  });

  it("distinguishes delegated public routes from external identity backchannels", () => {
    expect([...hostedPublicRoutes]).toEqual(["/api/auth/hosted/start", "/api/auth/hosted/callback"]);
    expect(hostedPublicRoutes.has("/api/handoff/consume")).toBe(false);
    expect(hostedPublicRoutes.has("/api/handoff/check")).toBe(false);
  });

  it("are all reachable from README.md", () => {
    const linked = new Set([...text("README.md").matchAll(/\]\(([^)#\s]+\.md)/g)].map((match) => match[1]!));
    expect(recipes.filter((recipe) => recipe !== "README.md" && !linked.has(recipe))).toEqual([]);
  });

  it("link to files and headings that exist", () => {
    const broken: string[] = [];
    for (const recipe of recipes) {
      for (const [, link] of text(recipe).matchAll(/\]\(([^)\s]+)\)/g)) {
        if (/^(https?:|mailto:|#)/.test(link!)) continue;
        const [file, anchor] = link!.split("#");
        const resolved = resolve(DOCS, file!);
        if (!existsSync(resolved)) { broken.push(`${recipe}: ${link}`); continue; }
        if (anchor && resolved.endsWith(".md")) {
          const headings = [...readFileSync(resolved, "utf8").matchAll(/^#+\s+(.+)$/gm)].map((match) => slug(match[1]!));
          if (!headings.includes(anchor)) broken.push(`${recipe}: ${link} (no such heading)`);
        }
      }
    }
    expect(broken).toEqual([]);
  });
});