/** * @file repo-links.test.js * @description Guards the two staleness classes that shipped broken commands to * users for weeks: (1) GitHub-shaped references in a repo that is self-hosted on * Gitea — the `owner/repo` marketplace shorthand resolves to github.com ONLY * (verified against the CLI: it rejects `host/owner/repo` outright and ignores * GH_HOST / GITHUB_HOST / CLAUDE_CODE_MARKETPLACE_URL), plus raw.githubusercontent * / *.github.io / `/blob/` URLs that 404 against Gitea; and (2) a stale * committed `openapi.yaml`, which had silently drifted a whole minor version * behind package.json. Pure file reads plus one in-process spec render. * @author Nguyễn Ngọc Trí Vĩ */ const { describe, it } = require("node:test"); const assert = require("node:assert/strict"); const fs = require("node:fs"); const path = require("node:path"); const { execFileSync } = require("node:child_process"); const REPO_ROOT = path.join(__dirname, "..", ".."); // Only git-tracked text files: keeps node_modules, client/dist and every // build artifact out without hand-maintaining an ignore list. const TRACKED_EXT = /\.(md|json|js|cjs|mjs|ts|tsx|sh|ya?ml|html|css)$/; // This file states every banned pattern verbatim — in the rules below and in // the comments explaining them — so it must exclude itself or it fails on its // own source. Derived from __filename rather than hardcoded so a rename can't // silently re-arm the self-report. const SELF = path.relative(REPO_ROOT, __filename); function trackedFiles() { // No GIT_* scrub here, deliberately: under this repo's pre-commit hook // GIT_INDEX_FILE points at the index being committed, so `ls-files` lints // exactly what is about to land. Scrubbing it would lint the previous commit // instead and let a newly staged violation through. const out = execFileSync("git", ["ls-files", "-z"], { cwd: REPO_ROOT, encoding: "utf8", maxBuffer: 20_000_000, }); return ( out .split("\0") .filter((f) => f && TRACKED_EXT.test(f)) .filter((f) => f !== SELF) // Dated design docs and plans record what was true when written; rewriting // history to satisfy a lint would be a lie about the decision record. .filter((f) => !f.startsWith("docs/superpowers/")) // Generated from server/openapi-extra/* (which IS linted): the drift test // below covers it, so linting both would only double-report one fix. .filter((f) => f !== "openapi.yaml" && !f.endsWith("package-lock.json")) // Third-party files copied unchanged (server/vendor/redoc): their links are upstream's. .filter((f) => !f.includes("/vendor/")) ); } // Each rule: what to look for, and what to do instead. A `skip` pattern is // tested against the MATCHED TEXT ONLY, never the surrounding line — a skip // written against nearby prose silently never fires. Add one only when the // exception is visible inside the match itself. const RULES = [ { name: "GitHub owner/repo marketplace shorthand", // `claude plugin marketplace add Smartgift-AI/AgentHub` — resolves to // github.com and fails, because this repo lives on Gitea. re: /plugin marketplace add\s+(?!https?:\/\/)(?!\.{0,2}\/)(?!["'$])[A-Za-z0-9][\w.-]*\/[A-Za-z0-9][\w.-]*/g, fix: "use the full https:// clone URL — the shorthand is GitHub-only", // Anthropic's own marketplace really does live on GitHub. skip: [/anthropics\/claude-plugins-official/], }, { name: "GitHub URL for a Smartgift repo", re: /(?:https?:\/\/)?(?:www\.)?github\.com\/[Ss]martgift[\w-]*\/[\w.-]+/g, fix: "point at git.smartgift.io.vn", skip: [], }, { name: "raw.githubusercontent.com", // Our repos are not on GitHub, so every raw URL 404s. Same-origin paths or // a Gitea /raw/branch// URL instead. re: /raw\.githubusercontent\.com\/[\w-]+\/[\w.-]+/g, fix: "use a same-origin path, or a Gitea /raw/branch// URL", // A third-party project that really is on GitHub: the public model price // list server/lib/pricing-sync.js reads. skip: [/raw\.githubusercontent\.com\/BerriAI\/litellm$/], }, { name: "GitHub Pages host for a Smartgift site", re: /[\w-]*smartgift[\w-]*\.github\.io/gi, fix: "there is no GitHub Pages site — link the Gitea docs directory", skip: [], }, { name: "GitHub /blob// path syntax on a Gitea URL", // Gitea serves files at /src/branch//, never /blob//. re: /git\.smartgift\.io\.vn\/[^\s"')\]]*\/blob\/[\w.-]+\//g, fix: "Gitea serves files at /src/branch//", skip: [], }, { name: "retired repository name", // Gitea 301-redirects the old name, so these never fail loudly — they just // rot until someone turns the redirect off. re: /Claude-Code-Monitor|Claude-Code-Agent-Monitor/g, fix: "the repository is Smartgift-AI/AgentHub", skip: [], }, ]; describe("repo links and generated files", () => { const files = trackedFiles(); it("finds tracked text files to lint", () => { assert.ok(files.length > 50, `expected a populated file list, got ${files.length}`); }); for (const rule of RULES) { it(`no ${rule.name}`, () => { const hits = []; for (const rel of files) { const text = fs.readFileSync(path.join(REPO_ROOT, rel), "utf8"); // A fresh regex per file: the /g rules carry lastIndex between calls. for (const m of text.matchAll(new RegExp(rule.re.source, rule.re.flags))) { if (rule.skip.some((s) => s.test(m[0]))) continue; const line = text.slice(0, m.index).split("\n").length; hits.push(`${rel}:${line}: ${m[0].trim()}`); } } assert.deepEqual(hits, [], `${rule.fix}\n ${hits.join("\n ")}`); }); } it("committed openapi.yaml matches the generator output", () => { const { render, OUT } = require("../../scripts/generate-openapi-yaml"); assert.equal( fs.readFileSync(OUT, "utf8"), render().text, "openapi.yaml is stale — run `npm run openapi:yaml` and commit the result" ); }); });