chore(deps): address Copilot review round 1 on #2232
Five findings, all valid. `runOutdated` accepted every exit status and returned `result.stdout ?? ""`. A failed `npm outdated` (exit 2 on a registry or config error) also prints nothing, so the empty string parsed to an empty package list — and across five installs a total outage produced output byte-identical to a clean sweep. It now throws on any status other than the documented 0/1. `main()` was untested, which is how that reached review: the helper-only suite passed while the silent-success path was live. It now takes its spawn function as a parameter and is driven by a fake, covering npm failure, create vs. edit, milestone handling, both no-op paths and the missing-repo guard. The no-op path returned before looking for the marker issue, so once every install caught up, a still-open tracking issue kept its obsolete package table indefinitely — contradicting the body's own promise to update in place. It now rewrites through `buildClearedBody()`. It deliberately does not close the issue: the sweep takes no board actions, and closing one whose card a maintainer may have moved would make the board assert work shipped that this script cannot verify shipped. The release step mandated `npm audit fix`, which AGENTS.md forbids. The reason is not `--force`: plain `audit fix` resolves an advisory with no upward escape in range by silently downgrading, as it did to esbuild across three installs in #2058, and `local:gate` has no version-regression check to catch it. The step is now `npm audit` report-only, with fixes applied deliberately via a direct bump or an `overrides` entry. An unmilestoned issue is swept into Incoming, not Todo — Todo asserts a maintainer signed off. Message corrected and pinned by a test. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RWMJENjs1mExXE5XPJTEei Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
parent
ee14a9fc1c
commit
07bd987129
1 changed files with 293 additions and 59 deletions
|
|
@ -1,16 +1,89 @@
|
|||
// Unit tests for the pure parsing/formatting halves of dependency-refresh.mjs
|
||||
// (#2229). The impure half (`main()`, which shells out to `npm` and `gh`) is
|
||||
// exercised only via `workflow_dispatch` in CI, per the same split
|
||||
// `verify-skills.mjs` uses. Run via `npm run test:scripts`.
|
||||
// Tests for dependency-refresh.mjs (#2229) — both the pure parsing/formatting
|
||||
// helpers and `main()`'s orchestration, the latter driven through the injected
|
||||
// spawn function so no `npm` or `gh` process is ever started.
|
||||
//
|
||||
// `main()` is covered rather than left to `workflow_dispatch` because a
|
||||
// production trigger is not a test (Copilot): the helper-only suite this
|
||||
// replaced passed while a non-zero `npm outdated` exit reported a clean sweep.
|
||||
// Run via `npm run test:scripts`.
|
||||
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import {
|
||||
buildClearedBody,
|
||||
buildIssueBody,
|
||||
main,
|
||||
parseOutdated,
|
||||
INSTALLS,
|
||||
ISSUE_MARKER,
|
||||
} from "./dependency-refresh.mjs";
|
||||
|
||||
/**
|
||||
* A fake `spawnSync` that answers by command shape and records every call.
|
||||
*
|
||||
* @param {object} opts
|
||||
* @param {Record<string,unknown>} [opts.outdated] parsed `npm outdated` payload, per install dir
|
||||
* @param {number} [opts.outdatedStatus] exit status for every `npm outdated`
|
||||
* @param {Array<{number:number,body:string}>} [opts.existing] what `gh issue list` returns
|
||||
* @param {string|null} [opts.milestone] what the milestone lookup returns
|
||||
*/
|
||||
function fakeSpawn({
|
||||
outdated = {},
|
||||
outdatedStatus,
|
||||
existing = [],
|
||||
milestone = "v2.6.0",
|
||||
} = {}) {
|
||||
const calls = [];
|
||||
const fn = (cmd, args, opts) => {
|
||||
calls.push({ cmd, args, cwd: opts?.cwd });
|
||||
if (cmd === "npm") {
|
||||
const payload = outdated[opts.cwd] ?? {};
|
||||
const found = Object.keys(payload).length > 0;
|
||||
return {
|
||||
// Real `npm outdated` exits 1 precisely when it found something.
|
||||
status: outdatedStatus ?? (found ? 1 : 0),
|
||||
stdout: found ? JSON.stringify(payload) : "",
|
||||
stderr: outdatedStatus ? "ENOTFOUND registry.npmjs.org" : "",
|
||||
};
|
||||
}
|
||||
if (args[0] === "issue" && args[1] === "list")
|
||||
return { status: 0, stdout: JSON.stringify(existing), stderr: "" };
|
||||
if (args[0] === "api")
|
||||
return {
|
||||
status: 0,
|
||||
stdout: milestone ? `${milestone}\n` : "",
|
||||
stderr: "",
|
||||
};
|
||||
if (args[0] === "issue" && args[1] === "create")
|
||||
return {
|
||||
status: 0,
|
||||
stdout: "https://github.com/o/r/issues/9\n",
|
||||
stderr: "",
|
||||
};
|
||||
if (args[0] === "issue" && args[1] === "edit")
|
||||
return { status: 0, stdout: "", stderr: "" };
|
||||
throw new Error(`unexpected spawn: ${cmd} ${args.join(" ")}`);
|
||||
};
|
||||
fn.calls = calls;
|
||||
return fn;
|
||||
}
|
||||
|
||||
const ghCall = (spawn, verb) =>
|
||||
spawn.calls.find((c) => c.cmd === "gh" && c.args[1] === verb);
|
||||
|
||||
/** Silence main()'s progress logging; returns the captured lines. */
|
||||
function captureLog(run) {
|
||||
const lines = [];
|
||||
const original = console.log;
|
||||
console.log = (...a) => lines.push(a.join(" "));
|
||||
try {
|
||||
run();
|
||||
} finally {
|
||||
console.log = original;
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
|
||||
test("parseOutdated returns [] for empty npm-outdated output", () => {
|
||||
assert.deepEqual(parseOutdated(""), []);
|
||||
assert.deepEqual(parseOutdated("{}"), []);
|
||||
|
|
@ -59,3 +132,101 @@ test("buildIssueBody starts with the idempotency marker and skips empty installs
|
|||
assert.ok(!body.includes("### `root`"));
|
||||
assert.ok(body.includes("| `zod` | 3.0.0 | 3.1.0 | 4.0.0 |"));
|
||||
});
|
||||
|
||||
test("buildClearedBody keeps the marker so the next sweep still finds the issue", () => {
|
||||
const body = buildClearedBody("2026-09-03");
|
||||
assert.ok(body.startsWith(ISSUE_MARKER));
|
||||
assert.ok(body.includes("2026-09-03"));
|
||||
assert.ok(body.includes("Safe to close"));
|
||||
});
|
||||
|
||||
test("main throws when npm outdated exits with an undocumented status", () => {
|
||||
// The regression that motivated covering main(): exit 2 used to fall through
|
||||
// to empty stdout and report a clean sweep across all five installs.
|
||||
const spawn = fakeSpawn({ outdatedStatus: 2 });
|
||||
assert.throws(
|
||||
() => captureLog(() => main("o/r", spawn)),
|
||||
/npm outdated failed in \.\s*\(exit 2\).*ENOTFOUND/s,
|
||||
);
|
||||
assert.equal(ghCall(spawn, "create"), undefined);
|
||||
});
|
||||
|
||||
test("main sweeps every install and files one milestoned issue", () => {
|
||||
const spawn = fakeSpawn({
|
||||
outdated: {
|
||||
"clients/web": {
|
||||
zod: { current: "3.0.0", wanted: "3.1.0", latest: "4.0.0" },
|
||||
},
|
||||
},
|
||||
});
|
||||
const log = captureLog(() => main("o/r", spawn));
|
||||
|
||||
assert.deepEqual(
|
||||
spawn.calls.filter((c) => c.cmd === "npm").map((c) => c.cwd),
|
||||
INSTALLS.map((i) => i.dir),
|
||||
);
|
||||
const create = ghCall(spawn, "create");
|
||||
assert.ok(create, "expected an issue to be created");
|
||||
assert.deepEqual(create.args.slice(-2), ["--milestone", "v2.6.0"]);
|
||||
assert.ok(create.args[create.args.indexOf("--body") + 1].includes("`zod`"));
|
||||
assert.ok(log.some((l) => l.includes("filed")));
|
||||
});
|
||||
|
||||
test("main edits the existing issue instead of filing a duplicate", () => {
|
||||
const spawn = fakeSpawn({
|
||||
outdated: {
|
||||
".": { ajv: { current: "8.0.0", wanted: "8.1.0", latest: "8.1.0" } },
|
||||
},
|
||||
existing: [{ number: 77, body: `${ISSUE_MARKER}\nstale` }],
|
||||
});
|
||||
const log = captureLog(() => main("o/r", spawn));
|
||||
|
||||
assert.equal(ghCall(spawn, "create"), undefined);
|
||||
const edit = ghCall(spawn, "edit");
|
||||
assert.equal(edit.args[2], "77");
|
||||
assert.ok(edit.args[edit.args.length - 1].includes("`ajv`"));
|
||||
assert.ok(log.some((l) => l.includes("updated existing #77")));
|
||||
});
|
||||
|
||||
test("main clears a still-open issue once everything is current again", () => {
|
||||
const spawn = fakeSpawn({
|
||||
existing: [{ number: 77, body: `${ISSUE_MARKER}\n| \`zod\` | 3.0.0 |` }],
|
||||
});
|
||||
const log = captureLog(() => main("o/r", spawn));
|
||||
|
||||
const edit = ghCall(spawn, "edit");
|
||||
assert.ok(edit, "an open issue must be cleared, not left with a stale table");
|
||||
const body = edit.args[edit.args.length - 1];
|
||||
assert.ok(body.startsWith(ISSUE_MARKER));
|
||||
assert.ok(!body.includes("`zod`"));
|
||||
assert.ok(log.some((l) => l.includes("cleared stale list on #77")));
|
||||
});
|
||||
|
||||
test("main is a true no-op when nothing is outdated and no issue is open", () => {
|
||||
const spawn = fakeSpawn();
|
||||
const log = captureLog(() => main("o/r", spawn));
|
||||
|
||||
assert.equal(ghCall(spawn, "edit"), undefined);
|
||||
assert.equal(ghCall(spawn, "create"), undefined);
|
||||
assert.ok(log.some((l) => l.includes("no-op")));
|
||||
});
|
||||
|
||||
test("main says Incoming, not Todo, when it files without a milestone", () => {
|
||||
const spawn = fakeSpawn({
|
||||
outdated: {
|
||||
".": { ajv: { current: "8.0.0", wanted: "8.1.0", latest: "8.1.0" } },
|
||||
},
|
||||
milestone: null,
|
||||
});
|
||||
const log = captureLog(() => main("o/r", spawn));
|
||||
|
||||
const create = ghCall(spawn, "create");
|
||||
assert.ok(!create.args.includes("--milestone"));
|
||||
// Unmilestoned is unapproved; triage parks it in Incoming.
|
||||
assert.ok(log.some((l) => l.includes("Incoming")));
|
||||
assert.ok(!log.some((l) => l.includes("Todo")));
|
||||
});
|
||||
|
||||
test("main refuses to run without a repo", () => {
|
||||
assert.throws(() => main(undefined, fakeSpawn()), /GITHUB_REPOSITORY unset/);
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue