mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 15:10:52 +00:00
243 lines
6.7 KiB
TypeScript
243 lines
6.7 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import {
|
|
applyClosePlan,
|
|
buildDuplicateClosePlan,
|
|
parseArgs,
|
|
parsePrNumberList,
|
|
parseUnifiedDiffRanges,
|
|
runDuplicateCloseWorkflow,
|
|
} from "../../scripts/close-duplicate-prs-after-merge.mjs";
|
|
|
|
function pr(params: {
|
|
body?: string;
|
|
files?: string[];
|
|
mergedAt?: string | null;
|
|
mergeCommit?: string;
|
|
number: number;
|
|
state?: string;
|
|
title?: string;
|
|
}) {
|
|
return {
|
|
body: params.body ?? "",
|
|
closingIssuesReferences: [],
|
|
files: (params.files ?? ["ui/src/ui/chat/grouped-render.ts"]).map((path) => ({ path })),
|
|
mergeCommit: params.mergeCommit ? { oid: params.mergeCommit } : null,
|
|
mergedAt: params.mergedAt ?? null,
|
|
number: params.number,
|
|
state: params.state ?? "OPEN",
|
|
title: params.title ?? `PR ${params.number}`,
|
|
url: `https://github.com/openclaw/openclaw/pull/${params.number}`,
|
|
};
|
|
}
|
|
|
|
describe("close duplicate PRs after merge", () => {
|
|
it("parses comma, whitespace, and hash-prefixed PR lists", () => {
|
|
expect(parsePrNumberList("#70530, 70592\n70530")).toEqual([70530, 70592]);
|
|
});
|
|
|
|
it("parses hunk ranges from unified diffs", () => {
|
|
const ranges = parseUnifiedDiffRanges(`diff --git a/a.ts b/a.ts
|
|
@@ -10,2 +20,4 @@
|
|
+x
|
|
diff --git a/b.ts b/b.ts
|
|
@@ -1 +5 @@
|
|
-a
|
|
+b`);
|
|
|
|
expect(ranges.get("a.ts")).toEqual([{ start: 20, end: 23 }]);
|
|
expect(ranges.get("b.ts")).toEqual([{ start: 5, end: 5 }]);
|
|
});
|
|
|
|
it("allows duplicate closure with overlapping hunks even without an explicit issue ref", () => {
|
|
const landed = pr({
|
|
body: "Fixes #70491",
|
|
mergeCommit: "6415e35",
|
|
mergedAt: "2026-04-23T17:13:32Z",
|
|
number: 70532,
|
|
state: "MERGED",
|
|
});
|
|
const candidate = pr({ number: 70530 });
|
|
const diffs = new Map([
|
|
[
|
|
70532,
|
|
`diff --git a/ui/src/ui/chat/grouped-render.ts b/ui/src/ui/chat/grouped-render.ts
|
|
@@ -402,8 +402,11 @@`,
|
|
],
|
|
[
|
|
70530,
|
|
`diff --git a/ui/src/ui/chat/grouped-render.ts b/ui/src/ui/chat/grouped-render.ts
|
|
@@ -402,8 +402,11 @@`,
|
|
],
|
|
]);
|
|
|
|
const plan = buildDuplicateClosePlan({
|
|
candidates: [candidate],
|
|
diffs,
|
|
landed,
|
|
repo: "openclaw/openclaw",
|
|
});
|
|
|
|
expect(plan).toMatchObject([
|
|
{
|
|
action: "close",
|
|
candidate: { number: 70530 },
|
|
evidence: {
|
|
overlappingHunks: true,
|
|
sharedFiles: ["ui/src/ui/chat/grouped-render.ts"],
|
|
sharedIssues: [],
|
|
},
|
|
},
|
|
]);
|
|
});
|
|
|
|
it("allows duplicate closure with a shared issue ref even when hunks drift", () => {
|
|
const landed = pr({
|
|
body: "Fixes #70491",
|
|
mergeCommit: "6415e35",
|
|
mergedAt: "2026-04-23T17:13:32Z",
|
|
number: 70532,
|
|
state: "MERGED",
|
|
});
|
|
const candidate = pr({ body: "Closes #70491", number: 70592 });
|
|
const diffs = new Map([
|
|
[
|
|
70532,
|
|
`diff --git a/ui/src/ui/chat/grouped-render.ts b/ui/src/ui/chat/grouped-render.ts
|
|
@@ -402,8 +402,11 @@`,
|
|
],
|
|
[
|
|
70592,
|
|
`diff --git a/ui/src/ui/chat/grouped-render.ts b/ui/src/ui/chat/grouped-render.ts
|
|
@@ -286,8 +286,11 @@`,
|
|
],
|
|
]);
|
|
|
|
const plan = buildDuplicateClosePlan({
|
|
candidates: [candidate],
|
|
diffs,
|
|
landed,
|
|
repo: "openclaw/openclaw",
|
|
});
|
|
|
|
expect(plan[0]).toMatchObject({
|
|
action: "close",
|
|
evidence: {
|
|
overlappingHunks: false,
|
|
sharedIssues: [70491],
|
|
},
|
|
});
|
|
});
|
|
|
|
it("refuses candidates without shared issue or overlapping hunks", () => {
|
|
const landed = pr({
|
|
body: "Fixes #70491",
|
|
mergeCommit: "6415e35",
|
|
mergedAt: "2026-04-23T17:13:32Z",
|
|
number: 70532,
|
|
state: "MERGED",
|
|
});
|
|
const candidate = pr({ body: "Fixes #1", number: 1 });
|
|
const diffs = new Map([
|
|
[70532, "diff --git a/a.ts b/a.ts\n@@ -1 +1 @@"],
|
|
[1, "diff --git a/a.ts b/a.ts\n@@ -99 +99 @@"],
|
|
]);
|
|
|
|
expect(() =>
|
|
buildDuplicateClosePlan({
|
|
candidates: [candidate],
|
|
diffs,
|
|
landed,
|
|
repo: "openclaw/openclaw",
|
|
}),
|
|
).toThrow("Refusing to close #1");
|
|
});
|
|
|
|
it("dry-runs through gh reads without mutating", () => {
|
|
const calls: string[][] = [];
|
|
const responses = new Map<string, string>([
|
|
[
|
|
"pr view 70532 --repo openclaw/openclaw --json number,title,body,state,mergedAt,mergeCommit,closingIssuesReferences,files,url",
|
|
JSON.stringify(
|
|
pr({
|
|
body: "Fixes #70491",
|
|
mergeCommit: "6415e35",
|
|
mergedAt: "2026-04-23T17:13:32Z",
|
|
number: 70532,
|
|
state: "MERGED",
|
|
}),
|
|
),
|
|
],
|
|
[
|
|
"pr view 70592 --repo openclaw/openclaw --json number,title,body,state,mergedAt,mergeCommit,closingIssuesReferences,files,url",
|
|
JSON.stringify(pr({ body: "Closes #70491", number: 70592 })),
|
|
],
|
|
[
|
|
"pr diff 70532 --repo openclaw/openclaw --color=never",
|
|
"diff --git a/ui/src/ui/chat/grouped-render.ts b/ui/src/ui/chat/grouped-render.ts\n@@ -402,8 +402,11 @@",
|
|
],
|
|
[
|
|
"pr diff 70592 --repo openclaw/openclaw --color=never",
|
|
"diff --git a/ui/src/ui/chat/grouped-render.ts b/ui/src/ui/chat/grouped-render.ts\n@@ -286,8 +286,11 @@",
|
|
],
|
|
]);
|
|
const runGh = (args: string[]) => {
|
|
calls.push(args);
|
|
const key = args.join(" ");
|
|
const response = responses.get(key);
|
|
if (response === undefined) {
|
|
throw new Error(`unexpected gh call: ${key}`);
|
|
}
|
|
return response;
|
|
};
|
|
|
|
const args = parseArgs(["--landed-pr", "70532", "--duplicates", "70592"], {
|
|
GITHUB_REPOSITORY: "openclaw/openclaw",
|
|
});
|
|
const plan = runDuplicateCloseWorkflow(args, runGh);
|
|
|
|
expect(plan).toHaveLength(1);
|
|
expect(calls.map((call) => call.slice(0, 2).join(" "))).toEqual([
|
|
"pr view",
|
|
"pr view",
|
|
"pr diff",
|
|
"pr diff",
|
|
]);
|
|
});
|
|
|
|
it("applies labels, comment, and close commands for close actions", () => {
|
|
const calls: string[][] = [];
|
|
applyClosePlan({
|
|
labels: ["duplicate", "close:duplicate"],
|
|
plan: [
|
|
{
|
|
action: "close",
|
|
candidate: pr({ number: 70592 }),
|
|
comment: "closing",
|
|
evidence: { overlappingHunks: false, sharedFiles: [], sharedIssues: [70491] },
|
|
},
|
|
],
|
|
repo: "openclaw/openclaw",
|
|
runGh: (args: string[]) => {
|
|
calls.push(args);
|
|
return "";
|
|
},
|
|
});
|
|
|
|
expect(calls).toEqual([
|
|
[
|
|
"pr",
|
|
"edit",
|
|
"70592",
|
|
"--repo",
|
|
"openclaw/openclaw",
|
|
"--add-label",
|
|
"duplicate",
|
|
"--add-label",
|
|
"close:duplicate",
|
|
],
|
|
["pr", "comment", "70592", "--repo", "openclaw/openclaw", "--body", "closing"],
|
|
["pr", "close", "70592", "--repo", "openclaw/openclaw"],
|
|
]);
|
|
});
|
|
});
|