mirror of
https://github.com/bookorbit/bookorbit.git
synced 2026-11-03 00:11:37 -06:00
a0477f0d0b
Use human activity and observed head changes because GitHub can update PR timestamps without contributor activity. Audit every open PR before applying changes and start existing PRs from a safe baseline.
186 lines
8.0 KiB
JavaScript
186 lines
8.0 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import { spawn } from "node:child_process";
|
|
import { createServer } from "node:http";
|
|
import test from "node:test";
|
|
import { fileURLToPath } from "node:url";
|
|
import { formatState, WARNING_MARKER } from "./policy.mjs";
|
|
|
|
const script = fileURLToPath(new URL("./run.mjs", import.meta.url));
|
|
const bot = { login: "github-actions[bot]", type: "Bot" };
|
|
const sha = "a".repeat(40);
|
|
|
|
const startServer = async (handler) => {
|
|
const server = createServer(async (request, response) => {
|
|
try {
|
|
const body = await new Promise((resolve) => {
|
|
let value = "";
|
|
request.on("data", (chunk) => (value += chunk));
|
|
request.on("end", () => resolve(value ? JSON.parse(value) : null));
|
|
});
|
|
const result = handler(request, body);
|
|
response.writeHead(result.status || 200, { "Content-Type": "application/json" });
|
|
response.end(JSON.stringify(result.body ?? null));
|
|
} catch {
|
|
response.writeHead(500);
|
|
response.end();
|
|
}
|
|
});
|
|
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
return server;
|
|
};
|
|
|
|
const run = (server, mode) =>
|
|
new Promise((resolve) => {
|
|
const child = spawn(process.execPath, [script, mode], {
|
|
env: {
|
|
...process.env,
|
|
GITHUB_API_URL: `http://127.0.0.1:${server.address().port}`,
|
|
GITHUB_REPOSITORY: "example/repo",
|
|
GITHUB_TOKEN: "test-token",
|
|
},
|
|
});
|
|
let stdout = "";
|
|
let stderr = "";
|
|
child.stdout.on("data", (chunk) => (stdout += chunk));
|
|
child.stderr.on("data", (chunk) => (stderr += chunk));
|
|
child.on("close", (status) => resolve({ status, stdout, stderr }));
|
|
});
|
|
|
|
test("dry-run inspects every page and performs no writes", async (t) => {
|
|
let writes = 0;
|
|
const server = await startServer((request) => {
|
|
if (request.method !== "GET") writes++;
|
|
const url = new URL(request.url, "http://localhost");
|
|
if (url.pathname === "/repos/example/repo/pulls") {
|
|
const page = Number(url.searchParams.get("page"));
|
|
const numbers = page === 1 ? Array.from({ length: 100 }, (_, index) => index + 1) : [101];
|
|
return { body: numbers.map((number) => ({ number })) };
|
|
}
|
|
const match = url.pathname.match(/^\/repos\/example\/repo\/pulls\/(\d+)$/);
|
|
if (match) return { body: { number: Number(match[1]), state: "open", head: { sha }, labels: [] } };
|
|
return { body: [] };
|
|
});
|
|
t.after(() => server.close());
|
|
|
|
const result = await run(server, "--dry-run");
|
|
assert.equal(result.status, 0, result.stderr);
|
|
assert.equal(writes, 0);
|
|
assert.match(result.stdout, /"inspected":101,"total":101,"mode":"dry-run"/);
|
|
});
|
|
|
|
test("apply rechecks activity before closing a warned PR", async (t) => {
|
|
const now = Date.now();
|
|
const daysAgo = (days) => new Date(now - days * 24 * 60 * 60 * 1000).toISOString();
|
|
const comments = [
|
|
{
|
|
id: 1,
|
|
user: bot,
|
|
body: formatState({ baselineAt: daysAgo(60), headSeenAt: daysAgo(60), headSha: sha }),
|
|
created_at: daysAgo(60),
|
|
},
|
|
{ id: 2, user: bot, body: WARNING_MARKER, created_at: daysAgo(15) },
|
|
];
|
|
const writes = [];
|
|
const server = await startServer((request, body) => {
|
|
const url = new URL(request.url, "http://localhost");
|
|
if (request.method !== "GET") writes.push({ method: request.method, path: url.pathname, body });
|
|
if (url.pathname === "/repos/example/repo/pulls") return { body: [{ number: 7 }] };
|
|
if (url.pathname === "/repos/example/repo/pulls/7") {
|
|
if (request.method === "PATCH") return { body: { state: "closed" } };
|
|
return { body: { number: 7, state: "open", head: { sha }, labels: [{ name: "stale" }] } };
|
|
}
|
|
if (url.pathname === "/repos/example/repo/issues/7/comments") return { body: comments };
|
|
return { body: [] };
|
|
});
|
|
t.after(() => server.close());
|
|
|
|
const result = await run(server, "--apply");
|
|
assert.equal(result.status, 0, result.stderr);
|
|
assert.deepEqual(writes, [{ method: "PATCH", path: "/repos/example/repo/pulls/7", body: { state: "closed" } }]);
|
|
});
|
|
|
|
test("apply defers closure when a human comments during the final recheck", async (t) => {
|
|
const now = Date.now();
|
|
const daysAgo = (days) => new Date(now - days * 24 * 60 * 60 * 1000).toISOString();
|
|
let commentReads = 0;
|
|
let closed = false;
|
|
const server = await startServer((request) => {
|
|
const url = new URL(request.url, "http://localhost");
|
|
if (url.pathname === "/repos/example/repo/pulls") return { body: [{ number: 7 }] };
|
|
if (url.pathname === "/repos/example/repo/pulls/7") {
|
|
if (request.method === "PATCH") closed = true;
|
|
return { body: { number: 7, state: "open", head: { sha }, labels: [{ name: "stale" }] } };
|
|
}
|
|
if (url.pathname === "/repos/example/repo/issues/7/comments") {
|
|
commentReads++;
|
|
return {
|
|
body: [
|
|
{ id: 1, user: bot, body: formatState({ baselineAt: daysAgo(60), headSeenAt: daysAgo(60), headSha: sha }), created_at: daysAgo(60) },
|
|
{ id: 2, user: bot, body: WARNING_MARKER, created_at: daysAgo(15) },
|
|
...(commentReads > 1
|
|
? [{ id: 3, user: { login: "contributor", type: "User" }, created_at: daysAgo(0.01), updated_at: daysAgo(0.01) }]
|
|
: []),
|
|
],
|
|
};
|
|
}
|
|
return { body: [] };
|
|
});
|
|
t.after(() => server.close());
|
|
|
|
const result = await run(server, "--apply");
|
|
assert.equal(result.status, 0, result.stderr);
|
|
assert.equal(commentReads, 2);
|
|
assert.equal(closed, false);
|
|
assert.match(result.stdout, /"action":"deferred"/);
|
|
});
|
|
|
|
test("apply labels and warns an inactive PR", async (t) => {
|
|
const baselineAt = new Date(Date.now() - 31 * 24 * 60 * 60 * 1000).toISOString();
|
|
const writes = [];
|
|
const server = await startServer((request, body) => {
|
|
const url = new URL(request.url, "http://localhost");
|
|
if (request.method !== "GET") writes.push({ method: request.method, path: url.pathname, body });
|
|
if (url.pathname === "/repos/example/repo/pulls") return { body: [{ number: 7 }] };
|
|
if (url.pathname === "/repos/example/repo/pulls/7") return { body: { number: 7, state: "open", head: { sha }, labels: [] } };
|
|
if (url.pathname === "/repos/example/repo/issues/7/comments") {
|
|
return { body: [{ id: 1, user: bot, body: formatState({ baselineAt, headSeenAt: baselineAt, headSha: sha }), created_at: baselineAt }] };
|
|
}
|
|
return { body: [] };
|
|
});
|
|
t.after(() => server.close());
|
|
|
|
const result = await run(server, "--apply");
|
|
assert.equal(result.status, 0, result.stderr);
|
|
assert.equal(writes.length, 2);
|
|
assert.deepEqual(writes[0], { method: "POST", path: "/repos/example/repo/issues/7/labels", body: { labels: ["stale"] } });
|
|
assert.equal(writes[1].method, "POST");
|
|
assert.equal(writes[1].path, "/repos/example/repo/issues/7/comments");
|
|
assert.match(writes[1].body.body, /bookorbit-stale-pr-warning:v1/);
|
|
});
|
|
|
|
test("apply updates the recorded head and removes stale after a push", async (t) => {
|
|
const baselineAt = new Date(Date.now() - 45 * 24 * 60 * 60 * 1000).toISOString();
|
|
const writes = [];
|
|
const server = await startServer((request, body) => {
|
|
const url = new URL(request.url, "http://localhost");
|
|
if (request.method !== "GET") writes.push({ method: request.method, path: url.pathname, body });
|
|
if (url.pathname === "/repos/example/repo/pulls") return { body: [{ number: 7 }] };
|
|
if (url.pathname === "/repos/example/repo/pulls/7") {
|
|
return { body: { number: 7, state: "open", head: { sha: "b".repeat(40) }, labels: [{ name: "stale" }] } };
|
|
}
|
|
if (url.pathname === "/repos/example/repo/issues/7/comments") {
|
|
return { body: [{ id: 31, user: bot, body: formatState({ baselineAt, headSeenAt: baselineAt, headSha: sha }), created_at: baselineAt }] };
|
|
}
|
|
return { body: [] };
|
|
});
|
|
t.after(() => server.close());
|
|
|
|
const result = await run(server, "--apply");
|
|
assert.equal(result.status, 0, result.stderr);
|
|
assert.equal(writes.length, 2);
|
|
assert.equal(writes[0].method, "PATCH");
|
|
assert.equal(writes[0].path, "/repos/example/repo/issues/comments/31");
|
|
assert.match(writes[0].body.body, /b{40}/);
|
|
assert.deepEqual(writes[1], { method: "DELETE", path: "/repos/example/repo/issues/7/labels/stale", body: null });
|
|
});
|