inspector/scripts/verify-typecheck-coverage.test.mjs
cliffhall 7f9be24bf3 chore(scripts): derive dep-lockstep candidates from what enters each tsc program
`verify:dep-lockstep` derived its candidate set from the packages the shared
first-party sources named *directly*, so a package whose declarations reach a
program only through another package's `.d.ts` was invisible to it — it could
skew across installs and recreate the two-copy blow-up #1896 was about while
the guard stayed green. `@modelcontextprotocol/sdk` was the live example: never
written in first-party code, yet 16 of its `.d.ts` files land in clients/web's
test program.

Replace the derivation with the measured one: list every client tsconfig
project with `tsc --listFilesOnly`, map each resolved `node_modules` file to
its owning install and package, and keep the packages that reach ONE program
from TWO installs. That is exactly the set that can put two structurally
distinct copies of a type in front of one checker. The rest of the guard is
unchanged — top-level lockfile comparison, deny-by-default, `TOLERATED_SKEW` as
a name allowlist scoped to within-a-major.

The listing machinery moves to `scripts/lib/tsc-program.mjs`, shared with
`verify:typecheck-coverage`, so the two guards can't disagree about what a
program contains. It is deliberately not cached to disk between the two
processes: a stale fingerprint would make a guard measure a program that no
longer exists and pass on a real miss. The second pass costs ~14s.

`TOLERATED_SKEW` is now empty. `jose` and `@modelcontextprotocol/ext-apps` are
root-only since #1970 and cannot skew; `react` (whose types are `@types/react`,
resolved from one install) and `hono` never reach one program from two
installs. Each would be a rationale for a skew that cannot occur.

Closes #1965

Signed-off-by: cliffhall <cliff@futurescale.com>
2026-08-16 12:17:15 -04:00

293 lines
12 KiB
JavaScript

// Table-driven tests for verify-typecheck-coverage's pure parsers. Importing the
// module exposes these without running the guard (its execution is behind
// `main()`, called only when the file is run directly). Each case pins a rule a
// #1799 review round found. Run via `npm run test:scripts`.
//
// The `tsc`-script and tsconfig-graph parsers this guard used to own moved to
// `lib/tsc-program.mjs` when `verify:dep-lockstep` began measuring the same
// programs (#1965); their cases moved to `lib/tsc-program.test.mjs` with them.
import { test } from "node:test";
import assert from "node:assert/strict";
import {
matchesTestGlob,
isRequiredSource,
integrityAdvice,
isScriptsTestFile,
testScriptGlobs,
testScriptNarrowingFlags,
testScriptProblems,
tscBuildStatus,
} from "./verify-typecheck-coverage.mjs";
test("isRequiredSource: TS extensions, ambient .d.ts excluded (r7)", () => {
for (const f of ["a.ts", "a.tsx", "a.mts", "a.cts"])
assert.ok(isRequiredSource(f), f);
for (const f of ["a.js", "a.d.ts", "a.d.mts", "a.d.cts", "a.json"])
assert.ok(!isRequiredSource(f), f);
});
test("tscBuildStatus: ok / neutered / none (r25)", () => {
const status = (build) =>
tscBuildStatus({ validate: "npm run build", build });
assert.equal(status("tsc -b && vite build"), "ok");
assert.equal(status("tsc --build"), "ok");
assert.equal(status("tsc -b --noCheck && vite build"), "neutered");
assert.equal(status("vite build"), "none");
assert.equal(status("tsc --noEmit -p tsconfig.json"), "none"); // -b required
});
test("matchesTestGlob: the guard's contract, not node's glob engine", () => {
// Only the two properties the guard actually relies on — the rest of node's
// glob semantics are node's to test, which is the point of delegating to it.
const g = "scripts/**/*.test.mjs";
assert.ok(matchesTestGlob("scripts/verify-typecheck-coverage.test.mjs", g)); // zero-depth **
assert.ok(matchesTestGlob("scripts/lib/npm-scripts.test.mjs", g)); // nested
assert.ok(!matchesTestGlob("scripts/lib/npm-scripts.spec.mjs", g)); // the probe-B rename
});
test("testScriptGlobs: harvests across delegation (r31 finding 1 / r32 finding 2)", () => {
// Direct form — the glob itself, with `node` and flags dropped.
assert.deepEqual(
testScriptGlobs({ "test:scripts": 'node --test "scripts/**/*.test.mjs"' }),
["scripts/**/*.test.mjs"],
);
// Delegating form — BOTH child globs, not the literal npm/run/<name> tokens.
const delegating = testScriptGlobs({
"test:scripts": "npm run test:scripts:lib && npm run test:scripts:guard",
"test:scripts:lib": 'node --test "scripts/lib/**/*.test.mjs"',
"test:scripts:guard": 'node --test "scripts/*.test.mjs"',
});
assert.ok(delegating.includes("scripts/lib/**/*.test.mjs"));
assert.ok(delegating.includes("scripts/*.test.mjs"));
// A pre<name> hook is reached too (npm runs it implicitly).
assert.ok(
testScriptGlobs({
"test:scripts": "node --test scripts/a.test.mjs",
"pretest:scripts": "node --test scripts/b.test.mjs",
}).includes("scripts/b.test.mjs"),
);
// An unreachable script contributes nothing.
assert.deepEqual(
testScriptGlobs({
"test:scripts": "node --test scripts/a.test.mjs",
other: "node --test scripts/z.test.mjs",
}),
["scripts/a.test.mjs"],
);
});
test("testScriptGlobs: only `node --test` segments contribute (r33 finding 1)", () => {
// A reachable NON-runner command's glob must not be attributed to the runner:
// `scripts/**/*.mjs` matches a renamed `*.spec.mjs`, so harvesting it would
// make the probe-B rename pass while `node --test` silently ran 6 fewer tests.
assert.deepEqual(
testScriptGlobs({
"test:scripts": 'node --test "scripts/**/*.test.mjs"',
"pretest:scripts": 'prettier --check "scripts/**/*.mjs"',
}),
["scripts/**/*.test.mjs"],
);
// Same within one command: only the `--test` segment's args are harvested.
assert.deepEqual(
testScriptGlobs({
"test:scripts":
'prettier --check "scripts/**/*.mjs" && node --test "scripts/**/*.test.mjs"',
}),
["scripts/**/*.test.mjs"],
);
});
test("testScriptGlobs: flag values and ./ prefixes (r34 findings 1 & 2)", () => {
// A glob-valued `--test-*` flag is NOT a positional arg. `scripts/**/*.mjs`
// matches a renamed `*.spec.mjs`, so harvesting it would vouch for the very
// file the runner skips — the r33 suppression one level in.
assert.deepEqual(
testScriptGlobs({
"test:scripts":
'node --test --experimental-test-coverage --test-coverage-include "scripts/**/*.mjs" "scripts/**/*.test.mjs"',
}),
["scripts/**/*.test.mjs"],
);
// A leading `./` is normalized off — `node --test` accepts it, `git ls-files`
// never emits it, so keeping it would blame every file for a rename that
// never happened.
assert.deepEqual(
testScriptGlobs({
"test:scripts": 'node --test "./scripts/**/*.test.mjs"',
}),
["scripts/**/*.test.mjs"],
);
// …and the normalized glob really does match what `git ls-files` emits.
assert.ok(
testScriptProblems(
{
validate: "npm run test:scripts",
"test:scripts": 'node --test "./scripts/**/*.test.mjs"',
},
["scripts/lib/npm-scripts.test.mjs"],
).length === 0,
);
});
test("testScriptGlobs: empty when no glob is named (r33 finding 2)", () => {
// The condition the "`test:scripts` names no path/glob" branch keys off — a
// bare `node --test` auto-discovers, so the guard can't tell what it runs.
assert.deepEqual(testScriptGlobs({ "test:scripts": "node --test" }), []);
assert.deepEqual(testScriptGlobs({}), []);
});
test("testScriptProblems: all three axes (r33 finding 2)", () => {
const WIRED = {
validate: "npm run test:scripts",
"test:scripts": 'node --test "scripts/**/*.test.mjs"',
};
const only = (p) => (assert.equal(p.length, 1, p.join("\n")), p[0]);
// Green: wired, non-empty, every file glob-matched.
assert.deepEqual(testScriptProblems(WIRED, ["scripts/a.test.mjs"]), []);
// Axis 1 — not reachable from `validate`. Reported alone: with the tests
// unrun, the other two axes are moot.
assert.match(
only(
testScriptProblems(
{ validate: "echo hi", "test:scripts": WIRED["test:scripts"] },
["scripts/a.test.mjs"],
),
),
/no longer runs `test:scripts`/,
);
// Axis 2 — no tracked test files at all.
assert.match(
only(testScriptProblems(WIRED, [])),
/no `scripts\/\*\*\/\*\.test\.\*` files are tracked/,
);
// Axis 3 — a file `node --test` would skip (the probe-B rename).
assert.match(
only(testScriptProblems(WIRED, ["scripts/a.spec.mjs"])),
/^scripts\/a\.spec\.mjs: not matched by the `test:scripts` glob/,
);
// The empty-harvest branch: ONE message naming the real problem, not one
// unfollowable blame per file. (`if (false)`-mutating it yields 2 messages.)
assert.match(
only(
testScriptProblems(
{ validate: "npm run test:scripts", "test:scripts": "node --test" },
["scripts/a.test.mjs", "scripts/b.test.mjs"],
),
),
/names no path\/glob/,
);
});
test("isScriptsTestFile: discovery by content, not name (r36 finding 1)", () => {
const TEST_SRC = 'import { test } from "node:test";\ntest("x", () => {});\n';
// The name is irrelevant — a dot→hyphen rename escapes every name pattern,
// and that is exactly the rename that silently dropped 6 of 24 tests.
assert.ok(isScriptsTestFile("scripts/lib/npm-scripts.test.mjs", TEST_SRC));
assert.ok(isScriptsTestFile("scripts/lib/npm-scripts-test.mjs", TEST_SRC));
assert.ok(isScriptsTestFile("scripts/tokenize-tests.mjs", TEST_SRC));
assert.ok(isScriptsTestFile("scripts/a.test.js", TEST_SRC));
assert.ok(
isScriptsTestFile(
"scripts/a.test.cjs",
"const { test } = require('node:test');\ntest('x', () => {});\n",
),
);
// A shared helper importing `node:test`'s `mock` registers no tests — telling
// it to rename itself to `*.test.mjs` would make node run an empty file.
assert.ok(
!isScriptsTestFile(
"scripts/lib/test-mocks.mjs",
'import { mock } from "node:test";\nexport const resetAll = () => mock.reset();\n',
),
);
// A guard script that merely mentions testing is not a test.
assert.ok(
!isScriptsTestFile(
"scripts/verify-typecheck-coverage.mjs",
"// runs the test:scripts glob\nimport path from 'node:path';\n",
),
);
// Non-JS never counts, whatever it contains.
assert.ok(!isScriptsTestFile("scripts/notes.md", TEST_SRC));
assert.ok(!isScriptsTestFile("scripts/a.test.ts", TEST_SRC));
});
test("testScriptNarrowingFlags: flags that shrink the run (r35 finding 1)", () => {
const flags = (cmd) => testScriptNarrowingFlags({ "test:scripts": cmd });
const GLOB = '"scripts/**/*.test.mjs"';
// `--test-shard 1/2` keeps every glob intact and runs half the suite, exit 0.
assert.deepEqual(flags(`node --test --test-shard 1/2 ${GLOB}`), [
"--test-shard",
]);
assert.deepEqual(flags(`node --test --test-only ${GLOB}`), ["--test-only"]);
assert.deepEqual(flags(`node --test --test-name-pattern zzz ${GLOB}`), [
"--test-name-pattern",
]);
assert.deepEqual(flags(`node --test --test-skip-pattern=zzz ${GLOB}`), [
"--test-skip-pattern",
]); // `=` form
// Not narrowing: the plain form, and a coverage flag that only reports.
assert.deepEqual(flags(`node --test ${GLOB}`), []);
assert.deepEqual(
flags(
`node --test --experimental-test-coverage --test-coverage-include "scripts/**/*.mjs" ${GLOB}`,
),
[],
);
// A narrowing flag on a NON-runner segment isn't the runner's (r33 gate).
assert.deepEqual(
testScriptNarrowingFlags({
"test:scripts": `node --test ${GLOB}`,
"pretest:scripts": "some-tool --test-only",
}),
[],
);
// Reported as a gate-integrity problem, with the globs still harvested.
const wired = {
validate: "npm run test:scripts",
"test:scripts": `node --test --test-shard 1/2 ${GLOB}`,
};
assert.deepEqual(testScriptGlobs(wired), ["scripts/**/*.test.mjs"]);
const problems = testScriptProblems(wired, ["scripts/a.test.mjs"]);
assert.equal(problems.length, 1);
assert.match(
problems[0],
/--test-shard.*runs only part of the matched suite/,
);
});
test("integrityAdvice: typecheck footer only for typecheck issues (r35 finding 2 / nit 3)", () => {
const TYPECHECK = "clients/cli: the root `validate` chain no longer runs …";
const SIBLING =
"the root `validate` no longer runs `verify:format-coverage` …";
const TESTS = "scripts/a.spec.mjs: not matched by the `test:scripts` glob …";
// Only typecheck-wiring issues → advise.
assert.match(
integrityAdvice([TYPECHECK], []),
/Restore the `typecheck` wiring/,
);
// Every issue is a non-typecheck one → stay silent. The sibling-guard vouch
// and client-enrollment problems count here too, not just `test:scripts`.
assert.equal(integrityAdvice([TESTS], [TESTS]), null);
assert.equal(integrityAdvice([SIBLING], [SIBLING]), null);
assert.equal(integrityAdvice([SIBLING, TESTS], [SIBLING, TESTS]), null);
// Client enrollment ("declares no `typecheck` script …") IS about the
// typecheck pass — the footer's first clause is its fix — so it advises.
const ENROLLMENT =
"clients/tui: declares no `typecheck` script, has no `tsconfig.json` `references` …";
assert.match(
integrityAdvice([ENROLLMENT], [SIBLING, TESTS]),
/Restore the `typecheck` wiring/,
);
// A mix still advises — the typecheck issue is real.
assert.match(
integrityAdvice([SIBLING, TYPECHECK, TESTS], [SIBLING, TESTS]),
/Restore the `typecheck` wiring/,
);
});