inspector/scripts/lib/tsc-program.test.mjs
cliffhall a9b9a917e3 chore(scripts): compare only the copies a program actually met
Two findings from the Copilot review of #1965, both real.

1. The candidates were flattened to package names before the version
   comparison, so a third install holding a different copy failed the guard
   even when no program loads that copy beside another — and the diagnostic
   named an install that never took part. `crossInstallPackages` now returns
   the co-occurrences themselves (name → program → install → entry paths) and
   `findSkew` compares per program, across only the installs that met there.

2. Folding a nested copy onto its outermost install lost the path that
   identifies which copy was loaded, and the version was then read from the
   install's top-level entry — absent or differently versioned, either way
   letting a real pair pass. `classifyModulePath` now also returns the
   `entryPath` npm keys the lockfile by, `topLevelLockVersions` becomes
   `lockVersionsByPath`, and each holder is priced from the entry the program
   resolved. A resolved copy with no lockfile entry is now a hard failure
   rather than a silently dropped holder.

The failure output prints `in <program>` with each holder's install path
beside its version, so a nested copy is distinguishable from a top-level one.

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

292 lines
9.8 KiB
JavaScript

// Table-driven tests for the shared `tsc` program helpers — the machinery both
// `verify:typecheck-coverage` (#1791) and `verify:dep-lockstep` (#1965) read a
// program through. One case per rule; the comment names the rule, so relaxing
// one shows up as a deleted assertion rather than a quiet behavior shift. The
// script-parsing and tsconfig-graph cases moved here with their code when the
// two guards were unified; their `(rN)` tags are the #1799 review rounds that
// found them. Run via `npm run test:scripts`.
import { test } from "node:test";
import assert from "node:assert/strict";
import {
classifyModulePath,
crossInstallPackages,
isDisablingFlag,
isTsc,
parseTsconfigReferences,
projectConfigFile,
refToProject,
typecheckProjects,
} from "./tsc-program.mjs";
test("isTsc: matches by basename incl. path-invoked (r18 regression)", () => {
for (const t of [
"tsc",
"node_modules/.bin/tsc",
"./node_modules/.bin/tsc.cmd",
])
assert.ok(isTsc(t), t);
for (const t of ["vitest", "prettier", "tscx", "atsc"])
assert.ok(!isTsc(t), t);
});
test("isDisablingFlag: case-insensitive (r18)", () => {
for (const t of [
"--noCheck",
"--nocheck",
"--listFilesOnly",
"--LISTFILESONLY",
])
assert.ok(isDisablingFlag(t), t);
for (const t of ["--noEmit", "-p", "--project", "noCheck"])
assert.ok(!isDisablingFlag(t), t);
});
test("typecheckProjects: harvests -p / --project / -b, implicit tsconfig.json (r13)", () => {
const { projects, neutered } = typecheckProjects({
typecheck:
"tsc --noEmit -p tsconfig.json && tsc --noEmit -p tsconfig.test.json",
});
assert.deepEqual(projects, ["tsconfig.json", "tsconfig.test.json"]);
assert.equal(neutered.length, 0);
// A bare `tsc` (no project flag) resolves the implicit ./tsconfig.json.
assert.deepEqual(typecheckProjects({ typecheck: "tsc --noEmit" }).projects, [
"tsconfig.json",
]);
// Path-invoked binary still counts (r18).
assert.deepEqual(
typecheckProjects({ typecheck: "node_modules/.bin/tsc -p tsconfig.json" })
.projects,
["tsconfig.json"],
);
// A quoted project path (r17).
assert.deepEqual(
typecheckProjects({ typecheck: `tsc -p "tsconfig.test.json"` }).projects,
["tsconfig.test.json"],
);
// `--project` long form, and `-b`/`--build` project paths (r13).
const proj = (cmd) => typecheckProjects({ typecheck: cmd }).projects;
assert.deepEqual(proj("tsc --noEmit --project tsconfig.json"), [
"tsconfig.json",
]);
assert.deepEqual(proj("tsc -b tsconfig.json"), ["tsconfig.json"]);
assert.deepEqual(proj("tsc --build tsconfig.json"), ["tsconfig.json"]);
assert.deepEqual(proj("tsc -b"), ["tsconfig.json"]); // implicit fallback
});
test("typecheckProjects: neutered by --noCheck / --listFilesOnly (r10)", () => {
const { projects, neutered } = typecheckProjects({
typecheck:
"tsc --noEmit -p tsconfig.json --noCheck && tsc --noEmit -p tsconfig.test.json",
});
assert.deepEqual(projects, ["tsconfig.test.json"]);
assert.deepEqual(neutered, [{ project: "tsconfig.json", flag: "--noCheck" }]);
});
test("typecheckProjects: delegating typecheck, ignores non-tsc segments (r15)", () => {
const { projects } = typecheckProjects({
typecheck: "npm run typecheck:src && npm run typecheck:test",
"typecheck:src": "tsc --noEmit -p tsconfig.json",
"typecheck:test": "tsc --noEmit --project tsconfig.test.json",
});
assert.deepEqual(projects.sort(), ["tsconfig.json", "tsconfig.test.json"]);
});
test("parseTsconfigReferences: JSONC tolerance (r17-nit2 block comments)", () => {
const refs = (raw) => parseTsconfigReferences(raw);
assert.deepEqual(refs('{ "references": [{ "path": "./a" }] }'), ["./a"]);
assert.deepEqual(
refs('/* solution */\n{ "references": [{ "path": "./a" }] }'),
["./a"],
);
assert.deepEqual(refs('{ "references": [{ "path": "./a" }] } // trailing'), [
"./a",
]);
assert.deepEqual(refs('{ "references": [{ "path": "./a" },] }'), ["./a"]); // trailing comma
assert.deepEqual(refs('{ "files": [] }'), []); // no references
assert.deepEqual(refs("{ not json"), []); // malformed
assert.deepEqual(refs('{ "references": [{ "prepend": true }] }'), []); // no path
});
test("projectConfigFile: directory-form entry means <dir>/tsconfig.json (r26)", () => {
assert.equal(
projectConfigFile("clients/cli", "tsconfig.test.json"),
"clients/cli/tsconfig.test.json",
);
assert.equal(
projectConfigFile("clients/cli", "packages/a"),
"clients/cli/packages/a/tsconfig.json",
);
assert.equal(
projectConfigFile("clients/cli", "."),
"clients/cli/tsconfig.json",
);
});
test("refToProject: refs resolve against the REFERRING config's dir (r26)", () => {
// A ref is relative to the tsconfig that declares it, not to clientDir.
assert.equal(
refToProject(
"clients/web",
"clients/web/tsconfig.json",
"./tsconfig.app.json",
),
"tsconfig.app.json",
);
assert.equal(
refToProject(
"clients/web",
"clients/web/sub/tsconfig.json",
"../other.json",
),
"other.json",
);
assert.equal(
refToProject("clients/web", "clients/web/sub/tsconfig.json", "./deep"),
"sub/deep",
);
});
test("classifyModulePath: install root is the OUTERMOST node_modules, package the innermost (#1965)", () => {
assert.deepEqual(classifyModulePath("node_modules/zod/index.d.ts"), {
installRoot: ".",
name: "zod",
entryPath: "node_modules/zod",
});
assert.deepEqual(
classifyModulePath("clients/web/node_modules/zod/index.d.ts"),
{
installRoot: "clients/web",
name: "zod",
entryPath: "node_modules/zod",
},
);
// A scoped package keeps both segments.
assert.deepEqual(
classifyModulePath("node_modules/@modelcontextprotocol/sdk/dist/x.d.ts"),
{
installRoot: ".",
name: "@modelcontextprotocol/sdk",
entryPath: "node_modules/@modelcontextprotocol/sdk",
},
);
// A NESTED copy folds onto its outermost install: npm resolving a transitive
// conflict inside one install is routine, not a cross-install skew. Its
// `entryPath` still names the copy itself, so the caller can price the version
// the program actually loaded (Copilot, #1965 r1).
assert.deepEqual(
classifyModulePath("node_modules/a/node_modules/zod/index.d.ts"),
{
installRoot: ".",
name: "zod",
entryPath: "node_modules/a/node_modules/zod",
},
);
assert.deepEqual(
classifyModulePath(
"clients/cli/node_modules/a/node_modules/@scope/b/index.d.ts",
),
{
installRoot: "clients/cli",
name: "@scope/b",
entryPath: "node_modules/a/node_modules/@scope/b",
},
);
});
test("classifyModulePath: paths that name no package", () => {
for (const rel of [
"core/mcp/client.ts", // first-party source
"clients/web/src/App.tsx",
"node_modules/.bin/tsc", // npm bookkeeping, not a package
"node_modules/.package-lock.json",
"node_modules/@scope", // a scope directory names no package
"node_modules", // the directory itself
])
assert.equal(classifyModulePath(rel), null, rel);
});
test("classifyModulePath: a path segment merely CONTAINING node_modules is not one", () => {
// Segment-wise matching, not substring: a first-party dir whose name embeds
// the word would otherwise be read as an install root.
assert.equal(classifyModulePath("core/node_modules_fixtures/a.ts"), null);
});
test("crossInstallPackages: two installs in ONE program is the whole test (#1965)", () => {
const found = crossInstallPackages([
{
label: "clients/web/tsconfig.test.json",
files: [
"node_modules/zod/index.d.ts",
"clients/web/node_modules/zod/index.d.ts",
"clients/web/node_modules/react/index.d.ts",
],
},
]);
assert.deepEqual([...found.keys()], ["zod"]);
const byRoot = found.get("zod").get("clients/web/tsconfig.test.json");
assert.deepEqual([...byRoot.keys()].sort(), [".", "clients/web"]);
// The entry paths ride along so the caller can price each copy from its own
// lockfile entry (Copilot, #1965 r1).
assert.deepEqual([...byRoot.get(".")], ["node_modules/zod"]);
});
test("crossInstallPackages: two installs across SEPARATE programs is not a candidate", () => {
// Two copies the type checker never has to relate — each program sees one.
const found = crossInstallPackages([
{ label: "a", files: ["node_modules/zod/index.d.ts"] },
{ label: "b", files: ["clients/cli/node_modules/zod/index.d.ts"] },
]);
assert.equal(found.size, 0);
});
test("crossInstallPackages: a nested duplicate inside one install is not a candidate", () => {
const found = crossInstallPackages([
{
label: "a",
files: [
"node_modules/zod/index.d.ts",
"node_modules/other/node_modules/zod/index.d.ts",
],
},
]);
assert.equal(found.size, 0);
});
test("crossInstallPackages: every program that saw both copies is recorded", () => {
const both = [
"node_modules/zod/index.d.ts",
"clients/web/node_modules/zod/index.d.ts",
];
const found = crossInstallPackages([
{ label: "web/app", files: both },
{ label: "web/test", files: both },
{ label: "cli/src", files: ["node_modules/zod/index.d.ts"] },
]);
assert.deepEqual([...found.get("zod").keys()].sort(), [
"web/app",
"web/test",
]);
});
test("crossInstallPackages: a nested copy is kept as its own entry path (Copilot, #1965 r1)", () => {
// Folded onto the root install for candidacy, but recorded at the path it was
// loaded from so its real version can be read.
const found = crossInstallPackages([
{
label: "p",
files: [
"node_modules/a/node_modules/zod/index.d.ts",
"clients/web/node_modules/zod/index.d.ts",
],
},
]);
assert.deepEqual(
[...found.get("zod").get("p").get(".")],
["node_modules/a/node_modules/zod"],
);
});