mirror of
https://github.com/modelcontextprotocol/servers.git
synced 2026-10-21 06:30:00 +08:00
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: cliffhall <cliff@futurescale.com>
661 lines
20 KiB
JavaScript
661 lines
20 KiB
JavaScript
// Tests for the monthly dependency sweep (#4874). The parsers and builders
|
|
// are driven directly; `main()` runs against a throwaway tree with `npm`,
|
|
// `uv` and `gh` replaced by a fake spawn, so filing, updating, clearing, the
|
|
// no-op paths, the failure paths and the dry run are all covered without
|
|
// touching the real tracker. 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 { tmpdir } from "node:os";
|
|
import path from "node:path";
|
|
import {
|
|
ISSUE_MARKER,
|
|
ISSUE_TITLE,
|
|
buildClearedBody,
|
|
buildIssueBody,
|
|
highestVersionTag,
|
|
installLabel,
|
|
isActionStale,
|
|
main,
|
|
parseActionRefs,
|
|
parseNpmOutdated,
|
|
parseUvDryRun,
|
|
parseUvTree,
|
|
parseVersionRef,
|
|
staleActions,
|
|
unrankedPins,
|
|
uvRows,
|
|
workspacePackageNames,
|
|
} from "./dependency-refresh.mjs";
|
|
|
|
const SHA = "d23441a48e516b6c34aea4fa41551a30e30af803";
|
|
const bot = { login: "app/github-actions", is_bot: true };
|
|
const sweepLabels = [
|
|
{ name: "v2" },
|
|
{ name: "chore" },
|
|
{ name: "dependencies" },
|
|
];
|
|
|
|
test("installLabel maps npm's dependent onto a manifest", () => {
|
|
assert.equal(installLabel("everything"), "src/everything");
|
|
assert.equal(installLabel("servers"), "root");
|
|
assert.equal(installLabel("mcp-servers-4874"), "root");
|
|
assert.equal(installLabel(undefined), "root");
|
|
// A package-name spelling is mapped too, rather than folded into root.
|
|
const names = {
|
|
"@modelcontextprotocol/server-sequential-thinking":
|
|
"src/sequentialthinking",
|
|
};
|
|
assert.equal(
|
|
installLabel("@modelcontextprotocol/server-sequential-thinking", names),
|
|
"src/sequentialthinking",
|
|
);
|
|
const byInstall = parseNpmOutdated(
|
|
JSON.stringify({
|
|
chalk: {
|
|
current: "5.6.2",
|
|
wanted: "5.6.2",
|
|
latest: "6.0.1",
|
|
dependent: "@modelcontextprotocol/server-sequential-thinking",
|
|
},
|
|
}),
|
|
names,
|
|
);
|
|
assert.deepEqual([...byInstall.keys()], ["src/sequentialthinking"]);
|
|
});
|
|
|
|
test("workspacePackageNames reads each workspace manifest", (t) => {
|
|
const root = mkdtempSync(path.join(tmpdir(), "dependency-refresh-ws-"));
|
|
t.after(() => rmSync(root, { recursive: true, force: true }));
|
|
mkdirSync(path.join(root, "src/memory"), { recursive: true });
|
|
writeFileSync(
|
|
path.join(root, "src/memory/package.json"),
|
|
JSON.stringify({ name: "@modelcontextprotocol/server-memory" }),
|
|
);
|
|
mkdirSync(path.join(root, "src/time"), { recursive: true });
|
|
writeFileSync(path.join(root, "src/time/package.json"), "{}");
|
|
assert.deepEqual(workspacePackageNames(root), {
|
|
"@modelcontextprotocol/server-memory": "src/memory",
|
|
});
|
|
});
|
|
|
|
test("parseNpmOutdated splits by dependent and dedupes per manifest", () => {
|
|
const byInstall = parseNpmOutdated(
|
|
JSON.stringify({
|
|
zod: [
|
|
{
|
|
current: "4.4.3",
|
|
wanted: "4.6.5",
|
|
latest: "4.6.5",
|
|
dependent: "memory",
|
|
},
|
|
{
|
|
current: "4.4.3",
|
|
wanted: "4.6.5",
|
|
latest: "4.6.5",
|
|
dependent: "everything",
|
|
},
|
|
],
|
|
diff: {
|
|
current: "8.0.4",
|
|
wanted: "8.0.4",
|
|
latest: "9.0.0",
|
|
dependent: "filesystem",
|
|
},
|
|
eslint: [
|
|
{
|
|
current: "10.0.0",
|
|
wanted: "10.1.0",
|
|
latest: "10.1.0",
|
|
dependent: "servers",
|
|
},
|
|
{
|
|
current: "10.0.0",
|
|
wanted: "10.1.0",
|
|
latest: "10.1.0",
|
|
dependent: "servers",
|
|
},
|
|
],
|
|
gone: { latest: "1.0.0", dependent: "memory" },
|
|
}),
|
|
);
|
|
assert.deepEqual([...byInstall.keys()].sort(), [
|
|
"root",
|
|
"src/everything",
|
|
"src/filesystem",
|
|
"src/memory",
|
|
]);
|
|
assert.equal(byInstall.get("root").length, 1);
|
|
assert.deepEqual(
|
|
byInstall.get("src/memory").map((r) => r.name),
|
|
["gone", "zod"],
|
|
);
|
|
assert.equal(byInstall.get("src/memory")[0].current, "(missing)");
|
|
assert.equal(parseNpmOutdated(" ").size, 0);
|
|
});
|
|
|
|
test("parseUvDryRun reads updates, additions and removals", () => {
|
|
assert.deepEqual(
|
|
parseUvDryRun(
|
|
[
|
|
"warning: something",
|
|
"Resolved 52 packages in 638ms",
|
|
"Update anyio v4.6.2.post1 -> v4.15.1",
|
|
"Add newdep v1.0.0",
|
|
"Remove sniffio v1.3.1",
|
|
].join("\n"),
|
|
),
|
|
[
|
|
{ name: "anyio", current: "4.6.2.post1", wanted: "4.15.1" },
|
|
{ name: "newdep", current: "(not locked)", wanted: "1.0.0" },
|
|
{ name: "sniffio", current: "1.3.1", wanted: "(removed)" },
|
|
],
|
|
);
|
|
});
|
|
|
|
test("parseUvTree keeps behind direct dependencies, without extras", () => {
|
|
const tree = parseUvTree(
|
|
[
|
|
"mcp-server-fetch v0.6.3",
|
|
"├── httpx[socks] v0.27.2 (latest: v0.28.1)",
|
|
"├── mcp v1.29.0 (latest: v2.3.0)",
|
|
"├── pytest-cov v7.1.0 (group: dev)",
|
|
"└── ruff v0.8.1 (group: dev) (latest: v0.16.10)",
|
|
].join("\n"),
|
|
);
|
|
assert.deepEqual([...tree.keys()], ["httpx", "mcp", "ruff"]);
|
|
assert.deepEqual(tree.get("ruff"), { current: "0.8.1", latest: "0.16.10" });
|
|
});
|
|
|
|
test("uvRows merges the lock refresh with the direct latest", () => {
|
|
const rows = uvRows(
|
|
[
|
|
{ name: "mcp", current: "1.29.0", wanted: "1.30.0" },
|
|
{ name: "idna", current: "3.18", wanted: "3.20" },
|
|
],
|
|
new Map([
|
|
["mcp", { current: "1.29.0", latest: "2.3.0" }],
|
|
["markdownify", { current: "0.14.1", latest: "1.2.3" }],
|
|
]),
|
|
);
|
|
assert.deepEqual(rows, [
|
|
{ name: "idna", current: "3.18", wanted: "3.20", latest: "—" },
|
|
{
|
|
name: "markdownify",
|
|
current: "0.14.1",
|
|
wanted: "0.14.1",
|
|
latest: "1.2.3",
|
|
},
|
|
{ name: "mcp", current: "1.29.0", wanted: "1.30.0", latest: "2.3.0" },
|
|
]);
|
|
});
|
|
|
|
test("parseActionRefs reads tags, SHA pins with their release, and skips the rest", () => {
|
|
const refs = parseActionRefs(
|
|
[
|
|
"steps:",
|
|
" - uses: actions/checkout@v6",
|
|
` uses: actions/checkout@${SHA} # v6.1.0`,
|
|
` - uses: "actions/setup-node@v7"`,
|
|
" - uses: 'owner/repo/sub@v2' # a comment",
|
|
` - uses: actions/cache@${SHA}`,
|
|
` - uses: actions/coarse@${SHA} # v7`,
|
|
" - uses: ./local-action",
|
|
" - uses: docker://alpine:3",
|
|
" - uses: no-ref",
|
|
" # uses: commented@v1",
|
|
].join("\n"),
|
|
);
|
|
assert.deepEqual(refs, [
|
|
{ action: "actions/checkout", ref: "v6" },
|
|
{ action: "actions/checkout", ref: SHA, version: "v6.1.0" },
|
|
{ action: "actions/setup-node", ref: "v7" },
|
|
{ action: "owner/repo/sub", ref: "v2" },
|
|
{ action: "actions/cache", ref: SHA },
|
|
// `# v7` names no exact release, so it does not rank the pin.
|
|
{ action: "actions/coarse", ref: SHA },
|
|
]);
|
|
assert.deepEqual(unrankedPins(refs), [
|
|
"actions/cache@d23441a",
|
|
"actions/coarse@d23441a",
|
|
]);
|
|
});
|
|
|
|
test("isActionStale compares at the precision the ref names", () => {
|
|
assert.deepEqual(parseVersionRef("v7.0.1"), [7, 0, 1]);
|
|
assert.equal(parseVersionRef(SHA), null);
|
|
assert.equal(isActionStale("v7", "v7.0.1"), false);
|
|
assert.equal(isActionStale("v7", "v8.0.0"), true);
|
|
assert.equal(isActionStale("v7.0.0", "v7.0.1"), true);
|
|
assert.equal(isActionStale("v8", "v7.9.9"), false);
|
|
assert.equal(isActionStale("main", "v1"), false);
|
|
});
|
|
|
|
test("staleActions ranks SHA pins by their comment and dedupes", () => {
|
|
const stale = staleActions(
|
|
[
|
|
{ action: "actions/checkout", ref: "v6" },
|
|
{ action: "actions/checkout", ref: "v6" },
|
|
{ action: "actions/checkout", ref: SHA, version: "v6.1.0" },
|
|
{ action: "actions/setup-node", ref: "v7" },
|
|
{ action: "unknown/action", ref: "v1" },
|
|
],
|
|
{
|
|
"actions/checkout": "v7.0.0",
|
|
"actions/setup-node": "v7.0.0",
|
|
"unknown/action": null,
|
|
},
|
|
);
|
|
assert.deepEqual(stale, [
|
|
{ action: "actions/checkout", current: "v6", latest: "v7.0.0" },
|
|
{
|
|
action: "actions/checkout",
|
|
current: "v6.1.0 (`d23441a`)",
|
|
latest: "v7.0.0",
|
|
},
|
|
]);
|
|
});
|
|
|
|
test("highestVersionTag is the greatest version, not the newest release", () => {
|
|
assert.equal(
|
|
highestVersionTag(["v6.9.1", "v8.0.0", "v7.2.0", "nightly"]),
|
|
"v8.0.0",
|
|
);
|
|
assert.equal(highestVersionTag(["v1", "v1.0.1"]), "v1.0.1");
|
|
assert.equal(highestVersionTag(["nightly"]), null);
|
|
});
|
|
|
|
test("buildIssueBody is null when nothing is behind, and lists each half", () => {
|
|
assert.equal(
|
|
buildIssueBody({
|
|
npm: [{ label: "root", packages: [] }],
|
|
uv: [],
|
|
actions: [],
|
|
}),
|
|
null,
|
|
);
|
|
const body = buildIssueBody({
|
|
npm: [
|
|
{
|
|
label: "src/memory",
|
|
packages: [
|
|
{ name: "zod", current: "4.4.3", wanted: "4.6.5", latest: "4.6.5" },
|
|
],
|
|
},
|
|
],
|
|
uv: [
|
|
{
|
|
label: "src/time",
|
|
packages: [
|
|
{ name: "mcp", current: "1.29.0", wanted: "1.30.0", latest: "2.3.0" },
|
|
],
|
|
},
|
|
],
|
|
actions: [{ action: "actions/checkout", current: "v6", latest: "v7.0.0" }],
|
|
unranked: ["actions/cache@d23441a"],
|
|
});
|
|
assert.ok(body.startsWith(ISSUE_MARKER));
|
|
for (const needle of [
|
|
"## npm",
|
|
"### `src/memory`",
|
|
"| `zod` | 4.4.3 | 4.6.5 | 4.6.5 |",
|
|
"## uv",
|
|
"### `src/time/uv.lock`",
|
|
"| `mcp` | 1.29.0 | 1.30.0 | 2.3.0 |",
|
|
"## GitHub Actions",
|
|
"| `actions/checkout` | v6 | v7.0.0 |",
|
|
"`actions/cache@d23441a`",
|
|
]) {
|
|
assert.ok(body.includes(needle), needle);
|
|
}
|
|
const actionsOnly = buildIssueBody({
|
|
npm: [],
|
|
uv: [],
|
|
actions: [{ action: "a/b", current: "v1", latest: "v2" }],
|
|
});
|
|
assert.ok(!actionsOnly.includes("## npm"));
|
|
assert.ok(!actionsOnly.includes("## uv"));
|
|
});
|
|
|
|
test("buildIssueBody files when unranked pins are the only finding", () => {
|
|
const body = buildIssueBody({
|
|
npm: [],
|
|
uv: [],
|
|
actions: [],
|
|
unranked: ["actions/cache@d23441a"],
|
|
});
|
|
assert.ok(body?.startsWith(ISSUE_MARKER));
|
|
assert.match(body, /`actions\/cache@d23441a`/);
|
|
assert.ok(!body.includes("## GitHub Actions"));
|
|
});
|
|
|
|
test("buildClearedBody keeps the marker and speaks for every ecosystem", () => {
|
|
const body = buildClearedBody("2026-11-01");
|
|
assert.ok(body.startsWith(ISSUE_MARKER));
|
|
assert.match(body, /2026-11-01/);
|
|
assert.match(body, /npm/);
|
|
assert.match(body, /uv\.lock/);
|
|
assert.match(body, /workflow/);
|
|
});
|
|
|
|
/** A checkout with workflows and one Python server. */
|
|
function fixtureRoot(t) {
|
|
const root = mkdtempSync(path.join(tmpdir(), "dependency-refresh-"));
|
|
t.after(() => rmSync(root, { recursive: true, force: true }));
|
|
mkdirSync(path.join(root, ".github/workflows"), { recursive: true });
|
|
writeFileSync(
|
|
path.join(root, ".github/workflows/ci.yml"),
|
|
`jobs:\n a:\n steps:\n - uses: actions/checkout@v6\n - uses: actions/setup-node@${SHA} # v7.0.0\n`,
|
|
);
|
|
writeFileSync(
|
|
path.join(root, ".github/workflows/notes.md"),
|
|
"uses: ignored/thing@v1\n",
|
|
);
|
|
mkdirSync(path.join(root, "src/time"), { recursive: true });
|
|
writeFileSync(path.join(root, "src/time/uv.lock"), "");
|
|
return root;
|
|
}
|
|
|
|
/**
|
|
* A fake for every command `main()` runs. `state` steers it; `calls` records
|
|
* what reached it.
|
|
*/
|
|
function fakeTools(state = {}) {
|
|
const calls = [];
|
|
const s = {
|
|
outdated: {
|
|
status: 1,
|
|
stdout: JSON.stringify({
|
|
zod: {
|
|
current: "4.4.3",
|
|
wanted: "4.6.5",
|
|
latest: "4.6.5",
|
|
dependent: "memory",
|
|
},
|
|
}),
|
|
},
|
|
uvLock: { status: 0, stderr: "Update mcp v1.29.0 -> v1.30.0\n" },
|
|
uvTree: { status: 0, stdout: "├── mcp v1.29.0 (latest: v2.3.0)\n" },
|
|
releases: {
|
|
"actions/checkout": "v6.1.0\nv7.0.0\n",
|
|
"actions/setup-node": "v7.0.0\n",
|
|
},
|
|
issues: [],
|
|
milestones: [
|
|
{ title: "v2.0.0", state: "open", due_on: "2026-10-17T00:00:00Z" },
|
|
],
|
|
...state,
|
|
};
|
|
const spawn = (cmd, args, opts) => {
|
|
calls.push({ cmd, args, input: opts?.input, cwd: opts?.cwd });
|
|
const ok = (extra) => ({ status: 0, stdout: "", stderr: "", ...extra });
|
|
if (cmd === "npm") return ok(s.outdated);
|
|
if (cmd === "uv") return ok(args[0] === "lock" ? s.uvLock : s.uvTree);
|
|
if (cmd !== "gh") throw new Error(`unexpected ${cmd}`);
|
|
if (
|
|
args[0] === "api" &&
|
|
args.includes("--paginate") &&
|
|
/releases/.test(args[2])
|
|
) {
|
|
const repo = args[2].split("/").slice(1, 3).join("/");
|
|
if (s.releasesFail) return ok({ status: 1, stderr: "HTTP 404" });
|
|
return ok({ stdout: s.releases[repo] ?? "" });
|
|
}
|
|
if (args[0] === "api" && /milestones/.test(args[1])) {
|
|
return ok({ stdout: JSON.stringify(s.milestones) });
|
|
}
|
|
if (args[0] === "issue" && args[1] === "list") {
|
|
return ok({ stdout: JSON.stringify(s.issues) });
|
|
}
|
|
if (args[0] === "issue" && args[1] === "create") {
|
|
return ok({ stdout: "https://github.com/o/r/issues/99\n" });
|
|
}
|
|
if (args[0] === "issue" && args[1] === "edit") return ok();
|
|
throw new Error(`unexpected gh ${args.join(" ")}`);
|
|
};
|
|
return { spawn, calls };
|
|
}
|
|
|
|
const writes = (calls) =>
|
|
calls.filter(
|
|
(c) =>
|
|
c.cmd === "gh" &&
|
|
c.args[0] === "issue" &&
|
|
["create", "edit", "comment"].includes(c.args[1]),
|
|
);
|
|
|
|
test("main files one milestoned, labeled issue covering npm, uv and Actions", (t) => {
|
|
const root = fixtureRoot(t);
|
|
const { spawn, calls } = fakeTools();
|
|
main({ repo: "o/r", root, spawn, dryRun: false, log: () => {} });
|
|
|
|
const npm = calls.find((c) => c.cmd === "npm");
|
|
assert.deepEqual(npm.args, [
|
|
"outdated",
|
|
"--json",
|
|
"--workspaces",
|
|
"--include-workspace-root",
|
|
]);
|
|
const uv = calls.filter((c) => c.cmd === "uv");
|
|
assert.deepEqual(
|
|
uv.map((c) => c.args[0]),
|
|
["lock", "tree"],
|
|
);
|
|
assert.ok(uv[0].args.includes("--dry-run"));
|
|
assert.equal(uv[0].cwd, path.join(root, "src/time"));
|
|
|
|
const [create] = writes(calls);
|
|
assert.deepEqual(create.args.slice(0, 2), ["issue", "create"]);
|
|
assert.ok(create.args.includes(ISSUE_TITLE));
|
|
assert.deepEqual(
|
|
create.args.filter((_, i) => create.args[i - 1] === "--label"),
|
|
["v2", "chore", "dependencies"],
|
|
);
|
|
assert.equal(create.args[create.args.indexOf("--milestone") + 1], "v2.0.0");
|
|
assert.match(create.input, /\| `zod` \| 4\.4\.3/);
|
|
assert.match(create.input, /\| `mcp` \| 1\.29\.0 \| 1\.30\.0 \| 2\.3\.0 \|/);
|
|
assert.match(create.input, /\| `actions\/checkout` \| v6 \| v7\.0\.0 \|/);
|
|
// The SHA pin's comment is v7.0.0, and so is the latest: current.
|
|
assert.ok(!create.input.includes("actions/setup-node"));
|
|
});
|
|
|
|
test("a dry run reads everything and writes nothing", (t) => {
|
|
const root = fixtureRoot(t);
|
|
const { spawn, calls } = fakeTools();
|
|
const lines = [];
|
|
main({ repo: "o/r", root, spawn, dryRun: true, log: (l) => lines.push(l) });
|
|
assert.equal(writes(calls).length, 0);
|
|
const out = lines.join("\n");
|
|
assert.match(out, /would create an issue/);
|
|
assert.match(out, /labels: v2, chore, dependencies/);
|
|
assert.match(out, /milestone: v2\.0\.0/);
|
|
assert.match(out, new RegExp(ISSUE_MARKER));
|
|
});
|
|
|
|
test("main updates the open sweep issue instead of filing another", (t) => {
|
|
const root = fixtureRoot(t);
|
|
const { spawn, calls } = fakeTools({
|
|
issues: [
|
|
{
|
|
number: 7,
|
|
title: ISSUE_TITLE,
|
|
body: `${ISSUE_MARKER}\nold`,
|
|
state: "OPEN",
|
|
author: bot,
|
|
labels: sweepLabels,
|
|
},
|
|
],
|
|
});
|
|
main({ repo: "o/r", root, spawn, log: () => {} });
|
|
const [edit] = writes(calls);
|
|
assert.deepEqual(edit.args.slice(0, 3), ["issue", "edit", "7"]);
|
|
assert.ok(
|
|
!calls.some((c) => c.cmd === "gh" && /milestones/.test(c.args[1] ?? "")),
|
|
);
|
|
});
|
|
|
|
test("main ignores a look-alike issue an outsider wrote", (t) => {
|
|
const root = fixtureRoot(t);
|
|
const warnings = [];
|
|
const { spawn, calls } = fakeTools({
|
|
issues: [
|
|
{
|
|
number: 7,
|
|
body: `${ISSUE_MARKER}\nforged`,
|
|
state: "OPEN",
|
|
author: { login: "octocat" },
|
|
labels: sweepLabels,
|
|
},
|
|
],
|
|
});
|
|
main({
|
|
repo: "o/r",
|
|
root,
|
|
spawn,
|
|
log: () => {},
|
|
warn: (w) => warnings.push(w),
|
|
});
|
|
assert.equal(writes(calls)[0].args[1], "create");
|
|
// Marker and labels with an unknown author is flagged, not trusted.
|
|
assert.match(warnings.join("\n"), /"octocat"/);
|
|
});
|
|
|
|
test("main leaves an up-to-date issue alone", (t) => {
|
|
const root = fixtureRoot(t);
|
|
// Render the body the run would produce, then present it as the existing one.
|
|
const first = fakeTools();
|
|
main({ repo: "o/r", root, spawn: first.spawn, log: () => {} });
|
|
const body = writes(first.calls)[0].input;
|
|
const { spawn, calls } = fakeTools({
|
|
issues: [
|
|
{ number: 7, body, state: "OPEN", author: bot, labels: sweepLabels },
|
|
],
|
|
});
|
|
main({ repo: "o/r", root, spawn, log: () => {} });
|
|
assert.equal(writes(calls).length, 0);
|
|
});
|
|
|
|
test("with nothing behind, main clears an open issue, once", (t) => {
|
|
const root = fixtureRoot(t);
|
|
const quiet = {
|
|
outdated: { status: 0, stdout: "" },
|
|
uvLock: { status: 0, stderr: "Resolved 1 package\n" },
|
|
uvTree: { status: 0, stdout: "├── mcp v1.29.0\n" },
|
|
releases: {
|
|
"actions/checkout": "v6.1.0\n",
|
|
"actions/setup-node": "v7.0.0\n",
|
|
},
|
|
};
|
|
const none = fakeTools(quiet);
|
|
main({ repo: "o/r", root, spawn: none.spawn, log: () => {} });
|
|
assert.equal(writes(none.calls).length, 0);
|
|
|
|
const open = fakeTools({
|
|
...quiet,
|
|
issues: [
|
|
{
|
|
number: 7,
|
|
body: `${ISSUE_MARKER}\nstale table`,
|
|
state: "OPEN",
|
|
author: bot,
|
|
labels: sweepLabels,
|
|
},
|
|
],
|
|
});
|
|
main({
|
|
repo: "o/r",
|
|
root,
|
|
spawn: open.spawn,
|
|
today: "2026-11-01",
|
|
log: () => {},
|
|
});
|
|
const [edit] = writes(open.calls);
|
|
assert.equal(edit.input, buildClearedBody("2026-11-01"));
|
|
|
|
const cleared = fakeTools({
|
|
...quiet,
|
|
issues: [
|
|
{
|
|
number: 7,
|
|
body: buildClearedBody("2026-11-01"),
|
|
state: "OPEN",
|
|
author: bot,
|
|
labels: sweepLabels,
|
|
},
|
|
],
|
|
});
|
|
main({
|
|
repo: "o/r",
|
|
root,
|
|
spawn: cleared.spawn,
|
|
today: "2026-11-01",
|
|
log: () => {},
|
|
});
|
|
assert.equal(writes(cleared.calls).length, 0);
|
|
});
|
|
|
|
test("main files unmilestoned when no milestone is dated", (t) => {
|
|
const root = fixtureRoot(t);
|
|
const { spawn, calls } = fakeTools({
|
|
milestones: [{ title: "someday", due_on: null }],
|
|
});
|
|
const lines = [];
|
|
main({ repo: "o/r", root, spawn, log: (l) => lines.push(l) });
|
|
assert.ok(!writes(calls)[0].args.includes("--milestone"));
|
|
assert.ok(lines.some((l) => /filed unmilestoned/.test(l)));
|
|
});
|
|
|
|
test("main fails, writing nothing, when a lookup fails", (t) => {
|
|
const root = fixtureRoot(t);
|
|
for (const [state, pattern] of [
|
|
[
|
|
{ outdated: { status: 2, stderr: "E404 registry" } },
|
|
/npm outdated failed/,
|
|
],
|
|
[
|
|
{ outdated: { status: 1, stdout: "", stderr: "ENOTFOUND" } },
|
|
/exited 1 with no outdated payload: ENOTFOUND/,
|
|
],
|
|
[
|
|
{ outdated: { status: 1, stdout: "{}" } },
|
|
/exited 1 with no outdated payload/,
|
|
],
|
|
[
|
|
{
|
|
outdated: {
|
|
status: 1,
|
|
stdout: JSON.stringify({ error: { code: "E503" } }),
|
|
},
|
|
},
|
|
/exited 1 with no outdated payload/,
|
|
],
|
|
[
|
|
{ uvLock: { status: 2, stderr: "no network" } },
|
|
/uv lock --upgrade --dry-run failed/,
|
|
],
|
|
[{ releasesFail: true }, /release lookup for actions\/checkout failed/],
|
|
]) {
|
|
const { spawn, calls } = fakeTools(state);
|
|
assert.throws(
|
|
() => main({ repo: "o/r", root, spawn, log: () => {} }),
|
|
pattern,
|
|
);
|
|
assert.equal(writes(calls).length, 0);
|
|
}
|
|
assert.throws(
|
|
() => main({ repo: "", root, log: () => {} }),
|
|
/GITHUB_REPOSITORY/,
|
|
);
|
|
assert.throws(
|
|
() =>
|
|
main({
|
|
repo: "o/r",
|
|
root,
|
|
spawn: () => ({ error: new Error("ENOENT npm") }),
|
|
log: () => {},
|
|
}),
|
|
/ENOENT/,
|
|
);
|
|
});
|