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
1 changed files with 583 additions and 27 deletions
|
|
@ -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