chore(deps): enrol installs by package.json, and fail on a missing lockfile
From Copilot's fifteenth pass of #1962. `installDirs` filtered on the presence of a lockfile, so a missing one silently removed that install from the comparison rather than failing. For the root — the install every shared source resolves from — that meant the guard could report success from client locks alone, which is the deny-by-default contract inverted. Enrolment is now by `package.json` (an install we are meant to compare), with the root always enrolled, and a missing lockfile is a named, loud failure. A `clients/` directory with no `package.json` is not an install and is skipped, so a stray scratch dir isn't asked for a lockfile it should never have. Three regression cases: a missing client lockfile in a fixture that IS skewed (so failing open would show as a pass), a missing ROOT lockfile, and a stray `clients/` dir that must not be enrolled. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HDdo1rNRVQnVRRdsqSrvG3
This commit is contained in:
parent
e1768e44ce
commit
288adbcfd6
2 changed files with 72 additions and 3 deletions
|
|
@ -228,6 +228,47 @@ test("main: exits 1 on a lockfile with a `packages` table but no root entry", ()
|
|||
);
|
||||
});
|
||||
|
||||
test("main: exits 1 on an install whose lockfile is missing (Copilot, #1962)", () => {
|
||||
// Enrolment is by `package.json`, so a missing lockfile is loud rather than a
|
||||
// silently absent row. The fixture IS skewed, so dropping the install instead
|
||||
// would leave the remaining holder unopposed and report aligned.
|
||||
withFixture(
|
||||
{ rootDeps: { ...ALIGNED, zod: "4.3.6" }, webDeps: ALIGNED },
|
||||
(dir) => {
|
||||
rmSync(path.join(dir, "clients", "web", "package-lock.json"));
|
||||
const { status, out } = runGuard(dir);
|
||||
assert.equal(status, 1, out);
|
||||
assert.match(out, /no lockfile/);
|
||||
assert.match(out, /clients\/web\/package-lock\.json/);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test("main: exits 1 when the ROOT lockfile is missing", () => {
|
||||
// The worst variant: the root is the install every shared source resolves
|
||||
// from, so omitting it could let the guard pass on client locks alone.
|
||||
withFixture({ rootDeps: ALIGNED, webDeps: ALIGNED }, (dir) => {
|
||||
rmSync(path.join(dir, "package-lock.json"));
|
||||
const { status, out } = runGuard(dir);
|
||||
assert.equal(status, 1, out);
|
||||
assert.match(out, /no lockfile/);
|
||||
assert.match(out, /^\s+\.\/package-lock\.json$/m);
|
||||
});
|
||||
});
|
||||
|
||||
test("main: a clients/ dir with no package.json is not an install", () => {
|
||||
// Enrolling a stray directory would demand a lockfile it should never have.
|
||||
withFixture({ rootDeps: ALIGNED, webDeps: ALIGNED }, (dir) => {
|
||||
mkdirSync(path.join(dir, "clients", "scratch"), { recursive: true });
|
||||
writeFileSync(
|
||||
path.join(dir, "clients", "scratch", "notes.md"),
|
||||
"scratch\n",
|
||||
);
|
||||
const { status, out } = runGuard(dir);
|
||||
assert.equal(status, 0, out);
|
||||
});
|
||||
});
|
||||
|
||||
test("main: exits 1 when the root validate no longer runs the sibling guard", () => {
|
||||
withFixture(
|
||||
{
|
||||
|
|
|
|||
|
|
@ -377,9 +377,18 @@ function installDirs() {
|
|||
.map((e) => `clients/${e.name}`)
|
||||
.sort()
|
||||
: [];
|
||||
return ["."]
|
||||
.concat(clients)
|
||||
.filter((dir) => existsSync(path.join(repoRoot, dir, "package-lock.json")));
|
||||
// Enrolment is by `package.json` — an install we are meant to compare —
|
||||
// NOT by the presence of a lockfile (Copilot, #1962). Filtering on the
|
||||
// lockfile made a missing one silently drop that install from the
|
||||
// comparison; for the root, the install every shared source resolves from,
|
||||
// that meant the guard could report success from client locks alone. A
|
||||
// missing lockfile is now a loud failure in `main`, not an absent row. The
|
||||
// root is always enrolled: it is this repo, so its manifest is a given.
|
||||
return ["."].concat(
|
||||
clients.filter((dir) =>
|
||||
existsSync(path.join(repoRoot, dir, "package.json")),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -427,6 +436,25 @@ export function main() {
|
|||
}
|
||||
|
||||
const dirs = installDirs();
|
||||
|
||||
// A missing lockfile is a failure, not a skipped install: dropping one would
|
||||
// remove its versions from the comparison and could report a real skew as
|
||||
// aligned.
|
||||
const missing = dirs.filter(
|
||||
(dir) => !existsSync(path.join(repoRoot, dir, "package-lock.json")),
|
||||
);
|
||||
if (missing.length > 0) {
|
||||
console.error(
|
||||
`verify:dep-lockstep — ${missing.length} install(s) have a package.json but no lockfile:\n`,
|
||||
);
|
||||
for (const dir of missing) console.error(` ${dir}/package-lock.json`);
|
||||
console.error(
|
||||
"\nEvery install must be compared; skipping one could report a real skew as aligned." +
|
||||
"\nRun `npm install` there, or remove the install if it is no longer part of the repo.",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const locks = dirs.map((dir) => {
|
||||
const file = path.join(repoRoot, dir, "package-lock.json");
|
||||
let lock;
|
||||
|
|
|
|||
Loading…
Reference in a new issue