Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion src/rules/predicted-gate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,10 @@ export function buildPredictedGateVerdict(args: {
// from the snapshot, never a live fetch. (#self-authored-parity)
const issueAuthorByNumber = new Map(issues.filter((issue) => issue.repoFullName === input.repoFullName).map((issue) => [issue.number, issue.authorLogin ?? null]));
const linkedIssueAuthorLogins = syntheticPr.linkedIssues.map((issueNumber) => issueAuthorByNumber.get(issueNumber) ?? null);
const advisory = buildPullRequestAdvisory(repo, syntheticPr, { otherOpenPullRequests: pullRequests, requireLinkedIssue, linkedIssueAuthorLogins });
// Mirror the live gate (listOtherOpenPullRequests): a closed/merged PR sharing a linked issue must not fire
// duplicate_pr_risk. authorHistory below still needs every state for its grace counts.
const openSiblings = pullRequests.filter((otherPr) => otherPr.state === "open");
const advisory = buildPullRequestAdvisory(repo, syntheticPr, { otherOpenPullRequests: openSiblings, requireLinkedIssue, linkedIssueAuthorLogins });

// Deterministic pre-merge checks parity (#11/#18): the LIVE gate enforces the repo's `review.pre_merge_checks`
// (from the SAME public .gittensory.yml the predictor already reads). With the PR's changed paths supplied,
Expand Down
14 changes: 14 additions & 0 deletions test/unit/predicted-gate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,20 @@ describe("buildPredictedGateVerdict", () => {
expect(result.blockers.some((b) => b.code === "duplicate_pr_risk")).toBe(false);
});

it("does NOT raise duplicate_pr_risk for closed/merged siblings sharing the linked issue (open-only parity with the live gate)", () => {
// A merged PR and an abandoned closed PR both share the new PR's linked issue, but neither is still
// competing, so the predictor must not flag a duplicate.
const result = verdict({
gate: { duplicates: "block" },
pullRequests: [
{ ...openPr(100, "Earlier upload retry", [7], "someone-else"), state: "merged", mergedAt: "2026-06-01T00:00:00.000Z" },
{ ...openPr(101, "Abandoned upload retry", [7], "someone-else"), state: "closed" },
],
});
expect(result.blockers.some((b) => b.code === "duplicate_pr_risk")).toBe(false);
expect(result.conclusion).toBe("success");
});

it("predicts a BLOCK for a missing linked issue only when linkedIssue:block", () => {
const blocked = verdict({ gate: { linkedIssue: "block" }, input: { body: "no issue here", linkedIssues: [] }, issues: [] });
expect(blocked.conclusion).toBe("failure");
Expand Down
Loading