Files
servers/scripts/lib/npm-scripts.test.mjs
cliffhallandClaude Opus 5.5 a5c40bf838 feat: port the skills harness (verify:skills, verify:skills:cli, skills:eval)
Port the MCP Inspector's skills infrastructure for #4858 Part 3:

- scripts/verify-skills.mjs, verify-skills-cli.mjs, skill-eval.mjs and
  scripts/lib/{skill-manifest,claude-cli,npm-scripts,win-shell-args}.mjs,
  with all their *.test.mjs files
- root npm scripts verify:skills, verify:skills:cli, skills:eval and
  test:scripts; yaml as a root devDependency
- docs/skill-authoring.md, with paths and provenance adapted
- a root-guards job in typescript.yml running verify:skills and
  verify:skills:cli on every push and pull request
- .claude/skills/ (kept by a .gitkeep until the first skill lands)
- un-ignore scripts/lib/, which the Python `lib/` pattern swallowed

Adapted to this repo: checkWiring reads typescript.yml, and carries two
temporary allowances (validate -> verify:format-coverage, removed by
#4864; local:gate -> verify:skills:cli, removed by #4871) that excuse a
link only while its pending script is absent and fail as stale once it
exists. The "no skills found" failure is kept, with a temporary
EMPTY_SKILLS_BOOTSTRAP allowance that fails as stale once a skill exists.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: cliffhall <cliff@futurescale.com>
2026-09-29 00:10:03 -04:00

145 lines
4.9 KiB
JavaScript

// Table-driven tests for the shared npm-script reachability helpers. Each case
// pins a rule the inspector#1799 review took a round to get right — the comment names it.
// Run via `npm run test:scripts` (node:test; the root has no vitest harness).
import { test } from "node:test";
import assert from "node:assert/strict";
import {
reachableScripts,
rootReachesScript,
rootRunsClientValidate,
tokenize,
} from "./npm-scripts.mjs";
test("tokenize: quoting (r17)", () => {
const cases = [
[
"tsc --noEmit -p tsconfig.json",
["tsc", "--noEmit", "-p", "tsconfig.json"],
],
[`tsc -p "tsconfig.test.json"`, ["tsc", "-p", "tsconfig.test.json"]],
[`tsc -p 'tsconfig.test.json'`, ["tsc", "-p", "tsconfig.test.json"]],
[
`prettier --check "core/**/*.ts"`,
["prettier", "--check", "core/**/*.ts"],
],
[" spaced out ", ["spaced", "out"]],
["", []],
];
for (const [input, expected] of cases)
assert.deepEqual(tokenize(input), expected, input);
});
test("reachableScripts: follows `npm run` refs and pre/post hooks (r10)", () => {
const scripts = {
validate: "npm run lint && npm run build",
lint: "eslint .",
build: "tsc",
prebuild: "echo pre", // implicit hook npm runs around `build`
postbuild: "echo post",
unrelated: "noop",
};
const reached = reachableScripts(scripts, "validate");
assert.ok(reached.has("validate"));
assert.ok(reached.has("lint"));
assert.ok(reached.has("build"));
assert.ok(reached.has("prebuild"), "pre<name> hook");
assert.ok(reached.has("postbuild"), "post<name> hook");
assert.ok(!reached.has("unrelated"));
});
test("reachableScripts: follows every npm spelling of `run` (inspector#2341)", () => {
// `run-script` is the canonical command; `run`, `rum` and `urn` are its
// aliases. A chain written in any of them reaches the same scripts, so a
// guard that only followed `npm run` would let `npm run-script validate:web`
// reintroduce what it exists to forbid (Copilot).
const scripts = {
gate: "npm run-script a && npm rum b && npm urn c && npm run d",
a: "noop",
b: "noop",
c: "noop",
d: "noop",
e: "noop",
};
const reached = reachableScripts(scripts, "gate");
for (const name of ["a", "b", "c", "d"])
assert.ok(reached.has(name), `npm spelling reaching ${name}`);
assert.ok(!reached.has("e"));
});
test("reachableScripts: a `prevalidate`-hosted typecheck is reachable (r10)", () => {
const scripts = {
validate: "npm run build && npm run test",
prevalidate: "npm run typecheck",
typecheck: "tsc --noEmit",
build: "tsc",
test: "vitest run",
};
assert.ok(reachableScripts(scripts, "validate").has("typecheck"));
});
test("rootRunsClientValidate: forms that count vs. don't", () => {
// `cmd` lives in `vt`, reached from `validate` via `npm run vt`.
const runs = (cmd) =>
rootRunsClientValidate({ validate: "npm run vt", vt: cmd }, "clients/tui");
// Counts:
assert.ok(runs("cd clients/tui && npm run validate"), "plain cd");
assert.ok(runs("cd ./clients/tui && npm run validate"), "leading ./ (r15)");
assert.ok(runs(`cd "clients/tui" && npm run validate`), "quoted dir (r17)");
assert.ok(
runs(`cd clients/tui && npm run "validate"`),
"quoted script name (r18)",
);
assert.ok(
runs("cd clients/tui && npm run build && npm run validate"),
"extra step",
);
assert.ok(runs("npm --prefix clients/tui run validate"), "--prefix (r19)");
// Does NOT count:
assert.ok(
!runs("cd clients/tui-next && npm run validate"),
"prefix-sibling must not match the shorter name (r16)",
);
assert.ok(
!runs("cd clients/tui && npm run validate:fast"),
"`run validate:fast` is a different script (r20)",
);
assert.ok(!runs("cd clients/tui && npm run build"), "no validate");
});
test("rootRunsClientValidate: only counts a reachable script (r5)", () => {
// A cd-validate call in a script nothing reachable from `validate` runs must
// NOT count — that's the whole reason the reachability restriction exists.
assert.ok(
!rootRunsClientValidate(
{
validate: "npm run something-else",
orphan: "cd clients/tui && npm run validate",
},
"clients/tui",
),
"orphan script isn't reachable from validate",
);
// Reached via one hop of indirection counts.
assert.ok(
rootRunsClientValidate(
{
validate: "npm run validate:tui",
"validate:tui": "cd clients/tui && npm run validate",
},
"clients/tui",
),
"reached via validate:tui",
);
});
test("rootReachesScript: sibling-guard vouching", () => {
const scripts = {
validate: "npm run verify:format-coverage && npm run validate:web",
"validate:web": "cd clients/web && npm run validate",
};
assert.ok(rootReachesScript(scripts, "verify:format-coverage"));
assert.ok(rootReachesScript(scripts, "validate:web"));
assert.ok(!rootReachesScript(scripts, "verify:typecheck-coverage"));
});