From Copilot's fourteenth pass of #1962. `hasReadableLockShape` and the diagnostic it drives both promise "lockfileVersion 2+", but the check only looked for a `packages` table with a root entry. A file declaring v1 while carrying such a table was accepted, produced an empty version map, and could make a real skew read as aligned once only one other holder remained — the same fail-open the shape check was added to close, through the half of the contract that wasn't enforced. The declared version is now verified: a finite number, 2 or greater. Absent, non-numeric, and declared-v1-with-packages cases are all in the table. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HDdo1rNRVQnVRRdsqSrvG3
491 lines
17 KiB
JavaScript
491 lines
17 KiB
JavaScript
// Table-driven tests for the pure helpers of the dep-lockstep guard (#1896).
|
|
// One case per rule the guard encodes — the comment names the rule, so a future
|
|
// change that relaxes one is visible as a deleted assertion rather than a quiet
|
|
// behavior shift. 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 {
|
|
findSkew,
|
|
hasReadableLockShape,
|
|
importedPackageNames,
|
|
isSharedSourceFile,
|
|
majorOf,
|
|
packageNameOf,
|
|
partitionSkew,
|
|
sourcesWithNoFiles,
|
|
topLevelLockVersions,
|
|
typeReferencePackageNames,
|
|
} from "./verify-dep-lockstep.mjs";
|
|
|
|
test("isSharedSourceFile: all four TS extensions, not just .ts/.tsx", () => {
|
|
// `.mts`/`.cts` are gated by `verify:format-coverage` and
|
|
// `verify:typecheck-coverage` too. None exist under the shared trees today,
|
|
// so dropping them here would go unnoticed until a new shared dependency
|
|
// arrived through one — the skew this guard exists to catch (Copilot, #1962).
|
|
for (const ext of [".ts", ".tsx", ".mts", ".cts"]) {
|
|
assert.equal(isSharedSourceFile(`core/mcp/thing${ext}`), true, ext);
|
|
assert.equal(
|
|
isSharedSourceFile(`test-servers/src/thing${ext}`),
|
|
true,
|
|
`test-servers ${ext}`,
|
|
);
|
|
}
|
|
});
|
|
|
|
test("isSharedSourceFile: non-TS files and other trees are excluded", () => {
|
|
const rejected = [
|
|
"core/README.md", // not TypeScript
|
|
"core/mcp/data.json",
|
|
"clients/web/src/App.tsx", // a client's own sources resolve from the client
|
|
"scripts/verify-dep-lockstep.mjs",
|
|
"test-servers/configs/modern-http.json",
|
|
// Path-boundary anchoring: a sibling dir whose name merely starts with a
|
|
// shared dir's name must not be swept in.
|
|
"core-internal/thing.ts",
|
|
"test-servers/src-legacy/thing.ts",
|
|
];
|
|
for (const file of rejected)
|
|
assert.equal(isSharedSourceFile(file), false, file);
|
|
});
|
|
|
|
test("sourcesWithNoFiles: each configured source must contribute (Copilot, #1962)", () => {
|
|
const dirs = ["core", "test-servers/src"];
|
|
const named = ["vitest.shared.mts"];
|
|
const complete = [
|
|
"core/mcp/a.ts",
|
|
"test-servers/src/b.ts",
|
|
"vitest.shared.mts",
|
|
];
|
|
assert.deepEqual(sourcesWithNoFiles(complete, dirs, named), []);
|
|
|
|
// The failure an aggregate count can't see: `core/` moved, but the other two
|
|
// sources keep `files.length` nonzero, so the guard would derive candidates
|
|
// from an incomplete set and pass on skew it should catch.
|
|
assert.deepEqual(
|
|
sourcesWithNoFiles(
|
|
["test-servers/src/b.ts", "vitest.shared.mts"],
|
|
dirs,
|
|
named,
|
|
),
|
|
["core"],
|
|
);
|
|
assert.deepEqual(
|
|
sourcesWithNoFiles(["core/mcp/a.ts", "test-servers/src/b.ts"], dirs, named),
|
|
["vitest.shared.mts"],
|
|
);
|
|
assert.deepEqual(sourcesWithNoFiles([], dirs, named), [
|
|
"core",
|
|
"test-servers/src",
|
|
"vitest.shared.mts",
|
|
]);
|
|
});
|
|
|
|
test("sourcesWithNoFiles: a prefix sibling does not vouch for a dir", () => {
|
|
// `core-internal/` starts with `core` but is not it — the boundary check has
|
|
// to be on a path separator, or a renamed dir would look present.
|
|
assert.deepEqual(sourcesWithNoFiles(["core-internal/a.ts"], ["core"], []), [
|
|
"core",
|
|
]);
|
|
});
|
|
|
|
test("packageNameOf: bare names, scopes, and subpaths", () => {
|
|
const cases = [
|
|
["zod", "zod"],
|
|
["zod/v4", "zod"], // subpath dropped — one package, one version
|
|
["@modelcontextprotocol/client", "@modelcontextprotocol/client"],
|
|
["@modelcontextprotocol/client/core", "@modelcontextprotocol/client"],
|
|
["react-dom/client", "react-dom"],
|
|
];
|
|
for (const [input, expected] of cases)
|
|
assert.equal(packageNameOf(input), expected, input);
|
|
});
|
|
|
|
test("packageNameOf: non-packages are rejected", () => {
|
|
// Relative/absolute paths, built-ins with and without the `node:` prefix,
|
|
// protocol specifiers, and prose that follows the word `from` in a comment.
|
|
const rejected = [
|
|
"./foo",
|
|
"../core/mcp",
|
|
"/abs/path",
|
|
"fs",
|
|
"path",
|
|
"node:crypto",
|
|
"node:test",
|
|
"file:",
|
|
"data:text/plain,x",
|
|
"cwd omitted",
|
|
"",
|
|
];
|
|
for (const input of rejected)
|
|
assert.equal(packageNameOf(input), null, JSON.stringify(input));
|
|
assert.equal(packageNameOf(undefined), null);
|
|
});
|
|
|
|
test("importedPackageNames: CommonJS and awkward dynamic-import forms (Copilot, #1962)", () => {
|
|
// Under-approximating is the dangerous direction: a package the scan misses
|
|
// never enters the candidate set, so its skew passes the guard silently.
|
|
// `.cts` sources in the shared trees use `import x = require(…)` as ordinary
|
|
// syntax, and a dynamic import may carry import attributes or a static
|
|
// template literal — none of which the original three patterns matched.
|
|
const source = `
|
|
import express = require("express");
|
|
const yaml = require("yaml");
|
|
const a = await import("undici", { with: { type: "json" } });
|
|
const b = await import(\`jose\`);
|
|
`;
|
|
assert.deepEqual([...importedPackageNames(source)].sort(), [
|
|
"express",
|
|
"jose",
|
|
"undici",
|
|
"yaml",
|
|
]);
|
|
});
|
|
|
|
test("importedPackageNames: comment trivia between tokens (Copilot, #1962)", () => {
|
|
// TypeScript allows a comment anywhere whitespace is legal, so all of these
|
|
// are valid imports. Missing one is the dangerous direction: the package
|
|
// never enters the candidate set and its skew passes the guard silently.
|
|
const source = `
|
|
import { a } from /* explanation */ "express";
|
|
const b = await import(/* webpackIgnore: true */ "undici");
|
|
const c = require(/* lazy */ "yaml");
|
|
import /* side effect */ "pino";
|
|
`;
|
|
assert.deepEqual([...importedPackageNames(source)].sort(), [
|
|
"express",
|
|
"pino",
|
|
"undici",
|
|
"yaml",
|
|
]);
|
|
});
|
|
|
|
test("importedPackageNames: line-comment trivia, not just block (Copilot, #1962)", () => {
|
|
// `//` runs to end-of-line and is legal in every position a block comment is,
|
|
// so a specifier can sit on the next line and these are still valid imports.
|
|
const source = [
|
|
"import { a } from // reason",
|
|
' "express";',
|
|
"const b = await import(// lazy",
|
|
' "undici");',
|
|
"const c = require(// lazy",
|
|
' "yaml");',
|
|
].join("\n");
|
|
assert.deepEqual([...importedPackageNames(source)].sort(), [
|
|
"express",
|
|
"undici",
|
|
"yaml",
|
|
]);
|
|
});
|
|
|
|
test("importedPackageNames: static, side-effect, and dynamic forms; builtins and relatives dropped", () => {
|
|
const source = `
|
|
import { z } from "zod/v4";
|
|
export type { Foo } from '@modelcontextprotocol/core';
|
|
import "./side-effect.css";
|
|
import "pino";
|
|
const mod = await import("chokidar");
|
|
import fs from "node:fs";
|
|
import { helper } from "../local/helper";
|
|
`;
|
|
assert.deepEqual([...importedPackageNames(source)].sort(), [
|
|
"@modelcontextprotocol/core",
|
|
"chokidar",
|
|
"pino",
|
|
"zod",
|
|
]);
|
|
});
|
|
|
|
test("importedPackageNames: triple-slash type references count (Copilot, #1962)", () => {
|
|
// A `/// <reference types="x" />` pulls in declarations exactly like an
|
|
// import, but TypeScript reports it in `typeReferenceDirectives`, not
|
|
// `importedFiles` — so reading only the latter let a referenced package skew
|
|
// unseen. `path` references name a file, not a package, and are ignored.
|
|
const source = [
|
|
'/// <reference types="node" />',
|
|
'/// <reference types="express" />',
|
|
'/// <reference path="./local.d.ts" />',
|
|
'import { z } from "zod";',
|
|
].join("\n");
|
|
assert.deepEqual([...importedPackageNames(source)].sort(), [
|
|
"@types/express",
|
|
"@types/node",
|
|
"express",
|
|
"node",
|
|
"zod",
|
|
]);
|
|
});
|
|
|
|
test("typeReferencePackageNames: both the bare and the @types form (Copilot, #1962)", () => {
|
|
// The directive names a *type*, not a package: `node` resolves to
|
|
// `@types/node`, while a package shipping its own declarations resolves to
|
|
// itself. Returning both over-approximates, the safe direction — whichever
|
|
// isn't installed drops out downstream.
|
|
assert.deepEqual(typeReferencePackageNames("node"), ["node", "@types/node"]);
|
|
// Scoped names mangle with a double underscore, TypeScript's convention.
|
|
assert.deepEqual(typeReferencePackageNames("@scope/pkg"), [
|
|
"@scope/pkg",
|
|
"@types/scope__pkg",
|
|
]);
|
|
assert.deepEqual(typeReferencePackageNames("./relative"), []);
|
|
assert.deepEqual(typeReferencePackageNames(""), []);
|
|
});
|
|
|
|
test("importedPackageNames: prose in comments never becomes a package (Copilot, #1962)", () => {
|
|
// The regex scan this replaced could not tell code from a comment, so
|
|
// `// adapted from "react"` added `react` to the candidate set — and if that
|
|
// installed package were skewed, an unrelated comment would fail `validate`.
|
|
// These use REAL package names, which is the case the old prose test missed:
|
|
// it only passed because `cwd omitted` isn't a valid package name.
|
|
const source = `
|
|
// adapted from "react"
|
|
/** Mirrors the behavior of "express", see require("yaml") below. */
|
|
/** The excluded set derived from \\\`hono\\\`-style paths. */
|
|
// const disabled = await import("undici");
|
|
import { z } from "zod";
|
|
`;
|
|
assert.deepEqual([...importedPackageNames(source)], ["zod"]);
|
|
});
|
|
|
|
test("importedPackageNames: a specifier inside a string literal is not an import", () => {
|
|
const source = `
|
|
const msg = 'run require("chokidar") to load it';
|
|
const re = /"jose"/;
|
|
import { z } from "zod";
|
|
`;
|
|
assert.deepEqual([...importedPackageNames(source)], ["zod"]);
|
|
});
|
|
|
|
test("topLevelLockVersions: nested duplicates are ignored", () => {
|
|
// A nested `node_modules/a/node_modules/b` is npm resolving a transitive
|
|
// conflict *inside* one install — routine, and not the cross-install skew
|
|
// this guard is about (`cosmiconfig`'s yaml@1 alongside the top-level yaml@2
|
|
// is the live example).
|
|
const lock = {
|
|
packages: {
|
|
"": { name: "root" },
|
|
"node_modules/zod": { version: "4.4.3" },
|
|
"node_modules/yaml": { version: "2.9.0" },
|
|
"node_modules/cosmiconfig/node_modules/yaml": { version: "1.10.3" },
|
|
"node_modules/@modelcontextprotocol/client": { version: "2.0.0-beta.5" },
|
|
"node_modules/no-version": { resolved: "https://example.test/x.tgz" },
|
|
},
|
|
};
|
|
assert.deepEqual([...topLevelLockVersions(lock)].sort(), [
|
|
["@modelcontextprotocol/client", "2.0.0-beta.5"],
|
|
["yaml", "2.9.0"],
|
|
["zod", "4.4.3"],
|
|
]);
|
|
});
|
|
|
|
test("topLevelLockVersions: a malformed or empty lockfile yields nothing", () => {
|
|
// Safe as a pure helper *because* `hasReadableLockShape` rejects these before
|
|
// any comparison — an empty map reaching `findSkew` is the fail-open path.
|
|
for (const lock of [undefined, null, {}, { packages: {} }])
|
|
assert.equal(topLevelLockVersions(lock).size, 0);
|
|
});
|
|
|
|
test("hasReadableLockShape: only a v2+ packages table with a root entry (Copilot, #1962)", () => {
|
|
assert.equal(
|
|
hasReadableLockShape({ lockfileVersion: 3, packages: { "": {} } }),
|
|
true,
|
|
);
|
|
assert.equal(
|
|
hasReadableLockShape({
|
|
lockfileVersion: 2,
|
|
packages: { "": {}, "node_modules/zod": { version: "4.4.3" } },
|
|
}),
|
|
true,
|
|
);
|
|
const rejected = [
|
|
undefined,
|
|
null,
|
|
{},
|
|
{ lockfileVersion: 3, packages: null },
|
|
{ lockfileVersion: 3, packages: [] }, // an array has no `""` key
|
|
{ lockfileVersion: 3, packages: {} }, // no root entry
|
|
{
|
|
lockfileVersion: 3,
|
|
packages: { "node_modules/zod": { version: "4.4.3" } },
|
|
},
|
|
{ lockfileVersion: 1, dependencies: { zod: { version: "4.4.3" } } },
|
|
// A declared v1 carrying a `packages` table: the version is checked, not
|
|
// inferred from the key's presence, so this is rejected rather than
|
|
// half-trusted into an empty (fail-open) version map.
|
|
{ lockfileVersion: 1, packages: { "": {} } },
|
|
{ packages: { "": {} } }, // no declared version at all
|
|
{ lockfileVersion: "3", packages: { "": {} } }, // not a number
|
|
];
|
|
for (const lock of rejected)
|
|
assert.equal(hasReadableLockShape(lock), false, JSON.stringify(lock));
|
|
});
|
|
|
|
test("findSkew: reports a package held at two versions", () => {
|
|
const installs = [
|
|
{ dir: ".", versions: new Map([["zod", "4.3.6"]]) },
|
|
{ dir: "clients/web", versions: new Map([["zod", "4.4.3"]]) },
|
|
{ dir: "clients/cli", versions: new Map([["zod", "4.4.3"]]) },
|
|
];
|
|
assert.deepEqual(findSkew(new Set(["zod"]), installs), [
|
|
{
|
|
name: "zod",
|
|
holders: [
|
|
{ dir: ".", version: "4.3.6" },
|
|
{ dir: "clients/web", version: "4.4.3" },
|
|
{ dir: "clients/cli", version: "4.4.3" },
|
|
],
|
|
},
|
|
]);
|
|
});
|
|
|
|
test("findSkew: agreement and single-install packages are not skew", () => {
|
|
const installs = [
|
|
{
|
|
dir: ".",
|
|
versions: new Map([
|
|
["zod", "4.4.3"],
|
|
["express", "5.2.1"],
|
|
]),
|
|
},
|
|
{ dir: "clients/web", versions: new Map([["zod", "4.4.3"]]) },
|
|
];
|
|
// `express` lives in one install only, so it cannot skew — a package absent
|
|
// from a client is not a finding.
|
|
assert.deepEqual(findSkew(new Set(["zod", "express"]), installs), []);
|
|
});
|
|
|
|
test("findSkew: a candidate in no lockfile is inert", () => {
|
|
// `@inspector/core` is a build-time alias, not a package; the scan picks it
|
|
// up and it must drop out here rather than error.
|
|
const installs = [
|
|
{ dir: ".", versions: new Map([["zod", "4.4.3"]]) },
|
|
{ dir: "clients/web", versions: new Map([["zod", "4.4.3"]]) },
|
|
];
|
|
assert.deepEqual(findSkew(new Set(["@inspector/core"]), installs), []);
|
|
});
|
|
|
|
test("findSkew: results are sorted by package name", () => {
|
|
const installs = [
|
|
{
|
|
dir: ".",
|
|
versions: new Map([
|
|
["zod", "1.0.0"],
|
|
["hono", "1.0.0"],
|
|
]),
|
|
},
|
|
{
|
|
dir: "clients/web",
|
|
versions: new Map([
|
|
["zod", "2.0.0"],
|
|
["hono", "2.0.0"],
|
|
]),
|
|
},
|
|
];
|
|
assert.deepEqual(
|
|
findSkew(new Set(["zod", "hono"]), installs).map((s) => s.name),
|
|
["hono", "zod"],
|
|
);
|
|
});
|
|
|
|
test("partitionSkew: the allowlist is by name, not by version pair", () => {
|
|
// So an ordinary patch float within a tolerated package does not churn the
|
|
// allowlist, while any *unlisted* package that starts skewing still fails.
|
|
const skewed = [
|
|
{
|
|
name: "react",
|
|
holders: [
|
|
{ dir: ".", version: "19.2.7" },
|
|
{ dir: "clients/web", version: "19.2.8" },
|
|
],
|
|
},
|
|
{
|
|
name: "zod",
|
|
holders: [
|
|
{ dir: ".", version: "4.3.6" },
|
|
{ dir: "clients/web", version: "4.4.3" },
|
|
],
|
|
},
|
|
];
|
|
const tolerated = new Map([["react", "shallow interfaces"]]);
|
|
const { failures, ignored } = partitionSkew(skewed, tolerated);
|
|
assert.deepEqual(
|
|
failures.map((s) => s.name),
|
|
["zod"],
|
|
);
|
|
assert.deepEqual(
|
|
ignored.map((s) => s.name),
|
|
["react"],
|
|
);
|
|
});
|
|
|
|
test("partitionSkew: deny by default — nothing tolerated fails everything", () => {
|
|
const skewed = [{ name: "zod", holders: [{ dir: ".", version: "1.0.0" }] }];
|
|
assert.equal(partitionSkew(skewed, new Map()).failures.length, 1);
|
|
});
|
|
|
|
test("partitionSkew: the allowlist tolerates skew only within a major (Copilot, #1962)", () => {
|
|
// Each rationale establishes that a patch/minor difference is benign; that is
|
|
// not evidence a React 18-vs-19 split is, so a listed package still fails
|
|
// across a major boundary.
|
|
const tolerated = new Map([["react", "shallow interfaces"]]);
|
|
const withinMajor = [
|
|
{
|
|
name: "react",
|
|
holders: [
|
|
{ dir: ".", version: "19.2.7" },
|
|
{ dir: "clients/web", version: "19.2.8" },
|
|
],
|
|
},
|
|
];
|
|
const acrossMajor = [
|
|
{
|
|
name: "react",
|
|
holders: [
|
|
{ dir: ".", version: "18.3.1" },
|
|
{ dir: "clients/web", version: "19.2.8" },
|
|
],
|
|
},
|
|
];
|
|
assert.equal(partitionSkew(withinMajor, tolerated).failures.length, 0);
|
|
assert.equal(partitionSkew(withinMajor, tolerated).ignored.length, 1);
|
|
assert.equal(partitionSkew(acrossMajor, tolerated).failures.length, 1);
|
|
assert.equal(partitionSkew(acrossMajor, tolerated).ignored.length, 0);
|
|
});
|
|
|
|
test("partitionSkew: an unparseable version can't be proven same-major, so it fails", () => {
|
|
const tolerated = new Map([["react", "shallow interfaces"]]);
|
|
const skewed = [
|
|
{
|
|
name: "react",
|
|
holders: [
|
|
{ dir: ".", version: "19.2.7" },
|
|
{ dir: "clients/web", version: "next" },
|
|
],
|
|
},
|
|
];
|
|
assert.equal(partitionSkew(skewed, tolerated).failures.length, 1);
|
|
});
|
|
|
|
test("majorOf: prerelease and build metadata are irrelevant", () => {
|
|
const cases = [
|
|
["4.4.3", "4"],
|
|
["2.0.0-beta.5", "2"],
|
|
["19.2.8", "19"],
|
|
["1.10.3+build.7", "1"],
|
|
];
|
|
for (const [input, expected] of cases)
|
|
assert.equal(majorOf(input), expected, input);
|
|
for (const bad of ["next", "", undefined, null, "v4.4.3"])
|
|
assert.equal(majorOf(bad), null, JSON.stringify(bad));
|
|
});
|
|
|
|
test("isSharedSourceFile: individually-named shared files are included (Copilot, #1962)", () => {
|
|
// `vitest.shared.mts` is root-owned, imported by every client's vitest
|
|
// config, and already treated as shared by `verify:typecheck-coverage`. It
|
|
// imports only Node built-ins today, which is why omitting it would go
|
|
// unnoticed until a third-party import appeared there and skewed.
|
|
assert.equal(isSharedSourceFile("vitest.shared.mts"), true);
|
|
// Still anchored: a same-named file nested elsewhere is not the shared one.
|
|
assert.equal(isSharedSourceFile("clients/web/vitest.shared.mts"), false);
|
|
});
|