chore: widen verify:dep-lockstep to declared cross-install copies (#2226)
`clients/cli` resolved `@types/node` 24.13.1 against the root's 24.13.3. Neither client declares it — #2196 consolidated it to the root — so the cli copy arrived transitively via `@types/express` and was constrained by no range of ours. `verify:dep-lockstep` reported OK and was right to: it compares only packages that reach one `tsc` **program** from two installs, and no one program sees both copies. Fixes the instance and the class, which the issue notes are not exclusive. **The class.** The guard grows a second tier alongside the program-derived one, asking the weaker but broader question `AGENTS.md`'s "one version per install-crossing dependency" rule actually states: does a package this repo declares anywhere resolve to two versions across our installs at all? The candidate set is every name in any install's `dependencies`/`devDependencies` that more than one install holds a **top-level** copy of — 17 packages today. Nested copies are excluded: one exists because some dependency asked for a different version, so it is that dependency's range, not ours. Neither tier subsumes the other. The program tier sees a copy no manifest names (the `@modelcontextprotocol/sdk` case, arriving through another package's `.d.ts`). The declared tier sees a transitive copy no program loads (this issue), two *clients* disagreeing with no root copy involved (`@types/react`, web against tui), and the peer shadows `AGENTS.md` calls out as ungated — `eslint`, `typescript`, `vitest`. Deny-by-default, with its own empty `TOLERATED_DECLARED_SKEW` and the same within-a-major rule, now stated once in a shared `toleratesSkew` so the two tiers cannot drift. **The instance, and three others the new tier surfaced.** All four were patch-level: - `@types/node` — cli 24.13.1 vs root 24.13.3. Transitive, so it takes an `overrides` entry in `clients/cli` (the repo's sanctioned lever for a transitive pin), at the root's own `^24.12.4` rather than a frozen number. - `@types/react` — web 19.2.18 vs tui 19.2.17. - `react` — web 19.2.8 vs root/tui 19.2.7. - `@vitejs/plugin-react` — web 6.0.5 vs root 6.0.2. The last three are `npm update` in the stale install: it moves the lockfile within the declared range without widening a range, which matters for the root `react ^19.0.0` that `ink`'s externalized-bundle exemption depends on. No declaration or range changed. Tests: 4 end-to-end cases (the fixture gains manifest deps and a client-side `solo` install, so the skew is real and no program holds both copies) and 13 helper cases. Closes #2226 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J86a6vMcA9csPtdsg7ZcCZ Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
parent
ed2a188069
commit
1e6bf6425a
3 changed files with 583 additions and 27 deletions
|
|
@ -82,6 +82,9 @@ function makeFixture({
|
|||
webTsconfig,
|
||||
rootNestedInner,
|
||||
cliDeps,
|
||||
rootManifestDeps,
|
||||
webManifestDeps,
|
||||
webSolo,
|
||||
} = {}) {
|
||||
// realpath matters: on macOS `tmpdir()` is `/var/...`, a symlink to
|
||||
// `/private/var/...`. The guard only runs `main()` when `import.meta.url`
|
||||
|
|
@ -101,6 +104,7 @@ function makeFixture({
|
|||
|
||||
write("package.json", {
|
||||
name: "fixture",
|
||||
...(rootManifestDeps ? { dependencies: rootManifestDeps } : {}),
|
||||
scripts: scripts ?? {
|
||||
validate: "npm run verify:format-coverage && npm run verify:dep-lockstep",
|
||||
"verify:format-coverage": "node scripts/verify-format-coverage.mjs",
|
||||
|
|
@ -121,6 +125,7 @@ function makeFixture({
|
|||
);
|
||||
write("clients/web/package.json", {
|
||||
name: "web",
|
||||
...(webManifestDeps ? { devDependencies: webManifestDeps } : {}),
|
||||
scripts: webScripts ?? { typecheck: "tsc --noEmit -p tsconfig.json" },
|
||||
});
|
||||
write(
|
||||
|
|
@ -166,6 +171,20 @@ function makeFixture({
|
|||
stub(installRoot, versions, "inner", "export type Inner = number;\n");
|
||||
}
|
||||
stub("", root, "solo", "export type Solo = string;\n");
|
||||
// `solo` under the CLIENT install as well. Nothing in `clients/web/src`
|
||||
// imports it, so the program still resolves exactly one copy (the root's, via
|
||||
// the shared `core/`) — the declared tier's whole point is that it sees this
|
||||
// second copy anyway (#2226).
|
||||
const webLockDeps = { ...web };
|
||||
if (webSolo) {
|
||||
stub(
|
||||
"clients/web/",
|
||||
{ solo: webSolo },
|
||||
"solo",
|
||||
"export type Solo = string;\n",
|
||||
);
|
||||
webLockDeps.solo = webSolo;
|
||||
}
|
||||
|
||||
// The root reaches `inner` only through a copy NESTED under `outer`, at a
|
||||
// version its top-level entry does not carry. npm's own conflict resolution,
|
||||
|
|
@ -182,7 +201,7 @@ function makeFixture({
|
|||
}
|
||||
|
||||
write("package-lock.json", lock(rootLockDeps));
|
||||
write("clients/web/package-lock.json", rawWebLock ?? lock(web));
|
||||
write("clients/web/package-lock.json", rawWebLock ?? lock(webLockDeps));
|
||||
|
||||
// A second client, to prove a third install's copy is not dragged into a
|
||||
// comparison it never took part in. Its program spans only its own install.
|
||||
|
|
@ -503,3 +522,75 @@ test("main: exits 1 when the root validate no longer runs the sibling guard", ()
|
|||
},
|
||||
);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// The declared tier (#2226) — skew across installs that no ONE program sees.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
test("main: a DECLARED package skewed across installs fails even when no program holds both copies (#2226)", () => {
|
||||
// The exact shape the issue reported. `solo` is installed under the root and
|
||||
// under `clients/web`, and both manifests name it — but only the shared
|
||||
// `core/` imports it, so the client's program resolves the root's copy alone
|
||||
// and the program tier is silent, correctly. The declared tier is what sees
|
||||
// it, which is the whole reason it exists: cli's `@types/node` was hoisted in
|
||||
// via `@types/express` and no program ever met the root's copy.
|
||||
withFixture(
|
||||
{
|
||||
rootManifestDeps: { solo: "^7.0.0" },
|
||||
webManifestDeps: { solo: "^7.0.0" },
|
||||
webSolo: "7.0.0",
|
||||
},
|
||||
(dir) => {
|
||||
const { status, out } = runGuard(dir);
|
||||
assert.equal(status, 1, out);
|
||||
assert.match(out, /1 declared dependency resolves/);
|
||||
assert.match(out, /7\.8\.9\s+\(\.\/node_modules\/solo\)/);
|
||||
assert.match(out, /7\.0\.0\s+\(clients\/web\/node_modules\/solo\)/);
|
||||
// The program tier must NOT have claimed it — its message names a program.
|
||||
assert.doesNotMatch(out, /in clients\/web\/tsconfig\.json/);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test("main: a declared package held at the same version in both installs passes and is counted", () => {
|
||||
withFixture(
|
||||
{
|
||||
rootManifestDeps: { solo: "^7.0.0" },
|
||||
webManifestDeps: { solo: "^7.0.0" },
|
||||
webSolo: "7.8.9",
|
||||
},
|
||||
(dir) => {
|
||||
const { status, out } = runGuard(dir);
|
||||
assert.equal(status, 0, out);
|
||||
assert.match(out, /1 declared dependencies agree/);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test("main: an installed package NO manifest declares is not a declared-tier candidate", () => {
|
||||
// `outer` and `inner` sit in both installs in every fixture, but no manifest
|
||||
// names them. The declared tier's boundary is what the repo declares, so its
|
||||
// candidate count is zero here — the program tier is what covers those.
|
||||
withFixture({}, (dir) => {
|
||||
const { status, out } = runGuard(dir);
|
||||
assert.equal(status, 0, out);
|
||||
assert.match(out, /0 declared dependencies agree/);
|
||||
});
|
||||
});
|
||||
|
||||
test("main: a declared package only ONE install holds cannot skew", () => {
|
||||
// `solo` is declared at the root and listed in the root lockfile only. One
|
||||
// holder is not an install-crossing dependency, so its version is nobody
|
||||
// else's business.
|
||||
withFixture(
|
||||
{
|
||||
rootManifestDeps: { solo: "^7.0.0" },
|
||||
webDeps: { outer: "1.2.3", inner: "4.5.6" },
|
||||
},
|
||||
(dir) => {
|
||||
const { status, out } = runGuard(dir);
|
||||
assert.equal(status, 0, out);
|
||||
assert.match(out, /0 declared dependencies agree/);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -44,6 +44,40 @@
|
|||
// packages verified to tolerate it (below). A dependency that starts skewing
|
||||
// fails `validate` and forces a decision, rather than surfacing months later as
|
||||
// an unexplained OOM.
|
||||
//
|
||||
// ## The second tier: declared packages, whatever any program loads (#2226)
|
||||
//
|
||||
// The program-derived tier above is deliberately narrow — it asks only "can two
|
||||
// structurally-distinct copies of a type meet inside ONE `tsc` program", because
|
||||
// that is the question the #1896 heap exhaustion turned on. That narrowness is a
|
||||
// real blind spot, and `AGENTS.md` documented it before anything measured it:
|
||||
// `clients/cli` resolved `@types/node` 24.13.1 against the root's 24.13.3, and
|
||||
// this guard reported OK — correctly, since no one program sees both copies.
|
||||
//
|
||||
// So a SECOND tier runs alongside it, asking the weaker but broader question
|
||||
// `AGENTS.md`'s "one version per install-crossing dependency" rule actually
|
||||
// states: does a package that this repo DECLARES somewhere resolve to two
|
||||
// different versions across our installs at all? The candidate set is every name
|
||||
// in any install's `dependencies`/`devDependencies` (root or client) that more
|
||||
// than one install holds a top-level copy of — 17 packages today, against the
|
||||
// program tier's much smaller set.
|
||||
//
|
||||
// The two tiers are complementary and neither subsumes the other:
|
||||
//
|
||||
// • The program tier sees a copy NO manifest names — the `@modelcontextprotocol/sdk`
|
||||
// case, whose `.d.ts` files arrive only through another package's types.
|
||||
// • The declared tier sees a copy that arrives TRANSITIVELY into one install
|
||||
// and is therefore constrained by no range of ours: cli's `@types/node` is
|
||||
// hoisted via `@types/express`, so nothing pulled it forward when the root
|
||||
// moved. It also sees two CLIENTS disagreeing (`@types/react` in web against
|
||||
// tui), which no single program can, and the tool copies `AGENTS.md` calls
|
||||
// out as ungated — `eslint`, `typescript`, `vitest` — which are peer shadows
|
||||
// that no `tsc` program loads at all.
|
||||
//
|
||||
// The declared tier is the weaker signal, so it is the one to reach for an
|
||||
// allowlist entry on. It is still deny-by-default: the cost of a false alarm is
|
||||
// one `npm update <pkg>` in the stale install, and the cost of a miss is the
|
||||
// class of bug that reproduces on one machine and not another.
|
||||
|
||||
import { readFileSync, existsSync, readdirSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
|
|
@ -97,6 +131,17 @@ const repoRoot = path.resolve(
|
|||
// a better basis for a rationale than a pre-emptive entry.
|
||||
const TOLERATED_SKEW = new Map();
|
||||
|
||||
// The declared tier's allowlist, same shape and same within-a-major rule as
|
||||
// TOLERATED_SKEW above, and empty for the same reason: an entry is worth writing
|
||||
// only with a real version pair in hand.
|
||||
//
|
||||
// Note what is deliberately NOT listed. `react` and `react-dom` are pinned per
|
||||
// client on purpose (`AGENTS.md`, so a client's renderer and the React it calls
|
||||
// into come from one install) — but "two copies by design" is not "two versions
|
||||
// by design", and the rule that survives is still one version everywhere. The
|
||||
// same holds for `vite` and `tsup`, declared by several clients each.
|
||||
const TOLERATED_DECLARED_SKEW = new Map();
|
||||
|
||||
/**
|
||||
* Installed versions in a parsed lockfile, keyed by the **install path** npm
|
||||
* writes — `node_modules/zod`, `node_modules/a/node_modules/zod`.
|
||||
|
|
@ -210,11 +255,99 @@ export function majorOf(version) {
|
|||
* listed package whose holders disagree on major is still a failure.
|
||||
*/
|
||||
export function partitionSkew(skewed, tolerated = TOLERATED_SKEW) {
|
||||
const isTolerated = (s) => {
|
||||
if (!tolerated.has(s.name)) return false;
|
||||
const majors = new Set(skewHolders(s).map((h) => majorOf(h.version)));
|
||||
return majors.size === 1 && !majors.has(null);
|
||||
const isTolerated = (s) => toleratesSkew(s.name, skewHolders(s), tolerated);
|
||||
return {
|
||||
failures: skewed.filter((s) => !isTolerated(s)),
|
||||
ignored: skewed.filter(isTolerated),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether `tolerated` excuses this package's skew: it must be listed, AND its
|
||||
* holders must agree on major. Shared by both tiers so the within-a-major rule
|
||||
* cannot be stated twice and drift — a listed package that splits across a major
|
||||
* fails in either tier.
|
||||
*/
|
||||
export function toleratesSkew(name, holders, tolerated) {
|
||||
if (!tolerated.has(name)) return false;
|
||||
const majors = new Set(holders.map((h) => majorOf(h.version)));
|
||||
return majors.size === 1 && !majors.has(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Every package name any install declares, across `dependencies` and
|
||||
* `devDependencies`.
|
||||
*
|
||||
* `manifests` is `[{ dir, manifest }]`. Both fields count because the boundary
|
||||
* this tier polices is "does the repo name it", not "does it ship": a
|
||||
* devDependency skew is exactly the `@types/*` and toolchain case #2226 is
|
||||
* about. `peerDependencies` and `optionalDependencies` are NOT declarations of
|
||||
* what this repo installs — a peer range is a constraint on the consumer, and
|
||||
* the copy npm auto-installs to satisfy one is caught anyway, because it lands
|
||||
* top-level in an install whose sibling declares the same name.
|
||||
*/
|
||||
export function declaredPackages(manifests) {
|
||||
const names = new Set();
|
||||
for (const { manifest } of manifests)
|
||||
for (const field of ["dependencies", "devDependencies"])
|
||||
for (const name of Object.keys(manifest?.[field] ?? {})) names.add(name);
|
||||
return names;
|
||||
}
|
||||
|
||||
/**
|
||||
* An install's TOP-LEVEL installed versions — `node_modules/<name>`, with
|
||||
* nested copies (`node_modules/a/node_modules/<name>`) excluded.
|
||||
*
|
||||
* Top-level is the right depth here: it is what the install's own code resolves,
|
||||
* and it is the single copy a `<name>` range in that manifest governs. A nested
|
||||
* copy exists precisely because some dependency asked for a different version,
|
||||
* so it is constrained by that dependency's range rather than by anything we
|
||||
* own — reporting it would fail the gate on a decision that is not ours to make.
|
||||
* (The program tier prices nested copies, because a program that actually loads
|
||||
* one is a different question.)
|
||||
*/
|
||||
export function topLevelVersions(lock) {
|
||||
const versions = new Map();
|
||||
for (const [entryPath, version] of lockVersionsByPath(lock)) {
|
||||
const rest = entryPath.slice("node_modules/".length);
|
||||
if (rest.includes("node_modules/")) continue;
|
||||
versions.set(rest, version);
|
||||
}
|
||||
return versions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Declared packages whose top-level copies disagree across installs.
|
||||
*
|
||||
* `declared` is {@link declaredPackages}' output and `versions` maps an install
|
||||
* dir to that install's {@link topLevelVersions}. An install that holds no
|
||||
* top-level copy of a name simply is not a holder — absence is not skew, and a
|
||||
* package only one install holds cannot cross an install boundary at all.
|
||||
*
|
||||
* Returns `[{ name, holders: [{ dir, version }] }]` sorted by name, holders in
|
||||
* install order.
|
||||
*/
|
||||
export function findDeclaredSkew(declared, versions) {
|
||||
const skewed = [];
|
||||
for (const name of [...declared].sort()) {
|
||||
const holders = [];
|
||||
for (const [dir, byName] of versions) {
|
||||
const version = byName.get(name);
|
||||
if (version !== undefined) holders.push({ dir, version });
|
||||
}
|
||||
if (holders.length < 2) continue;
|
||||
if (new Set(holders.map((h) => h.version)).size > 1)
|
||||
skewed.push({ name, holders });
|
||||
}
|
||||
return skewed;
|
||||
}
|
||||
|
||||
/** {@link partitionSkew} for the declared tier's `{ name, holders }` entries. */
|
||||
export function partitionDeclaredSkew(
|
||||
skewed,
|
||||
tolerated = TOLERATED_DECLARED_SKEW,
|
||||
) {
|
||||
const isTolerated = (s) => toleratesSkew(s.name, s.holders, tolerated);
|
||||
return {
|
||||
failures: skewed.filter((s) => !isTolerated(s)),
|
||||
ignored: skewed.filter(isTolerated),
|
||||
|
|
@ -495,13 +628,86 @@ export function main() {
|
|||
process.exit(1);
|
||||
}
|
||||
|
||||
// ---- Second tier: every DECLARED package held by more than one install.
|
||||
// Independent of what any `tsc` program loads, so it sees the transitive and
|
||||
// peer-shadow copies the program tier structurally cannot (#2226).
|
||||
const manifests = dirs.map((dir) => {
|
||||
const file = path.join(repoRoot, dir, "package.json");
|
||||
try {
|
||||
return { dir, manifest: JSON.parse(readFileSync(file, "utf8")) };
|
||||
} catch (cause) {
|
||||
throw new Error(
|
||||
`verify:dep-lockstep — could not parse ${dir}/package.json.`,
|
||||
{ cause },
|
||||
);
|
||||
}
|
||||
});
|
||||
const declaredVersions = new Map(
|
||||
locks.map(({ dir, lock }) => [dir, topLevelVersions(lock)]),
|
||||
);
|
||||
const declared = declaredPackages(manifests);
|
||||
const declaredSkew = findDeclaredSkew(declared, declaredVersions);
|
||||
const { failures: declaredFailures, ignored: declaredIgnored } =
|
||||
partitionDeclaredSkew(declaredSkew);
|
||||
|
||||
if (declaredFailures.length > 0) {
|
||||
console.error(
|
||||
`verify:dep-lockstep — ${declaredFailures.length} declared ${declaredFailures.length === 1 ? "dependency resolves" : "dependencies resolve"} to different versions across installs:\n`,
|
||||
);
|
||||
let anyListed = false;
|
||||
for (const failure of declaredFailures) {
|
||||
const listed = TOLERATED_DECLARED_SKEW.has(failure.name);
|
||||
anyListed ||= listed;
|
||||
console.error(
|
||||
` ${failure.name}${listed ? " (allowlisted — but this is a MAJOR skew)" : ""}`,
|
||||
);
|
||||
for (const { dir, version } of failure.holders)
|
||||
console.error(
|
||||
` ${version} (${dir}/node_modules/${failure.name})`,
|
||||
);
|
||||
}
|
||||
console.error(
|
||||
"\nThese are packages this repo declares somewhere, held at two versions across our installs —" +
|
||||
"\nthe `one version per install-crossing dependency` rule in AGENTS.md. Unlike the check above," +
|
||||
"\nthis tier does not require the copies to meet in one `tsc` program, so it catches a copy that" +
|
||||
"\narrives TRANSITIVELY into one install (cli's `@types/node`, hoisted via `@types/express`, #2226)" +
|
||||
"\nor as a peer shadow no program loads at all.",
|
||||
);
|
||||
console.error(
|
||||
"\nAlign them with `npm update <pkg>` in whichever install is behind — that moves the lockfile" +
|
||||
"\nwithin the declared range, without widening a range as `npm install <pkg>@<version>` would. If a" +
|
||||
"\ntransitive copy will not move, pin it with an `overrides` entry in that install (see" +
|
||||
"\nclients/cli). If the skew is genuinely benign, add it to TOLERATED_DECLARED_SKEW in" +
|
||||
"\nscripts/verify-dep-lockstep.mjs with the reason.",
|
||||
);
|
||||
if (anyListed)
|
||||
console.error(
|
||||
"\nNote: an allowlisted package is tolerated only WITHIN a major version. Align the major.",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const note = ignored.length > 0 ? `, ${ignored.length} tolerated` : "";
|
||||
const declaredNote =
|
||||
declaredIgnored.length > 0 ? `, ${declaredIgnored.length} tolerated` : "";
|
||||
console.log(
|
||||
`verify:dep-lockstep — OK: ${found.size} install-crossing dependencies agree across ${dirs.length} installs${note} ` +
|
||||
`(derived from ${programs.length} tsc programs).`,
|
||||
`(derived from ${programs.length} tsc programs); ` +
|
||||
`${countDeclaredHeld(declared, declaredVersions)} declared dependencies agree across those installs${declaredNote}.`,
|
||||
);
|
||||
}
|
||||
|
||||
/** How many declared packages more than one install holds — the declared tier's candidate count. */
|
||||
export function countDeclaredHeld(declared, versions) {
|
||||
let n = 0;
|
||||
for (const name of declared) {
|
||||
let holders = 0;
|
||||
for (const byName of versions.values()) if (byName.has(name)) holders += 1;
|
||||
if (holders > 1) n += 1;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
// Run only when executed directly (`node scripts/verify-dep-lockstep.mjs`);
|
||||
// importing this file (tests) exposes the pure helpers without running the guard.
|
||||
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href)
|
||||
|
|
|
|||
|
|
@ -13,11 +13,17 @@ import { test } from "node:test";
|
|||
import assert from "node:assert/strict";
|
||||
import {
|
||||
clientProjects,
|
||||
countDeclaredHeld,
|
||||
declaredPackages,
|
||||
findDeclaredSkew,
|
||||
findSkew,
|
||||
hasReadableLockShape,
|
||||
lockVersionsByPath,
|
||||
majorOf,
|
||||
partitionDeclaredSkew,
|
||||
partitionSkew,
|
||||
toleratesSkew,
|
||||
topLevelVersions,
|
||||
} from "./verify-dep-lockstep.mjs";
|
||||
|
||||
test("clientProjects: a `typecheck` script's projects win over references", () => {
|
||||
|
|
@ -402,3 +408,247 @@ test("majorOf: prerelease and build metadata are irrelevant", () => {
|
|||
for (const bad of ["next", "", undefined, null, "v4.4.3"])
|
||||
assert.equal(majorOf(bad), null, JSON.stringify(bad));
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// The declared tier (#2226). One case per rule, same convention as above.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
test("declaredPackages: dependencies and devDependencies from every install", () => {
|
||||
// devDependencies count because the boundary is "does the repo name it", not
|
||||
// "does it ship" — `@types/node` and the toolchain are exactly the case #2226
|
||||
// is about, and all of them are devDependencies.
|
||||
assert.deepEqual(
|
||||
[
|
||||
...declaredPackages([
|
||||
{ dir: ".", manifest: { dependencies: { react: "^19.0.0" } } },
|
||||
{
|
||||
dir: "clients/web",
|
||||
manifest: {
|
||||
dependencies: { "react-dom": "^19.2.4" },
|
||||
devDependencies: { "@types/react": "^19.2.14" },
|
||||
},
|
||||
},
|
||||
]),
|
||||
].sort(),
|
||||
["@types/react", "react", "react-dom"],
|
||||
);
|
||||
});
|
||||
|
||||
test("declaredPackages: peer and optional ranges are not declarations", () => {
|
||||
// A peer range constrains the consumer rather than naming what we install;
|
||||
// the copy npm auto-installs to satisfy one is still caught, because it lands
|
||||
// top-level in an install whose sibling declares the same name.
|
||||
assert.deepEqual(
|
||||
[
|
||||
...declaredPackages([
|
||||
{
|
||||
dir: ".",
|
||||
manifest: {
|
||||
peerDependencies: { react: "^19.0.0" },
|
||||
optionalDependencies: { fsevents: "^2.3.3" },
|
||||
},
|
||||
},
|
||||
]),
|
||||
],
|
||||
[],
|
||||
);
|
||||
});
|
||||
|
||||
test("declaredPackages: a manifest with no dependency fields contributes nothing", () => {
|
||||
assert.equal(
|
||||
declaredPackages([{ dir: "clients/launcher", manifest: { name: "l" } }])
|
||||
.size,
|
||||
0,
|
||||
);
|
||||
});
|
||||
|
||||
test("topLevelVersions: nested copies are excluded", () => {
|
||||
// Top-level is what the install's own code resolves and what a range in that
|
||||
// manifest governs. A nested copy exists because some dependency asked for a
|
||||
// different version, so it is constrained by that dependency, not by us.
|
||||
const versions = topLevelVersions({
|
||||
lockfileVersion: 3,
|
||||
packages: {
|
||||
"": { name: "fixture" },
|
||||
"node_modules/zod": { version: "4.4.3" },
|
||||
"node_modules/outer/node_modules/zod": { version: "3.0.0" },
|
||||
"clients/web/node_modules/zod": { version: "9.9.9" },
|
||||
},
|
||||
});
|
||||
assert.deepEqual([...versions], [["zod", "4.4.3"]]);
|
||||
});
|
||||
|
||||
test("findDeclaredSkew: two installs disagreeing is skew, and every holder is named", () => {
|
||||
const skewed = findDeclaredSkew(
|
||||
new Set(["@types/node"]),
|
||||
new Map([
|
||||
[".", new Map([["@types/node", "24.13.3"]])],
|
||||
["clients/cli", new Map([["@types/node", "24.13.1"]])],
|
||||
["clients/web", new Map([["@types/node", "24.13.3"]])],
|
||||
]),
|
||||
);
|
||||
assert.deepEqual(skewed, [
|
||||
{
|
||||
name: "@types/node",
|
||||
holders: [
|
||||
{ dir: ".", version: "24.13.3" },
|
||||
{ dir: "clients/cli", version: "24.13.1" },
|
||||
{ dir: "clients/web", version: "24.13.3" },
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test("findDeclaredSkew: two CLIENTS disagreeing is skew, with no root copy involved", () => {
|
||||
// No single `tsc` program can see this pair, which is why the program tier
|
||||
// structurally cannot report it (`@types/react`, web against tui).
|
||||
assert.deepEqual(
|
||||
findDeclaredSkew(
|
||||
new Set(["@types/react"]),
|
||||
new Map([
|
||||
[".", new Map()],
|
||||
["clients/web", new Map([["@types/react", "19.2.18"]])],
|
||||
["clients/tui", new Map([["@types/react", "19.2.17"]])],
|
||||
]),
|
||||
).map((s) => s.name),
|
||||
["@types/react"],
|
||||
);
|
||||
});
|
||||
|
||||
test("findDeclaredSkew: one holder cannot skew, and agreement is not skew", () => {
|
||||
assert.deepEqual(
|
||||
findDeclaredSkew(
|
||||
new Set(["only-root", "agreed"]),
|
||||
new Map([
|
||||
[
|
||||
".",
|
||||
new Map([
|
||||
["only-root", "1.0.0"],
|
||||
["agreed", "2.0.0"],
|
||||
]),
|
||||
],
|
||||
["clients/web", new Map([["agreed", "2.0.0"]])],
|
||||
]),
|
||||
),
|
||||
[],
|
||||
);
|
||||
});
|
||||
|
||||
test("findDeclaredSkew: an installed package no manifest declares is not a candidate", () => {
|
||||
assert.deepEqual(
|
||||
findDeclaredSkew(
|
||||
new Set(),
|
||||
new Map([
|
||||
[".", new Map([["undeclared", "1.0.0"]])],
|
||||
["clients/web", new Map([["undeclared", "2.0.0"]])],
|
||||
]),
|
||||
),
|
||||
[],
|
||||
);
|
||||
});
|
||||
|
||||
test("findDeclaredSkew: results are sorted by package name", () => {
|
||||
const two = new Map([
|
||||
[
|
||||
".",
|
||||
new Map([
|
||||
["zod", "1.0.0"],
|
||||
["ajv", "1.0.0"],
|
||||
]),
|
||||
],
|
||||
[
|
||||
"clients/web",
|
||||
new Map([
|
||||
["zod", "2.0.0"],
|
||||
["ajv", "2.0.0"],
|
||||
]),
|
||||
],
|
||||
]);
|
||||
assert.deepEqual(
|
||||
findDeclaredSkew(new Set(["zod", "ajv"]), two).map((s) => s.name),
|
||||
["ajv", "zod"],
|
||||
);
|
||||
});
|
||||
|
||||
test("partitionDeclaredSkew: deny by default", () => {
|
||||
const skewed = [
|
||||
{
|
||||
name: "zod",
|
||||
holders: [
|
||||
{ dir: ".", version: "4.4.3" },
|
||||
{ dir: "w", version: "4.3.6" },
|
||||
],
|
||||
},
|
||||
];
|
||||
const { failures, ignored } = partitionDeclaredSkew(skewed, new Map());
|
||||
assert.deepEqual(
|
||||
failures.map((f) => f.name),
|
||||
["zod"],
|
||||
);
|
||||
assert.deepEqual(ignored, []);
|
||||
});
|
||||
|
||||
test("partitionDeclaredSkew: an allowlisted package tolerates only a within-major skew", () => {
|
||||
const within = {
|
||||
name: "hono",
|
||||
holders: [
|
||||
{ dir: ".", version: "4.1.0" },
|
||||
{ dir: "w", version: "4.2.0" },
|
||||
],
|
||||
};
|
||||
const across = {
|
||||
name: "hono",
|
||||
holders: [
|
||||
{ dir: ".", version: "4.1.0" },
|
||||
{ dir: "w", version: "5.0.0" },
|
||||
],
|
||||
};
|
||||
const allow = new Map([["hono", "reason"]]);
|
||||
assert.deepEqual(partitionDeclaredSkew([within], allow).ignored.length, 1);
|
||||
assert.deepEqual(partitionDeclaredSkew([across], allow).failures.length, 1);
|
||||
});
|
||||
|
||||
test("toleratesSkew: the within-a-major rule is stated once for both tiers", () => {
|
||||
const allow = new Map([["p", "reason"]]);
|
||||
assert.equal(
|
||||
toleratesSkew("p", [{ version: "1.1.0" }, { version: "1.2.0" }], allow),
|
||||
true,
|
||||
);
|
||||
assert.equal(
|
||||
toleratesSkew("p", [{ version: "1.1.0" }, { version: "2.0.0" }], allow),
|
||||
false,
|
||||
);
|
||||
// Unparseable can't be proven same-major, so it fails rather than passes.
|
||||
assert.equal(
|
||||
toleratesSkew("p", [{ version: "1.1.0" }, { version: "next" }], allow),
|
||||
false,
|
||||
);
|
||||
assert.equal(
|
||||
toleratesSkew("other", [{ version: "1.1.0" }, { version: "1.2.0" }], allow),
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
test("countDeclaredHeld: counts declared names more than one install holds", () => {
|
||||
const versions = new Map([
|
||||
[
|
||||
".",
|
||||
new Map([
|
||||
["shared", "1.0.0"],
|
||||
["root-only", "1.0.0"],
|
||||
]),
|
||||
],
|
||||
[
|
||||
"clients/web",
|
||||
new Map([
|
||||
["shared", "1.0.0"],
|
||||
["undeclared", "1.0.0"],
|
||||
]),
|
||||
],
|
||||
]);
|
||||
assert.equal(
|
||||
countDeclaredHeld(new Set(["shared", "root-only", "undeclared"]), versions),
|
||||
// `shared` alone: the other two are held by one install each.
|
||||
1,
|
||||
);
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue