// Tests for dependabot-alerts.mjs (#2233) — both the pure grouping/formatting // helpers and `main()`'s orchestration, the latter driven through the injected // spawn function so no `gh` process is ever started. // // `main()` is covered rather than left to `workflow_dispatch` because a // production trigger is not a test (Copilot): everything that can go wrong in // the orchestration — a paginated alert feed, a manifest that has moved on, a // half-written board card, a comment posted twice — goes wrong only against the // real API, where nothing would be asserted. // Run via `npm run test:scripts`. import { test } from "node:test"; import assert from "node:assert/strict"; import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; import { dirname, join } from "node:path"; import { tmpdir } from "node:os"; import { advisoryKey, parseClearedDate, SUPPORTED_ECOSYSTEM, PRIORITY_FIELD_ID, STATUS_FIELD_ID, buildClearedBody, overrideAncestors, scopedOverrideExample, buildCommentMarker, pickMilestone, narrowToApplicable, lockfileEntries, remediation, buildIssueBody, buildIssueTitle, buildMarker, buildNewAdvisoryComment, groupAlerts, isDirectDependency, isPermissionDenied, lockfileVersions, main, mergeGhsas, parseCommentMarker, parseMarker, toSemverRange, } from "./dependabot-alerts.mjs"; /** Shaped like a real `GET /repos/{o}/{r}/dependabot/alerts` entry. */ function alert({ ghsa, pkg = "fast-uri", manifest = "package-lock.json", fixed = "3.1.6", severity = "high", range = ">= 3.0.0, < 3.1.6", scope = "runtime", cve = null, state = "open", ecosystem = "npm", }) { return { state, html_url: `https://github.com/o/r/security/dependabot/${ghsa}`, dependency: { package: { name: pkg, ecosystem }, manifest_path: manifest, scope, }, security_advisory: { ghsa_id: ghsa, cve_id: cve, severity, summary: `${pkg} is bad`, }, security_vulnerability: { vulnerable_version_range: range, first_patched_version: { identifier: fixed }, }, }; } test("toSemverRange turns GitHub's comma-separated conjuncts into semver ANDs", () => { assert.equal(toSemverRange(">= 3.1.3, < 3.1.6"), ">= 3.1.3 < 3.1.6"); assert.equal(toSemverRange("<= 4.28.6"), "<= 4.28.6"); assert.equal(toSemverRange(">= 2.2.5, < 6.16.0"), ">= 2.2.5 < 6.16.0"); }); test("toSemverRange tolerates stray whitespace and trailing commas", () => { assert.equal(toSemverRange(" >= 1.0.0 , < 2.0.0 , "), ">= 1.0.0 < 2.0.0"); }); test("lockfileVersions finds hoisted and nested copies, deduped and sorted", () => { const lock = { packages: { "": { dependencies: { zod: "^3.0.0" } }, "node_modules/fast-uri": { version: "3.1.5" }, "node_modules/ajv/node_modules/fast-uri": { version: "3.0.1" }, "node_modules/other/node_modules/fast-uri": { version: "3.1.5" }, "node_modules/fast-uri-lookalike": { version: "9.9.9" }, }, }; assert.deepEqual(lockfileVersions(lock, "fast-uri"), ["3.0.1", "3.1.5"]); }); test("lockfileVersions returns [] when the package is absent", () => { assert.deepEqual(lockfileVersions({ packages: {} }, "qs"), []); assert.deepEqual(lockfileVersions({}, "qs"), []); }); test("isDirectDependency reads the root manifest entry, not the tree", () => { const lock = { packages: { "": { dependencies: { zod: "^4.0.0" }, devDependencies: { vitest: "1" } }, "node_modules/fast-uri": { version: "3.1.5" }, }, }; assert.equal(isDirectDependency(lock, "zod"), true); assert.equal(isDirectDependency(lock, "vitest"), true); assert.equal(isDirectDependency(lock, "fast-uri"), false); }); test("groupAlerts collapses advisories into one entry per bump", () => { const grouped = groupAlerts([ alert({ ghsa: "GHSA-5jgf", range: ">= 3.1.3, < 3.1.6" }), alert({ ghsa: "GHSA-f65p" }), alert({ ghsa: "GHSA-jqff" }), alert({ ghsa: "GHSA-fph4", range: ">= 3.1.2, < 3.1.6" }), alert({ ghsa: "GHSA-x5fp", pkg: "qs", fixed: "6.16.0", severity: "medium", range: ">= 6.14.2, <= 6.15.3", }), alert({ ghsa: "GHSA-73wf", pkg: "browserslist", manifest: "clients/tui/package-lock.json", fixed: "4.28.7", scope: "development", range: "<= 4.28.6", }), ]); assert.deepEqual( grouped.map((g) => [g.package, g.manifestPath, g.fixedIn, g.ghsas.length]), [ ["browserslist", "clients/tui/package-lock.json", "4.28.7", 1], ["fast-uri", "package-lock.json", "3.1.6", 4], ["qs", "package-lock.json", "6.16.0", 1], ], ); // GHSAs are sorted within a group so the marker is stable across runs. assert.deepEqual(grouped[1].ghsas, [ "GHSA-5jgf", "GHSA-f65p", "GHSA-fph4", "GHSA-jqff", ]); }); test("groupAlerts keeps the highest severity across a group", () => { const [group] = groupAlerts([ alert({ ghsa: "GHSA-a", severity: "low" }), alert({ ghsa: "GHSA-b", severity: "critical" }), alert({ ghsa: "GHSA-c", severity: "medium" }), ]); assert.equal(group.severity, "critical"); }); test("groupAlerts splits a package whose advisories need different bumps", () => { const grouped = groupAlerts([ alert({ ghsa: "GHSA-a", fixed: "3.1.6" }), alert({ ghsa: "GHSA-b", fixed: "4.0.0" }), ]); assert.equal(grouped.length, 2); assert.deepEqual( grouped.map((g) => g.fixedIn), ["3.1.6", "4.0.0"], ); }); test("groupAlerts drops closed alerts and ones with no patched version", () => { const unpatched = alert({ ghsa: "GHSA-x" }); unpatched.security_vulnerability.first_patched_version = null; assert.deepEqual( groupAlerts([alert({ ghsa: "GHSA-y", state: "fixed" }), unpatched]), [], ); }); test("buildMarker and parseMarker round-trip, sorting the GHSA list", () => { const marker = buildMarker({ package: "fast-uri", manifestPath: "package-lock.json", fixedIn: "3.1.6", ghsas: ["GHSA-b", "GHSA-a"], }); assert.equal( marker, "", ); assert.deepEqual(parseMarker(`${marker}\nbody text`), { package: "fast-uri", manifestPath: "package-lock.json", fixedIn: "3.1.6", ghsas: ["GHSA-a", "GHSA-b"], }); }); test("the marker carries fixedIn, so two bumps of one package stay distinct", () => { const [a] = groupAlerts([alert({ ghsa: "GHSA-a", fixed: "3.1.6" })]); const [b] = groupAlerts([alert({ ghsa: "GHSA-b", fixed: "4.0.0" })]); assert.notEqual(buildMarker(a), buildMarker(b)); assert.equal(parseMarker(buildMarker(a)).fixedIn, "3.1.6"); assert.equal(parseMarker(buildMarker(b)).fixedIn, "4.0.0"); }); test("buildCommentMarker and parseCommentMarker round-trip", () => { const marker = buildCommentMarker(["GHSA-b", "GHSA-a"]); assert.equal(marker, ""); assert.deepEqual(parseCommentMarker(`${marker}\ntext`), ["GHSA-a", "GHSA-b"]); assert.equal(parseCommentMarker("an ordinary comment"), null); }); test("isPermissionDenied tolerates a scope refusal", () => { // The two ways GitHub says "this token may not read that": an explicit 403, // and a 404 hiding a resource the token cannot see. assert.equal( isPermissionDenied("gh: HTTP 403: Resource not accessible by integration"), true, ); assert.equal(isPermissionDenied("gh: HTTP 404: Not Found"), true); }); test("isPermissionDenied treats a bad token or a rate limit as a real failure", () => { // ⚠️ A rate limit is also a 403, so status alone cannot decide this — waving // it through would exit green having skipped the sweep's own precondition. assert.equal( isPermissionDenied("gh: API rate limit exceeded (HTTP 403)"), false, ); assert.equal( isPermissionDenied( "gh: HTTP 403: You have exceeded a secondary rate limit", ), false, ); // 401 is a bad or expired token, never a scope question. assert.equal(isPermissionDenied("gh: HTTP 401: Bad credentials"), false); assert.equal( isPermissionDenied("gh: HTTP 500: Internal Server Error"), false, ); }); test("parseMarker returns null for an unmarked or absent body", () => { assert.equal(parseMarker(undefined), null); assert.equal(parseMarker("just an issue someone wrote"), null); // The marker is the FIRST line or it is not the idempotency key. assert.equal( parseMarker( "preamble\n", ), null, ); }); test("mergeGhsas reports only the advisories the issue does not already name", () => { assert.deepEqual(mergeGhsas(["GHSA-a", "GHSA-b"], ["GHSA-b", "GHSA-c"]), { merged: ["GHSA-a", "GHSA-b", "GHSA-c"], added: ["GHSA-c"], }); }); test("mergeGhsas reports nothing added when the issue already covers them", () => { assert.deepEqual(mergeGhsas(["GHSA-a", "GHSA-b"], ["GHSA-a"]), { merged: ["GHSA-a", "GHSA-b"], added: [], }); }); /** The probe shape `buildIssueBody` takes: one vulnerable copy, nested by default. */ const nested = ( version = "3.1.5", path = "node_modules/ajv/node_modules/fast-uri", ) => ({ affected: [{ path, version, hoisted: false }], declared: false, }); const hoisted = (version = "3.1.5") => ({ affected: [{ path: "node_modules/fast-uri", version, hoisted: true }], declared: true, }); test("narrowToApplicable drops advisories the installed version is out of range of", () => { // Same package, manifest and patched version, so one group — but different // vulnerable ranges, and 3.1.0 is in range of only one of them. const [group] = groupAlerts([ alert({ ghsa: "GHSA-narrow", range: ">= 3.1.3, < 3.1.6", severity: "critical", }), alert({ ghsa: "GHSA-wide", range: ">= 3.0.0, < 3.1.6", severity: "medium", }), ]); assert.equal(group.advisories.length, 2); const result = narrowToApplicable(group, [ { path: "node_modules/fast-uri", version: "3.1.0", hoisted: true }, ]); assert.deepEqual(result.group.ghsas, ["GHSA-wide"]); // Severity is re-derived: the critical one does not apply here. assert.equal(result.group.severity, "medium"); assert.equal(result.affected.length, 1); // ...and the marker cannot claim an advisory this branch is not exposed to. assert.deepEqual(parseMarker(buildMarker(result.group)).ghsas, ["GHSA-wide"]); }); test("narrowToApplicable keeps every advisory that does apply", () => { const [group] = groupAlerts([ alert({ ghsa: "GHSA-narrow", range: ">= 3.1.3, < 3.1.6" }), alert({ ghsa: "GHSA-wide", range: ">= 3.0.0, < 3.1.6" }), ]); const result = narrowToApplicable(group, [ { path: "node_modules/fast-uri", version: "3.1.5", hoisted: true }, ]); assert.deepEqual(result.group.ghsas, ["GHSA-narrow", "GHSA-wide"]); }); test("narrowToApplicable returns null when nothing installed is in range", () => { const [group] = groupAlerts([alert({ ghsa: "GHSA-a" })]); assert.equal( narrowToApplicable(group, [ { path: "node_modules/fast-uri", version: "3.1.6", hoisted: true }, ]), null, ); assert.equal(narrowToApplicable(group, []), null); }); test("buildIssueBody escapes a disjoint range so the table survives it", () => { // `||` is legal in a semver range and is also the Markdown column separator. const [group] = groupAlerts([ alert({ ghsa: "GHSA-a", range: ">= 1.0.0, < 2.0.0 || >= 3.0.0, < 3.5.0" }), ]); const body = buildIssueBody(group, nested()); const row = body .split("\n") .find((line) => line.includes("GHSA-a") && line.startsWith("|")); // Count only the pipes Markdown will treat as separators: five columns means // four inner separators plus the two outer ones. const separators = row.replace(/\\\|/g, "").split("|").length - 1; assert.equal( separators, 6, `escaped row should keep its column count: ${row}`, ); assert.ok(row.includes(String.raw`\|\|`), "the range's own pipes survive"); }); test("pickMilestone takes the nearest due date, never an undated bucket", () => { // jq sorts null before every string, so `sort_by(.due_on) | .[0]` over this // list would return "Backlog" — an open bucket with no release date at all. const milestones = [ { title: "Backlog", state: "open", due_on: null }, { title: "v2.7.0", state: "open", due_on: "2026-09-16T00:00:00Z" }, { title: "v2.6.0", state: "open", due_on: "2026-09-09T00:00:00Z" }, ]; assert.equal(pickMilestone(milestones), "v2.6.0"); }); test("pickMilestone ignores closed milestones and empty input", () => { assert.equal( pickMilestone([ { title: "v2.5.0", state: "closed", due_on: "2026-01-01T00:00:00Z" }, ]), null, ); // Nothing dated and open means no bucket to take: filed unmilestoned, and // the board write is skipped so triage places it. assert.equal(pickMilestone([{ title: "Backlog", due_on: null }]), null); assert.equal(pickMilestone([]), null); assert.equal(pickMilestone(undefined), null); }); test("buildIssueTitle names the bump and pluralizes the advisory count", () => { const [many] = groupAlerts([ alert({ ghsa: "GHSA-a" }), alert({ ghsa: "GHSA-b" }), ]); assert.equal( buildIssueTitle(many), "chore(deps): bump `fast-uri` to `3.1.6` in `package-lock.json` (2 advisories)", ); const [one] = groupAlerts([alert({ ghsa: "GHSA-a" })]); assert.equal( buildIssueTitle(one), "chore(deps): bump `fast-uri` to `3.1.6` in `package-lock.json` (1 advisory)", ); }); test("buildIssueBody leads with the marker and asks for an overrides pin when transitive", () => { const [group] = groupAlerts([ alert({ ghsa: "GHSA-a", cve: "CVE-2026-1" }), alert({ ghsa: "GHSA-b" }), ]); const body = buildIssueBody(group, nested()); assert.ok(body.startsWith(buildMarker(group))); assert.match(body, /\| Vulnerable on `v2\/main` \| `3\.1\.5` \|/); assert.match(body, /\| Fixed in \| `3\.1\.6` \|/); assert.match(body, /GHSA-a/); assert.match(body, /CVE-2026-1/); assert.match(body, /`overrides`/); assert.doesNotMatch(body, /raise its declared range/); }); test("buildIssueBody asks a direct dependency's range to be raised, not widened", () => { const [group] = groupAlerts([alert({ ghsa: "GHSA-a" })]); const body = buildIssueBody(group, hoisted()); assert.match(body, /Raise the declared range in `package\.json`/); assert.match(body, /can no longer resolve below `3\.1\.6`/); // Prescribing `>=3.1.6` would throw away the manifest's compatibility bound. assert.doesNotMatch(body, /range to `>=/); assert.doesNotMatch(body, /overrides/); }); test("remediation reads the vulnerable copies, not the declaration", () => { const declaredSafe = [ { path: "node_modules/ajv/node_modules/fast-uri", version: "3.1.5", hoisted: false, }, ]; // The manifest declares `fast-uri`, but the copy in range is a nested one: // raising the declared range would change nothing at all. assert.deepEqual(remediation(declaredSafe, true), { direct: false, transitive: true, }); assert.deepEqual( remediation( [{ path: "node_modules/fast-uri", version: "3.1.5", hoisted: true }], true, ), { direct: true, transitive: false }, ); // An undeclared hoisted copy got there transitively like any other, so it // needs the override — "hoisted" is not a synonym for "declared". assert.deepEqual( remediation( [{ path: "node_modules/fast-uri", version: "3.1.5", hoisted: true }], false, ), { direct: false, transitive: true }, ); }); test("buildIssueBody asks for BOTH edits when declared and nested copies are vulnerable", () => { const [group] = groupAlerts([alert({ ghsa: "GHSA-a" })]); const body = buildIssueBody(group, { declared: true, affected: [ { path: "node_modules/fast-uri", version: "3.1.5", hoisted: true }, { path: "node_modules/ajv/node_modules/fast-uri", version: "3.0.1", hoisted: false, }, ], }); assert.match(body, /Both edits are needed/); assert.match(body, /1\. \*\*Raise the declared range/); assert.match(body, /2\. \*\*Add a parent-scoped \[`overrides`\]/); // A package-wide pin would be rejected: the manifest declares it directly. assert.match(body, /EOVERRIDE/); assert.match(body, /"ajv": \{\n\s+"fast-uri": "3\.1\.6"/); // The table names the copies, so the maintainer can see why. assert.match( body, /\| `node_modules\/ajv\/node_modules\/fast-uri` \| `3\.0\.1` \|/, ); }); test("buildIssueBody asks only for an override when the declared copy is safe", () => { const [group] = groupAlerts([alert({ ghsa: "GHSA-a" })]); // A valid lock: safe declared fast-uri@4, vulnerable nested fast-uri@3.1.5. const body = buildIssueBody(group, { declared: true, affected: [ { path: "node_modules/ajv/node_modules/fast-uri", version: "3.1.5", hoisted: false, }, ], }); assert.doesNotMatch(body, /Raise the declared range/); assert.match(body, /Add an \[`overrides`\]/); }); test("lockfileEntries keeps each copy's path and marks the hoisted one", () => { const lock = { packages: { "": { dependencies: { "fast-uri": "^4.0.0" } }, "node_modules/fast-uri": { version: "4.0.0" }, "node_modules/ajv/node_modules/fast-uri": { version: "3.1.5" }, }, }; assert.deepEqual(lockfileEntries(lock, "fast-uri"), [ { path: "node_modules/ajv/node_modules/fast-uri", version: "3.1.5", hoisted: false, }, { path: "node_modules/fast-uri", version: "4.0.0", hoisted: true }, ]); }); test("buildIssueBody honors an overridden GHSA list when rewriting an issue", () => { const [group] = groupAlerts([alert({ ghsa: "GHSA-a" })]); const body = buildIssueBody(group, { ...nested(), ghsas: ["GHSA-a", "GHSA-old"], }); assert.deepEqual(parseMarker(body).ghsas, ["GHSA-a", "GHSA-old"]); }); test("buildNewAdvisoryComment lists only the newly-seen advisories", () => { const [group] = groupAlerts([ alert({ ghsa: "GHSA-a" }), alert({ ghsa: "GHSA-b" }), ]); const comment = buildNewAdvisoryComment(group, ["GHSA-b"]); assert.match(comment, /1 new Dependabot advisory/); assert.match(comment, /GHSA-b/); assert.doesNotMatch(comment, /GHSA-a/); }); // --------------------------------------------------------------------------- // main() orchestration, driven through the injected spawn function. // // `workflow_dispatch` is a production trigger, not a test — and everything that // can go wrong here goes wrong in production only: a paginated alert feed, a // manifest that has moved on, a half-written board card, a comment posted twice // (Copilot). Manifests are read from the working tree, so each test runs in a // temp directory it populates itself. // --------------------------------------------------------------------------- /** * A `spawnSync` stand-in. `gh` responses are matched on the argument list, in * the order the handlers are declared, and every call is recorded. */ function fakeSpawn({ securityFixes = { enabled: false, paused: false }, securityFixesStatus = 0, securityFixesStderr = "", alertPages = [[]], issues = [], comments = [], milestone = "v2.6.0", boardEditStatus = 0, } = {}) { const calls = []; const ok = (stdout = "") => ({ status: 0, stdout, stderr: "" }); const spawn = (cmd, args, opts) => { calls.push({ cmd, args, opts }); const joined = args.join(" "); if (joined.includes("automated-security-fixes")) { return securityFixesStatus === 0 ? ok(JSON.stringify(securityFixes)) : { status: securityFixesStatus, stdout: "", stderr: securityFixesStderr, }; } if (joined.includes("dependabot/alerts")) { // `--slurp` yields one array PER PAGE; main() must flatten them. return ok(JSON.stringify(alertPages)); } if (joined.includes("milestones")) return ok( JSON.stringify( milestone ? [ { title: milestone, state: "open", due_on: "2026-09-09T00:00:00Z", }, ] : [], ), ); // `--slurp`, so one array per page — `issues` may be a flat list or pages. if (joined.includes("/issues?")) return ok(JSON.stringify(Array.isArray(issues[0]) ? issues : [issues])); if (args[0] === "issue" && args[1] === "view") return ok(JSON.stringify({ comments })); if (args[0] === "issue" && args[1] === "create") return ok("https://github.com/o/r/issues/77"); if (args[0] === "project" && args[1] === "item-add") return ok(JSON.stringify({ id: "PVTI_fake" })); if (args[0] === "project" && args[1] === "field-list") return ok( JSON.stringify({ fields: [ { name: "Status", options: [{ name: "Todo", id: "todo-id" }] }, { name: "Priority", options: [{ name: "High", id: "high-id" }] }, ], }), ); if (args[0] === "project" && args[1] === "item-edit") return boardEditStatus === 0 ? ok() : { status: boardEditStatus, stdout: "", stderr: "field edit blew up" }; return ok(); }; spawn.calls = calls; return spawn; } const ghCall = (spawn, verb) => spawn.calls.find( (c) => c.cmd === "gh" && c.args[0] === "issue" && c.args[1] === verb, ); const ghCalls = (spawn, verb) => spawn.calls.filter( (c) => c.cmd === "gh" && c.args[0] === "issue" && c.args[1] === verb, ); function captureLog(run) { const lines = []; const original = console.log; console.log = (...a) => lines.push(a.join(" ")); try { run(); } finally { console.log = original; } return lines; } /** Run `body` in a temp cwd populated with `files` (path -> JSON value). */ function inTempRepo(files, body) { const dir = mkdtempSync(join(tmpdir(), "dependabot-alerts-")); const cwd = process.cwd(); try { for (const [path, value] of Object.entries(files)) { const full = join(dir, path); mkdirSync(dirname(full), { recursive: true }); writeFileSync(full, JSON.stringify(value)); } process.chdir(dir); return body(); } finally { process.chdir(cwd); rmSync(dir, { recursive: true, force: true }); } } /** * The probe `main()` derives from `lockWith`: one hoisted, undeclared copy. * A fixture issue built from this renders byte-identically to what `main()` * would produce, which is what makes the no-op path assertable. */ const asInstalled = (version = "3.1.5") => ({ affected: [{ path: "node_modules/fast-uri", version, hoisted: true }], declared: false, }); /** A lockfile holding one transitive copy of `pkg` at `version`. */ const lockWith = (pkg, version) => ({ lockfileVersion: 3, packages: { "": { dependencies: {} }, [`node_modules/${pkg}`]: { version } }, }); /** Without a PAT the board is never touched, which most tests want. */ function withoutProjectToken(body) { const saved = process.env.PROJECT_TOKEN; delete process.env.PROJECT_TOKEN; try { return body(); } finally { if (saved !== undefined) process.env.PROJECT_TOKEN = saved; } } function withProjectToken(body) { const saved = process.env.PROJECT_TOKEN; process.env.PROJECT_TOKEN = "pat"; try { return body(); } finally { if (saved === undefined) delete process.env.PROJECT_TOKEN; else process.env.PROJECT_TOKEN = saved; } } test("main files one issue per bump, flattening a paginated alert feed", () => { // Two pages, as `--slurp` returns them: a single JSON.parse of concatenated // pages would have thrown before this ever reached grouping. const spawn = fakeSpawn({ alertPages: [ [ alert({ ghsa: "GHSA-a", range: ">= 3.1.3, < 3.1.6" }), alert({ ghsa: "GHSA-b" }), ], [ alert({ ghsa: "GHSA-c", pkg: "browserslist", manifest: "clients/tui/package-lock.json", fixed: "4.28.7", range: "<= 4.28.6", }), ], ], }); const log = inTempRepo( { "package-lock.json": lockWith("fast-uri", "3.1.5"), "clients/tui/package-lock.json": lockWith("browserslist", "4.28.2"), }, () => withoutProjectToken(() => captureLog(() => main("o/r", spawn))), ); const created = ghCalls(spawn, "create"); assert.equal(created.length, 2, "3 advisories, 2 bumps, 2 issues"); const titles = created.map((c) => c.args[c.args.indexOf("--title") + 1]); assert.ok(titles.some((t) => t.includes("`fast-uri` to `3.1.6`"))); assert.ok(titles.some((t) => t.includes("`browserslist` to `4.28.7`"))); // The fast-uri issue names both of its advisories. const fastUri = created.find((c) => c.args[c.args.indexOf("--title") + 1].includes("fast-uri"), ); const body = fastUri.args[fastUri.args.indexOf("--body") + 1]; assert.deepEqual(parseMarker(body).ghsas, ["GHSA-a", "GHSA-b"]); assert.deepEqual(created[0].args.slice(-2), ["--milestone", "v2.6.0"]); assert.ok(log.some((l) => l.includes("filed"))); }); test("main skips an alert already out of range on the checked-out branch", () => { const spawn = fakeSpawn({ alertPages: [[alert({ ghsa: "GHSA-a" })]] }); const log = inTempRepo( { "package-lock.json": lockWith("fast-uri", "3.1.6") }, () => withoutProjectToken(() => captureLog(() => main("o/r", spawn))), ); assert.equal(ghCall(spawn, "create"), undefined); assert.ok(log.some((l) => l.includes("already out of range"))); }); test("main skips an alert whose manifest is absent from the checkout", () => { const spawn = fakeSpawn({ alertPages: [ [alert({ ghsa: "GHSA-a", manifest: "gone/package-lock.json" })], ], }); const log = inTempRepo({}, () => withoutProjectToken(() => captureLog(() => main("o/r", spawn))), ); assert.equal(ghCall(spawn, "create"), undefined); assert.ok(log.some((l) => l.includes("absent on v2/main"))); }); test("main is a complete no-op on a second run", () => { const [group] = groupAlerts([alert({ ghsa: "GHSA-a" })]); const spawn = fakeSpawn({ alertPages: [[alert({ ghsa: "GHSA-a" })]], issues: [ { number: 41, title: buildIssueTitle(group), body: buildIssueBody(group, asInstalled()), }, ], }); const log = inTempRepo( { "package-lock.json": lockWith("fast-uri", "3.1.5") }, () => withoutProjectToken(() => captureLog(() => main("o/r", spawn))), ); assert.equal(ghCall(spawn, "create"), undefined); assert.equal(ghCall(spawn, "comment"), undefined); assert.equal(ghCall(spawn, "edit"), undefined); assert.ok(log.some((l) => l.includes("#41 is up to date"))); }); test("main will not update an issue whose bump differs, even for the same package", () => { // The marker's `fixed=` is what keeps a 4.0.0 bump off the 3.1.6 issue. const [old] = groupAlerts([alert({ ghsa: "GHSA-a", fixed: "3.1.6" })]); const spawn = fakeSpawn({ alertPages: [[alert({ ghsa: "GHSA-b", fixed: "4.0.0", range: "< 4.0.0" })]], issues: [ { number: 41, title: buildIssueTitle(old), body: buildIssueBody(old, asInstalled()), }, ], }); const log = inTempRepo( { "package-lock.json": lockWith("fast-uri", "3.1.5") }, () => withoutProjectToken(() => captureLog(() => main("o/r", spawn, "2026-09-04")), ), ); assert.ok(ghCall(spawn, "create"), "a different bump gets its own issue"); // ...and the 3.1.6 issue, whose alert is no longer in the open feed, is // cleared rather than left asserting a vulnerability nobody tracks. const edit = ghCall(spawn, "edit"); assert.ok(edit, "the superseded issue is reconciled"); assert.match( edit.args[edit.args.indexOf("--body") + 1], /fixed or dismissed/, ); assert.ok(log.some((l) => l.includes("fixed or dismissed"))); }); test("main comments a new advisory BEFORE rewriting the marker", () => { const [old] = groupAlerts([alert({ ghsa: "GHSA-a" })]); const spawn = fakeSpawn({ alertPages: [[alert({ ghsa: "GHSA-a" }), alert({ ghsa: "GHSA-b" })]], issues: [ { number: 41, title: buildIssueTitle(old), body: buildIssueBody(old, asInstalled()), }, ], }); inTempRepo({ "package-lock.json": lockWith("fast-uri", "3.1.5") }, () => withoutProjectToken(() => captureLog(() => main("o/r", spawn))), ); const order = spawn.calls .filter( (c) => c.args[0] === "issue" && ["comment", "edit"].includes(c.args[1]), ) .map((c) => c.args[1]); // Marker-first would let a failed comment be skipped forever by the no-op branch. assert.deepEqual(order, ["comment", "edit"]); const comment = ghCall(spawn, "comment"); const text = comment.args[comment.args.indexOf("--body") + 1]; assert.deepEqual(parseCommentMarker(text), ["GHSA-b"]); assert.ok(!text.includes("GHSA-a"), "only the newly-seen advisory"); const edit = ghCall(spawn, "edit"); assert.deepEqual( parseMarker(edit.args[edit.args.indexOf("--body") + 1]).ghsas, ["GHSA-a", "GHSA-b"], ); }); test("main does not repeat a comment it already posted", () => { const [old] = groupAlerts([alert({ ghsa: "GHSA-a" })]); const spawn = fakeSpawn({ alertPages: [[alert({ ghsa: "GHSA-a" }), alert({ ghsa: "GHSA-b" })]], issues: [ { number: 41, title: buildIssueTitle(old), body: buildIssueBody(old, asInstalled()), }, ], comments: [{ body: `${buildCommentMarker(["GHSA-b"])}\nsaid already` }], }); inTempRepo({ "package-lock.json": lockWith("fast-uri", "3.1.5") }, () => withoutProjectToken(() => captureLog(() => main("o/r", spawn))), ); assert.equal(ghCall(spawn, "comment"), undefined); // The marker still gets brought up to date. assert.ok(ghCall(spawn, "edit")); }); test("main announces only the advisories no comment has claimed yet", () => { // The exact shape a failed marker edit leaves behind: the comment for `b` // went out, the body edit did not, and a third advisory has since arrived. // Comparing whole GHSA sets would find no match and announce `b` twice. const [old] = groupAlerts([alert({ ghsa: "GHSA-a" })]); const spawn = fakeSpawn({ alertPages: [ [ alert({ ghsa: "GHSA-a" }), alert({ ghsa: "GHSA-b" }), alert({ ghsa: "GHSA-c" }), ], ], issues: [ { number: 41, title: buildIssueTitle(old), body: buildIssueBody(old, asInstalled()), }, ], comments: [{ body: `${buildCommentMarker(["GHSA-b"])}\nannounced b` }], }); inTempRepo({ "package-lock.json": lockWith("fast-uri", "3.1.5") }, () => withoutProjectToken(() => captureLog(() => main("o/r", spawn))), ); const comment = ghCall(spawn, "comment"); assert.ok(comment, "the unannounced advisory still gets a comment"); const text = comment.args[comment.args.indexOf("--body") + 1]; assert.deepEqual(parseCommentMarker(text), ["GHSA-c"]); assert.ok(!text.includes("GHSA-b"), "b was already announced"); }); test("main refreshes the title when an issue grows another advisory", () => { const [old] = groupAlerts([alert({ ghsa: "GHSA-a" })]); const spawn = fakeSpawn({ alertPages: [[alert({ ghsa: "GHSA-a" }), alert({ ghsa: "GHSA-b" })]], issues: [ { number: 41, title: buildIssueTitle(old), body: buildIssueBody(old, asInstalled()), }, ], }); inTempRepo({ "package-lock.json": lockWith("fast-uri", "3.1.5") }, () => withoutProjectToken(() => captureLog(() => main("o/r", spawn))), ); const edit = ghCall(spawn, "edit"); // Filed as "(1 advisory)"; editing only the body would leave it saying so. assert.match( edit.args[edit.args.indexOf("--title") + 1], /\(2 advisories\)$/, ); }); test("main fails loudly when Dependabot security PRs are back on", () => { const spawn = fakeSpawn({ securityFixes: { enabled: true, paused: false } }); assert.throws( () => captureLog(() => main("o/r", spawn)), /security-update PRs are ENABLED again/, ); assert.equal(spawn.calls.length, 1, "nothing else runs"); }); test("main continues, reporting UNVERIFIED, when the token cannot read the setting", () => { const spawn = fakeSpawn({ securityFixesStatus: 1, securityFixesStderr: "gh: HTTP 403: Resource not accessible by integration", alertPages: [[]], }); const log = captureLog(() => main("o/r", spawn)); assert.ok(log.some((l) => l.includes("UNVERIFIED"))); assert.ok(log.some((l) => l.includes("no open alerts"))); }); test("main throws when the setting lookup fails for a non-permission reason", () => { const spawn = fakeSpawn({ securityFixesStatus: 1, securityFixesStderr: "gh: HTTP 502: Bad Gateway", }); // Swallowing this would exit green having skipped the sweep's own precondition. assert.throws( () => captureLog(() => main("o/r", spawn)), /automated-security-fixes lookup failed.*502/s, ); }); test("main leaves an unmilestoned issue off the board for triage", () => { const spawn = fakeSpawn({ alertPages: [[alert({ ghsa: "GHSA-a" })]], milestone: "", }); const log = inTempRepo( { "package-lock.json": lockWith("fast-uri", "3.1.5") }, () => withProjectToken(() => captureLog(() => main("o/r", spawn))), ); assert.ok(ghCall(spawn, "create")); // `Incoming` <=> no milestone: boarding it at Todo would assert an approval // the invariant reads off the milestone. assert.equal( spawn.calls.find( (c) => c.args[0] === "project" && c.args[1] === "item-add", ), undefined, ); assert.ok(log.some((l) => l.includes("unmilestoned and unboarded"))); }); test("main refreshes an issue whose exposure shrank, without commenting", () => { // Filed when both advisories applied; `v2/main` has since moved to 3.1.0, // which is out of range of the narrow one. Nothing is NEW, so `added` is // empty — but the body still claims an advisory that no longer applies. const [filed] = groupAlerts([ alert({ ghsa: "GHSA-narrow", range: ">= 3.1.3, < 3.1.6" }), alert({ ghsa: "GHSA-wide", range: ">= 3.0.0, < 3.1.6" }), ]); const spawn = fakeSpawn({ alertPages: [ [ alert({ ghsa: "GHSA-narrow", range: ">= 3.1.3, < 3.1.6" }), alert({ ghsa: "GHSA-wide", range: ">= 3.0.0, < 3.1.6" }), ], ], issues: [ { number: 41, title: buildIssueTitle(filed), body: buildIssueBody(filed, asInstalled()), }, ], }); const log = inTempRepo( { "package-lock.json": lockWith("fast-uri", "3.1.0") }, () => withoutProjectToken(() => captureLog(() => main("o/r", spawn))), ); // A comment is for genuinely new advisories, and there are none. assert.equal(ghCall(spawn, "comment"), undefined); const edit = ghCall(spawn, "edit"); assert.ok(edit, "the stale body is rewritten"); const body = edit.args[edit.args.indexOf("--body") + 1]; assert.ok(!body.includes("| [GHSA-narrow]"), "no longer in the table"); assert.match(body, /`3\.1\.0`/, "the affected version is refreshed"); assert.match(edit.args[edit.args.indexOf("--title") + 1], /\(1 advisory\)$/); // The marker stays monotonic: it records what has been announced, so the // dropped advisory cannot be re-announced later. assert.deepEqual(parseMarker(body).ghsas, ["GHSA-narrow", "GHSA-wide"]); assert.ok(log.some((l) => l.includes("refreshed #41"))); }); test("main clears an open issue when the manifest is gone", () => { const [group] = groupAlerts([ alert({ ghsa: "GHSA-a", manifest: "clients/gone/package-lock.json" }), ]); const spawn = fakeSpawn({ alertPages: [ [alert({ ghsa: "GHSA-a", manifest: "clients/gone/package-lock.json" })], ], issues: [ { number: 41, title: buildIssueTitle(group), body: buildIssueBody(group, asInstalled()), }, ], }); const log = inTempRepo({}, () => withoutProjectToken(() => captureLog(() => main("o/r", spawn, "2026-09-04")), ), ); const edit = ghCall(spawn, "edit"); assert.ok(edit, "the stale issue is rewritten, not silently skipped"); const body = edit.args[edit.args.indexOf("--body") + 1]; assert.match(body, /No longer applicable on `v2\/main` as of 2026-09-04/); assert.match(body, /no longer part of this repo/); // The marker survives, so the issue is reused if the advisory comes back. assert.deepEqual(parseMarker(body).ghsas, ["GHSA-a"]); assert.equal(ghCall(spawn, "create"), undefined); assert.ok(log.some((l) => l.includes("cleared #41"))); }); test("main clears an open issue when every copy moved out of range", () => { const [group] = groupAlerts([alert({ ghsa: "GHSA-a" })]); const spawn = fakeSpawn({ alertPages: [[alert({ ghsa: "GHSA-a" })]], issues: [ { number: 41, title: buildIssueTitle(group), body: buildIssueBody(group, asInstalled()), }, ], }); const log = inTempRepo( { "package-lock.json": lockWith("fast-uri", "3.1.6") }, () => withoutProjectToken(() => captureLog(() => main("o/r", spawn, "2026-09-04")), ), ); const edit = ghCall(spawn, "edit"); assert.ok(edit); const body = edit.args[edit.args.indexOf("--body") + 1]; assert.match(body, /every installed copy is out of range \(`3\.1\.6`\)/); assert.ok(log.some((l) => l.includes("cleared #41"))); }); test("main does not re-clear an issue it already cleared", () => { // The second run of a cleared sweep must touch nothing at all. const [group] = groupAlerts([alert({ ghsa: "GHSA-a" })]); const spawn = fakeSpawn({ alertPages: [[alert({ ghsa: "GHSA-a" })]], issues: [ { number: 41, title: buildIssueTitle(group), body: buildClearedBody(group, { ghsas: ["GHSA-a"], reason: "every installed copy is out of range (`3.1.6`)", today: "2026-09-04", }), }, ], }); inTempRepo({ "package-lock.json": lockWith("fast-uri", "3.1.6") }, () => withoutProjectToken(() => captureLog(() => main("o/r", spawn, "2026-09-04")), ), ); assert.equal(ghCall(spawn, "edit"), undefined); assert.equal(ghCall(spawn, "create"), undefined); }); test("main skips quietly when nothing applies and no issue is open", () => { const spawn = fakeSpawn({ alertPages: [[alert({ ghsa: "GHSA-a" })]] }); inTempRepo({ "package-lock.json": lockWith("fast-uri", "3.1.6") }, () => withoutProjectToken(() => captureLog(() => main("o/r", spawn))), ); assert.equal(ghCall(spawn, "edit"), undefined); assert.equal(ghCall(spawn, "create"), undefined); }); test("buildIssueBody counts the applicable alerts in its prose, not the marker", () => { const [group] = groupAlerts([alert({ ghsa: "GHSA-a" })]); // The marker carries a second, since-closed advisory; the prose must not. const body = buildIssueBody(group, { ...nested(), ghsas: ["GHSA-a", "GHSA-closed"], }); assert.match(body, /Filed automatically from 1 open Dependabot alert\b/); assert.deepEqual(parseMarker(body).ghsas, ["GHSA-a", "GHSA-closed"]); }); test("main clears an issue when its last alert is fixed or dismissed", () => { // `openAlerts` asks for state=open, so a fixed alert simply vanishes and its // group is never built. The issue has to be reconciled from the other side. const [group] = groupAlerts([alert({ ghsa: "GHSA-a" })]); const spawn = fakeSpawn({ alertPages: [[]], issues: [ { number: 41, title: buildIssueTitle(group), body: buildIssueBody(group, asInstalled()), }, ], }); const log = inTempRepo( { "package-lock.json": lockWith("fast-uri", "3.1.5") }, () => withoutProjectToken(() => captureLog(() => main("o/r", spawn, "2026-09-04")), ), ); const edit = ghCall(spawn, "edit"); assert.ok(edit, "the zero-alert run still reconciles open issues"); const body = edit.args[edit.args.indexOf("--body") + 1]; assert.match(body, /every alert it tracked has been fixed or dismissed/); assert.deepEqual(parseMarker(body).ghsas, ["GHSA-a"]); assert.ok(log.some((l) => l.includes("fixed or dismissed"))); }); test("main reconciles a vanished group even while other groups remain", () => { // The early return is only half of it: a disappeared group is also never // visited by the loop when the feed still has other bumps in it. const [gone] = groupAlerts([ alert({ ghsa: "GHSA-gone", pkg: "qs", fixed: "6.16.0" }), ]); const spawn = fakeSpawn({ alertPages: [[alert({ ghsa: "GHSA-a" })]], issues: [ { number: 41, title: buildIssueTitle(gone), body: buildIssueBody(gone, { affected: [ { path: "node_modules/qs", version: "6.15.3", hoisted: true }, ], declared: false, }), }, ], }); inTempRepo({ "package-lock.json": lockWith("fast-uri", "3.1.5") }, () => withoutProjectToken(() => captureLog(() => main("o/r", spawn, "2026-09-04")), ), ); assert.ok(ghCall(spawn, "create"), "the live bump is still filed"); const edit = ghCall(spawn, "edit"); assert.ok(edit, "the vanished bump's issue is still cleared"); assert.equal(edit.args[2], "41"); }); test("main does not re-clear a vanished group's issue on the next run", () => { const spawn = fakeSpawn({ alertPages: [[]], issues: [ { number: 41, title: "chore(deps): bump `fast-uri` to `3.1.6` in `package-lock.json` (1 advisory)", body: buildClearedBody( { package: "fast-uri", manifestPath: "package-lock.json", fixedIn: "3.1.6", }, { ghsas: ["GHSA-a"], reason: "every alert it tracked has been fixed or dismissed", today: "2026-09-04", }, ), }, ], }); inTempRepo({}, () => withoutProjectToken(() => captureLog(() => main("o/r", spawn, "2026-09-04")), ), ); assert.equal(ghCall(spawn, "edit"), undefined); }); test("main boards a filed issue at Todo/High using resolved option ids", () => { // The acceptance-critical path: the two field edits that actually place the // card. Previously only its failure modes were covered. const spawn = fakeSpawn({ alertPages: [[alert({ ghsa: "GHSA-a" })]] }); const log = inTempRepo( { "package-lock.json": lockWith("fast-uri", "3.1.5") }, () => withProjectToken(() => captureLog(() => main("o/r", spawn))), ); const add = spawn.calls.find( (c) => c.args[0] === "project" && c.args[1] === "item-add", ); assert.ok(add, "the card is added"); const edits = spawn.calls.filter( (c) => c.args[0] === "project" && c.args[1] === "item-edit", ); assert.equal(edits.length, 2, "Status and Priority are separate calls"); const optionOf = (call) => call.args[call.args.indexOf("--single-select-option-id") + 1]; const fieldOf = (call) => call.args[call.args.indexOf("--field-id") + 1]; assert.deepEqual( edits.map((e) => [fieldOf(e), optionOf(e)]), [ [STATUS_FIELD_ID, "todo-id"], [PRIORITY_FIELD_ID, "high-id"], ], ); // Resolved by NAME at run time, never hardcoded. assert.ok( spawn.calls.some( (c) => c.args[0] === "project" && c.args[1] === "field-list", ), ); assert.ok(log.some((l) => l.includes("boarded"))); }); test("overrideAncestors reads the parent chain, scoped names included", () => { assert.deepEqual( overrideAncestors("node_modules/ajv/node_modules/fast-uri"), ["ajv"], ); assert.deepEqual( overrideAncestors( "node_modules/@sc/a/node_modules/b/node_modules/fast-uri", ), ["@sc/a", "b"], ); assert.deepEqual(overrideAncestors("node_modules/fast-uri"), []); }); test("scopedOverrideExample nests each vulnerable copy under its parents", () => { const json = scopedOverrideExample( [ { path: "node_modules/fast-uri", hoisted: true }, { path: "node_modules/ajv/node_modules/fast-uri", hoisted: false }, { path: "node_modules/@sc/x/node_modules/fast-uri", hoisted: false }, ], { package: "fast-uri", fixedIn: "3.1.6" }, ); // The hoisted copy is the declared one and gets no override entry. assert.deepEqual(JSON.parse(json), { overrides: { ajv: { "fast-uri": "3.1.6" }, "@sc/x": { "fast-uri": "3.1.6" }, }, }); }); test("groupAlerts records the ecosystem so non-npm alerts are identifiable", () => { const [docker] = groupAlerts([ alert({ ghsa: "GHSA-d", pkg: "node", manifest: "Dockerfile", ecosystem: "docker", }), ]); assert.equal(docker.ecosystem, "docker"); assert.notEqual(docker.ecosystem, SUPPORTED_ECOSYSTEM); }); test("main skips a non-npm alert loudly instead of crashing on its manifest", () => { // ⚠️ This repo has a Dockerfile, so this is reachable. Parsing it as a // lockfile threw and aborted the entire sweep before any npm group ran. const spawn = fakeSpawn({ alertPages: [ [ alert({ ghsa: "GHSA-docker", pkg: "node", manifest: "Dockerfile", ecosystem: "docker", }), alert({ ghsa: "GHSA-a" }), ], ], }); const log = inTempRepo( { "package-lock.json": lockWith("fast-uri", "3.1.5"), }, () => withoutProjectToken(() => captureLog(() => main("o/r", spawn))), ); // The npm bump is still filed — the non-npm alert must not abort the run. const created = ghCalls(spawn, "create"); assert.equal(created.length, 1); assert.match( created[0].args[created[0].args.indexOf("--title") + 1], /`fast-uri`/, ); // ...and the skipped one is named, with its GHSA, so a human can file it. assert.ok( log.some( (l) => l.includes("docker") && l.includes("Dockerfile") && l.includes("GHSA-docker") && l.includes("raise it by hand"), ), `expected a loud skip line, got: ${log.join(" | ")}`, ); }); test("main says superseded, not fixed, when the GHSA is still open elsewhere", () => { // GitHub revised `first_patched_version`, so the advisory moved to a new key // while staying open. Calling that "fixed or dismissed" would stand down a // live exposure. const [filed] = groupAlerts([alert({ ghsa: "GHSA-a", fixed: "3.1.6" })]); const spawn = fakeSpawn({ alertPages: [[alert({ ghsa: "GHSA-a", fixed: "3.1.7", range: "< 3.1.7" })]], issues: [ { number: 41, title: buildIssueTitle(filed), body: buildIssueBody(filed, asInstalled()), }, ], }); inTempRepo({ "package-lock.json": lockWith("fast-uri", "3.1.5") }, () => withoutProjectToken(() => captureLog(() => main("o/r", spawn, "2026-09-04")), ), ); const edit = ghCall(spawn, "edit"); assert.ok(edit); const body = edit.args[edit.args.indexOf("--body") + 1]; assert.match(body, /superseded/); assert.match(body, /`GHSA-a` is still open/); assert.doesNotMatch(body, /fixed or dismissed/); // ...and the new bump gets its own issue. assert.ok(ghCall(spawn, "create")); }); test("buildIssueBody does not claim security PRs are off when unverified", () => { const [group] = groupAlerts([alert({ ghsa: "GHSA-a" })]); const verified = buildIssueBody(group, nested()); assert.match(verified, /Dependabot opens no security-update PRs/); const unverified = buildIssueBody(group, { ...nested(), securityPrsOff: false, }); assert.doesNotMatch(unverified, /opens no security-update PRs/); assert.match(unverified, /The fix is written by hand/); }); test("main omits the security-PR claim when the token could not read it", () => { const spawn = fakeSpawn({ securityFixesStatus: 1, securityFixesStderr: "gh: HTTP 403: Resource not accessible by integration", alertPages: [[alert({ ghsa: "GHSA-a" })]], }); const log = inTempRepo( { "package-lock.json": lockWith("fast-uri", "3.1.5") }, () => withoutProjectToken(() => captureLog(() => main("o/r", spawn))), ); assert.ok(log.some((l) => l.includes("UNVERIFIED"))); const create = ghCall(spawn, "create"); assert.ok(create); assert.doesNotMatch( create.args[create.args.indexOf("--body") + 1], /opens no security-update PRs/, ); }); test("main will not stand down an open alert that lost its patched version", () => { // groupAlerts drops an alert with no first_patched_version, so the bump's key // disappears — but the advisory is still OPEN. Reading the open-GHSA set from // the groups instead of the raw feed would report it as fixed or dismissed. const [filed] = groupAlerts([alert({ ghsa: "GHSA-a" })]); const unpatched = alert({ ghsa: "GHSA-a" }); unpatched.security_vulnerability.first_patched_version = null; const spawn = fakeSpawn({ alertPages: [[unpatched]], issues: [ { number: 41, title: buildIssueTitle(filed), body: buildIssueBody(filed, asInstalled()), }, ], }); const log = inTempRepo( { "package-lock.json": lockWith("fast-uri", "3.1.5") }, () => withoutProjectToken(() => captureLog(() => main("o/r", spawn, "2026-09-04")), ), ); // Nothing to bump to means no replacement issue exists, so "superseded" would // be false and clearing would stand down a live exposure nothing is tracking. // The correct move is to leave the issue exactly as it is. assert.equal(ghCall(spawn, "edit"), undefined); assert.ok( log.some( (l) => l.includes("left as is") && l.includes("GHSA-a") && l.includes("could not establish a replacement"), ), `expected a left-as-is line, got: ${log.join(" | ")}`, ); }); test("a cleared issue is not re-edited on a LATER day", () => { // ⚠️ The bug the same-date no-reclear test could never catch: a cleared issue // stays open, so the sweep sees it again tomorrow. With today's date rendered // into the body, every cleared issue would be edited once a day forever. const [group] = groupAlerts([alert({ ghsa: "GHSA-a" })]); const cleared = buildClearedBody( { package: "fast-uri", manifestPath: "package-lock.json", fixedIn: "3.1.6", }, { ghsas: ["GHSA-a"], reason: "every alert it tracked has been fixed or dismissed", today: "2026-09-04", }, ); const spawn = fakeSpawn({ alertPages: [[]], issues: [{ number: 41, title: buildIssueTitle(group), body: cleared }], }); inTempRepo({}, () => withoutProjectToken(() => // A DIFFERENT day from the one the body records. captureLog(() => main("o/r", spawn, "2026-09-11")), ), ); assert.equal(ghCall(spawn, "edit"), undefined); }); test("a cleared issue takes the new date when its reason changes", () => { // A real change still gets one edit — and takes the new date, since the state // genuinely changed on that day. const cleared = buildClearedBody( { package: "fast-uri", manifestPath: "package-lock.json", fixedIn: "3.1.6", }, { ghsas: ["GHSA-a"], reason: "an older reason", today: "2026-09-04" }, ); const spawn = fakeSpawn({ alertPages: [[]], issues: [{ number: 41, title: "t", body: cleared }], }); inTempRepo({}, () => withoutProjectToken(() => captureLog(() => main("o/r", spawn, "2026-09-11")), ), ); const edit = ghCall(spawn, "edit"); assert.ok(edit, "a changed reason is still written"); assert.equal( parseClearedDate(edit.args[edit.args.indexOf("--body") + 1]), "2026-09-11", ); }); test("parseClearedDate reads the date back, and only from a cleared body", () => { const body = buildClearedBody( { package: "p", manifestPath: "package-lock.json", fixedIn: "1.0.0" }, { ghsas: ["GHSA-a"], reason: "why", today: "2026-09-04" }, ); assert.equal(parseClearedDate(body), "2026-09-04"); assert.equal(parseClearedDate("an ordinary issue body"), null); assert.equal(parseClearedDate(undefined), null); }); test("main does not clear an issue when the lockfile cannot be parsed", () => { // A malformed lockfile is evidence of nothing. Treating it like an absent one // would stand down a live alert on the strength of a read error. const [group] = groupAlerts([alert({ ghsa: "GHSA-a" })]); const spawn = fakeSpawn({ alertPages: [[alert({ ghsa: "GHSA-a" })]], issues: [ { number: 41, title: buildIssueTitle(group), body: buildIssueBody(group, asInstalled()), }, ], }); const dir = mkdtempSync(join(tmpdir(), "dependabot-alerts-")); const cwd = process.cwd(); let log; try { writeFileSync(join(dir, "package-lock.json"), "{ truncated…"); process.chdir(dir); log = withoutProjectToken(() => captureLog(() => main("o/r", spawn, "2026-09-04")), ); } finally { process.chdir(cwd); rmSync(dir, { recursive: true, force: true }); } assert.equal(ghCall(spawn, "edit"), undefined, "the issue is left alone"); assert.equal(ghCall(spawn, "create"), undefined); assert.ok( log.some( (l) => l.includes("could not be parsed") && l.includes("WITHOUT clearing"), ), `expected a parse-failure line, got: ${log.join(" | ")}`, ); }); test("advisoryKey distinguishes the same GHSA in different manifests", () => { assert.notEqual( advisoryKey("fast-uri", "package-lock.json", "GHSA-a"), advisoryKey("fast-uri", "clients/tui/package-lock.json", "GHSA-a"), ); }); test("another manifest's alert cannot vouch for this one when clearing", () => { // The same GHSA legitimately covers the root install and a client's. Here the // ROOT alert lost its patched version while the TUI one is still filable — // keying on the GHSA alone would let the TUI group vouch for the root issue // and clear it, even though the root alert is open with no bump available. const [rootFiled] = groupAlerts([alert({ ghsa: "GHSA-a" })]); const rootUnpatched = alert({ ghsa: "GHSA-a" }); rootUnpatched.security_vulnerability.first_patched_version = null; const spawn = fakeSpawn({ alertPages: [ [ rootUnpatched, alert({ ghsa: "GHSA-a", manifest: "clients/tui/package-lock.json", range: "< 3.1.6", }), ], ], issues: [ { number: 41, title: buildIssueTitle(rootFiled), body: buildIssueBody(rootFiled, asInstalled()), }, ], }); const log = inTempRepo( { "package-lock.json": lockWith("fast-uri", "3.1.5"), "clients/tui/package-lock.json": lockWith("fast-uri", "3.1.5"), }, () => withoutProjectToken(() => captureLog(() => main("o/r", spawn, "2026-09-04")), ), ); // The TUI bump is filed; the root issue is left alone, not cleared. assert.ok(ghCall(spawn, "create")); assert.equal(ghCall(spawn, "edit"), undefined); assert.ok( log.some((l) => l.includes("#41 left as is")), `expected the root issue left as is, got: ${log.join(" | ")}`, ); }); test("a revised bump does not clear the old issue when the probe is indeterminate", () => { // GitHub revised first_patched_version, so the old key vanished — but the // replacement group was skipped because the lockfile would not parse. Nothing // was established, so "superseded, it has its own issue" would be a guess. const [filed] = groupAlerts([alert({ ghsa: "GHSA-a", fixed: "3.1.6" })]); const spawn = fakeSpawn({ alertPages: [[alert({ ghsa: "GHSA-a", fixed: "3.1.7", range: "< 3.1.7" })]], issues: [ { number: 41, title: buildIssueTitle(filed), body: buildIssueBody(filed, asInstalled()), }, ], }); const dir = mkdtempSync(join(tmpdir(), "dependabot-alerts-")); const cwd = process.cwd(); let log; try { writeFileSync(join(dir, "package-lock.json"), "{ truncated…"); process.chdir(dir); log = withoutProjectToken(() => captureLog(() => main("o/r", spawn, "2026-09-04")), ); } finally { process.chdir(cwd); rmSync(dir, { recursive: true, force: true }); } assert.equal(ghCall(spawn, "edit"), undefined, "the old issue is preserved"); assert.equal(ghCall(spawn, "create"), undefined); assert.ok( log.some((l) => l.includes("could not establish a replacement")), `expected an indeterminate line, got: ${log.join(" | ")}`, ); }); test("a revised bump that is no longer exposed clears without claiming an issue", () => { // Same revision, but the probe DID run and found nothing in range. That is // positive evidence, so the old issue clears — saying exposure is gone rather // than claiming a replacement issue that was never filed. const [filed] = groupAlerts([alert({ ghsa: "GHSA-a", fixed: "3.1.6" })]); const spawn = fakeSpawn({ alertPages: [ [alert({ ghsa: "GHSA-a", fixed: "3.1.7", range: ">= 3.1.6, < 3.1.7" })], ], issues: [ { number: 41, title: buildIssueTitle(filed), body: buildIssueBody(filed, asInstalled()), }, ], }); inTempRepo({ "package-lock.json": lockWith("fast-uri", "3.1.5") }, () => withoutProjectToken(() => captureLog(() => main("o/r", spawn, "2026-09-04")), ), ); const edit = ghCall(spawn, "edit"); assert.ok(edit); const body = edit.args[edit.args.indexOf("--body") + 1]; assert.match(body, /no installed copy is in range/); assert.doesNotMatch(body, /has their own issue|superseded/); }); test("main reads every page of open dependabot issues", () => { // The second page holds the matching marker. Truncating the lookup would // file a duplicate issue rather than recognising this one. const [group] = groupAlerts([alert({ ghsa: "GHSA-a" })]); const spawn = fakeSpawn({ alertPages: [[alert({ ghsa: "GHSA-a" })]], issues: [ [{ number: 1, body: "an unrelated dependabot issue" }], [ { number: 41, title: buildIssueTitle(group), body: buildIssueBody(group, asInstalled()), }, ], ], }); const log = inTempRepo( { "package-lock.json": lockWith("fast-uri", "3.1.5") }, () => withoutProjectToken(() => captureLog(() => main("o/r", spawn))), ); assert.equal(ghCall(spawn, "create"), undefined); assert.ok(log.some((l) => l.includes("#41 is up to date"))); }); test("main drops a pull request returned by the issues endpoint", () => { // `/issues` returns PRs too; one carrying no marker must not be mistaken for // a match, nor crash the lookup. const spawn = fakeSpawn({ alertPages: [[alert({ ghsa: "GHSA-a" })]], issues: [[{ number: 9, body: "a PR body", pull_request: { url: "..." } }]], }); inTempRepo({ "package-lock.json": lockWith("fast-uri", "3.1.5") }, () => withoutProjectToken(() => captureLog(() => main("o/r", spawn))), ); assert.ok(ghCall(spawn, "create"), "the issue is still filed"); }); test("main files an issue naming only the advisories that apply here", () => { const spawn = fakeSpawn({ alertPages: [ [ alert({ ghsa: "GHSA-narrow", range: ">= 3.1.3, < 3.1.6" }), alert({ ghsa: "GHSA-wide", range: ">= 3.0.0, < 3.1.6" }), ], ], }); inTempRepo({ "package-lock.json": lockWith("fast-uri", "3.1.0") }, () => withoutProjectToken(() => captureLog(() => main("o/r", spawn))), ); const create = ghCall(spawn, "create"); const body = create.args[create.args.indexOf("--body") + 1]; assert.deepEqual(parseMarker(body).ghsas, ["GHSA-wide"]); assert.ok(!body.includes("GHSA-narrow"), "3.1.0 is out of that range"); assert.match( create.args[create.args.indexOf("--title") + 1], /\(1 advisory\)$/, ); }); test("main fails the run when a card is added but its fields are not set", () => { const spawn = fakeSpawn({ alertPages: [ [ alert({ ghsa: "GHSA-a" }), alert({ ghsa: "GHSA-b", pkg: "qs", fixed: "6.16.0", range: "< 6.16.0", }), ], ], boardEditStatus: 1, }); assert.throws( () => inTempRepo( { "package-lock.json": { lockfileVersion: 3, packages: { "": { dependencies: {} }, "node_modules/fast-uri": { version: "3.1.5" }, "node_modules/qs": { version: "6.15.3" }, }, }, }, () => withProjectToken(() => captureLog(() => main("o/r", spawn))), ), /incomplete board placement/, ); // A half-placed card is worth failing over — but not before both issues exist. assert.equal(ghCalls(spawn, "create").length, 2); });