157 lines
6.5 KiB
JavaScript
157 lines
6.5 KiB
JavaScript
// Local unit test for duplicate-prs.js -- mocks the GitHub client and runs the
|
|
// real decision logic. No network. The script paginates a GraphQL search and
|
|
// then closes/labels/comments the newer PRs for each over-subscribed issue.
|
|
|
|
const path = require("path");
|
|
const script = require(path.resolve(".github/workflows/duplicate-prs.js"));
|
|
|
|
// Build a PR node shaped like the GraphQL response. `issues` is the list of
|
|
// closing-issue references; `assoc` is the authorAssociation; `labels` is the
|
|
// label name list.
|
|
function pr({ number, createdAt, author = "ext", assoc = "CONTRIBUTOR", issues = [], labels = [] }) {
|
|
return {
|
|
number,
|
|
createdAt,
|
|
url: `https://example/pr/${number}`,
|
|
author: { login: author },
|
|
authorAssociation: assoc,
|
|
labels: { nodes: labels.map((name) => ({ name })) },
|
|
closingIssuesReferences: { nodes: issues.map((n) => ({ number: n })) },
|
|
};
|
|
}
|
|
|
|
// Run the script against a set of PR nodes; returns the side effects.
|
|
async function run(nodes) {
|
|
const closed = [];
|
|
const labeled = [];
|
|
const commented = [];
|
|
let calls = 0;
|
|
const github = {
|
|
// Single page: first call returns the nodes, then stop.
|
|
graphql: async () => {
|
|
const done = calls++ > 0;
|
|
return {
|
|
rateLimit: { remaining: 4999, resetAt: "n/a" },
|
|
search: {
|
|
pageInfo: { hasNextPage: !done, endCursor: "c" },
|
|
nodes: done ? [] : nodes,
|
|
},
|
|
};
|
|
},
|
|
rest: {
|
|
pulls: {
|
|
update: async ({ pull_number, state }) => closed.push({ pull_number, state }),
|
|
},
|
|
issues: {
|
|
addLabels: async ({ issue_number, labels }) => labeled.push({ issue_number, labels }),
|
|
createComment: async ({ issue_number, body }) => commented.push({ issue_number, body }),
|
|
},
|
|
},
|
|
};
|
|
const context = { repo: { owner: "omnigent-ai", repo: "omnigent" } };
|
|
await script({ context, github });
|
|
return {
|
|
closed: closed.map((c) => c.pull_number).sort((a, b) => a - b),
|
|
labeled: labeled.map((l) => l.issue_number).sort((a, b) => a - b),
|
|
commented,
|
|
};
|
|
}
|
|
|
|
function assert(name, cond, detail) {
|
|
console.log(`${cond ? "PASS" : "FAIL"} ${name}${detail ? " -- " + detail : ""}`);
|
|
if (!cond) process.exitCode = 1;
|
|
}
|
|
|
|
(async () => {
|
|
// 1. Two community PRs on the same issue: keep oldest (#1), close newer (#2).
|
|
let r = await run([
|
|
pr({ number: 1, createdAt: "2026-06-01T00:00:00Z", issues: [100] }),
|
|
pr({ number: 2, createdAt: "2026-06-02T00:00:00Z", issues: [100] }),
|
|
]);
|
|
assert("closes the newer duplicate, keeps the oldest",
|
|
JSON.stringify(r.closed) === JSON.stringify([2]) &&
|
|
JSON.stringify(r.labeled) === JSON.stringify([2]) &&
|
|
r.commented.length === 1 && r.commented[0].body.includes("#1"),
|
|
JSON.stringify(r));
|
|
|
|
// 2. Three PRs on one issue: keep oldest, close the other two.
|
|
r = await run([
|
|
pr({ number: 5, createdAt: "2026-06-03T00:00:00Z", issues: [7] }),
|
|
pr({ number: 3, createdAt: "2026-06-01T00:00:00Z", issues: [7] }),
|
|
pr({ number: 4, createdAt: "2026-06-02T00:00:00Z", issues: [7] }),
|
|
]);
|
|
assert("keeps oldest of three, closes the other two",
|
|
JSON.stringify(r.closed) === JSON.stringify([4, 5]), JSON.stringify(r));
|
|
|
|
// 3. Single PR per issue: nothing closed.
|
|
r = await run([
|
|
pr({ number: 1, createdAt: "2026-06-01T00:00:00Z", issues: [1] }),
|
|
pr({ number: 2, createdAt: "2026-06-02T00:00:00Z", issues: [2] }),
|
|
]);
|
|
assert("distinct issues -> no closures", r.closed.length === 0, JSON.stringify(r));
|
|
|
|
// 4a. Maintainer PR (older) is the keeper -> newer community duplicate closes.
|
|
r = await run([
|
|
pr({ number: 1, createdAt: "2026-06-01T00:00:00Z", issues: [9], assoc: "MEMBER" }),
|
|
pr({ number: 2, createdAt: "2026-06-02T00:00:00Z", issues: [9] }),
|
|
]);
|
|
assert("maintainer keeper -> newer community duplicate is closed",
|
|
JSON.stringify(r.closed) === JSON.stringify([2]), JSON.stringify(r));
|
|
|
|
// 4b. Community PR (older) keeper, maintainer PR (newer) duplicate -> the
|
|
// maintainer PR is flagged (heads-up comment + label) but never closed.
|
|
r = await run([
|
|
pr({ number: 1, createdAt: "2026-06-01T00:00:00Z", issues: [9] }),
|
|
pr({ number: 2, createdAt: "2026-06-02T00:00:00Z", issues: [9], assoc: "MEMBER" }),
|
|
]);
|
|
assert("maintainer duplicate is flagged, not closed",
|
|
r.closed.length === 0 &&
|
|
JSON.stringify(r.labeled) === JSON.stringify([2]) &&
|
|
r.commented.length === 1 &&
|
|
r.commented[0].issue_number === 2 &&
|
|
r.commented[0].body.includes("won't be auto-closed"),
|
|
JSON.stringify(r));
|
|
|
|
// 4c. Two maintainer PRs on one issue -> neither is closed; the newer one is
|
|
// flagged with the heads-up comment.
|
|
r = await run([
|
|
pr({ number: 1, createdAt: "2026-06-01T00:00:00Z", issues: [9], assoc: "OWNER" }),
|
|
pr({ number: 2, createdAt: "2026-06-02T00:00:00Z", issues: [9], assoc: "COLLABORATOR" }),
|
|
]);
|
|
assert("two maintainer PRs -> none closed, newer flagged",
|
|
r.closed.length === 0 &&
|
|
JSON.stringify(r.labeled) === JSON.stringify([2]) &&
|
|
r.commented.length === 1 && r.commented[0].issue_number === 2,
|
|
JSON.stringify(r));
|
|
|
|
// 4d. Mixed group: maintainer keeper + two community duplicates -> both close.
|
|
r = await run([
|
|
pr({ number: 1, createdAt: "2026-06-01T00:00:00Z", issues: [9], assoc: "MEMBER" }),
|
|
pr({ number: 2, createdAt: "2026-06-02T00:00:00Z", issues: [9] }),
|
|
pr({ number: 3, createdAt: "2026-06-03T00:00:00Z", issues: [9] }),
|
|
]);
|
|
assert("maintainer keeper + 2 community dupes -> both community closed",
|
|
JSON.stringify(r.closed) === JSON.stringify([2, 3]), JSON.stringify(r));
|
|
|
|
// 5. Already-labeled duplicate is skipped (filtered before grouping).
|
|
r = await run([
|
|
pr({ number: 1, createdAt: "2026-06-01T00:00:00Z", issues: [9] }),
|
|
pr({ number: 2, createdAt: "2026-06-02T00:00:00Z", issues: [9], labels: ["duplicate"] }),
|
|
]);
|
|
assert("already-labeled duplicate is skipped", r.closed.length === 0, JSON.stringify(r));
|
|
|
|
// 6. PR referencing multiple issues is ambiguous -> skipped.
|
|
r = await run([
|
|
pr({ number: 1, createdAt: "2026-06-01T00:00:00Z", issues: [9] }),
|
|
pr({ number: 2, createdAt: "2026-06-02T00:00:00Z", issues: [9, 10] }),
|
|
]);
|
|
assert("multi-issue PR is skipped, no duplicate group forms", r.closed.length === 0, JSON.stringify(r));
|
|
|
|
// 7. PR with no issue reference is ignored.
|
|
r = await run([
|
|
pr({ number: 1, createdAt: "2026-06-01T00:00:00Z", issues: [9] }),
|
|
pr({ number: 2, createdAt: "2026-06-02T00:00:00Z", issues: [] }),
|
|
]);
|
|
assert("PR with no issue reference is ignored", r.closed.length === 0, JSON.stringify(r));
|
|
})();
|