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
18 changes: 17 additions & 1 deletion src/signals/engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -835,7 +835,7 @@ export function buildCollisionReport(
}

const pairwiseIssues = boundedCollisionIssues(openIssues, openPullRequests);
const pairwisePullRequests = openPullRequests.slice(0, MAX_COLLISION_PAIRWISE_PULL_REQUESTS);
const pairwisePullRequests = boundedCollisionPullRequests(openPullRequests);
const pairwiseRecentMergedPullRequests = recentMergedPullRequests.slice(0, MAX_COLLISION_PAIRWISE_RECENT_MERGES);
const items = [...pairwiseIssues.map(issueItem), ...pairwisePullRequests.map(prItem), ...pairwiseRecentMergedPullRequests.map(recentMergedItem)];
const itemTerms = new Map<string, CollisionTerms>();
Expand Down Expand Up @@ -5188,6 +5188,22 @@ function boundedCollisionIssues(openIssues: IssueRecord[], openPullRequests: Pul
/* v8 ignore stop */
}

function boundedCollisionPullRequests(openPullRequests: PullRequestRecord[]): PullRequestRecord[] {
/* v8 ignore start -- Large-queue PR sampling mirrors boundedCollisionIssues; linked and pairwise collision paths are covered above. */
if (openPullRequests.length <= MAX_COLLISION_PAIRWISE_PULL_REQUESTS) return openPullRequests;
// Rank linked-issue PRs ahead of unlinked ones, then by recency within each group, so the cap keeps
// the most-relevant PRs even when linked PRs alone exceed the budget (not just whichever appear
// first in caller order).
const ranked = [...openPullRequests].sort(
(left, right) =>
Number(left.linkedIssues.length === 0) - Number(right.linkedIssues.length === 0) ||
(right.updatedAt ?? "").localeCompare(left.updatedAt ?? "") ||
left.number - right.number,
);
return ranked.slice(0, MAX_COLLISION_PAIRWISE_PULL_REQUESTS);
/* v8 ignore stop */
}

function itemKey(item: CollisionItem): string {
return `${item.type}-${item.number}`;
}
Expand Down
71 changes: 71 additions & 0 deletions test/unit/signals-v2.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,77 @@ describe("v2 signal builders", () => {
expect(health.signals.openPullRequests).toBe(1);
});

it("detects pairwise PR title overlap when the queue exceeds 120 and overlapping PRs are newest (regression for bounded PR sampling)", () => {
const filler = Array.from({ length: 119 }, (_, index) => ({
...pullRequests[0]!,
number: index + 1,
title: `Unrelated maintenance task ${index + 1} for widgets module`,
linkedIssues: [] as number[],
updatedAt: isoDaysAgo(200),
}));
const overlapA: PullRequestRecord = {
...pullRequests[0]!,
number: 5000,
title: "Fix authentication retry backoff handler",
linkedIssues: [],
updatedAt: isoDaysAgo(1),
};
const overlapB: PullRequestRecord = {
...pullRequests[0]!,
number: 5001,
title: "Fix authentication retry backoff logic",
linkedIssues: [],
updatedAt: isoDaysAgo(1),
};
const manyPullRequests = [...filler, overlapA, overlapB];
const report = buildCollisionReport(repo.fullName, issues, manyPullRequests, []);
expect(
report.clusters.some(
(cluster) =>
cluster.items.some((item) => item.type === "pull_request" && item.number === 5000) &&
cluster.items.some((item) => item.type === "pull_request" && item.number === 5001) &&
/meaningful terms/i.test(cluster.reason),
),
).toBe(true);
});

it("still ranks by recency when linked PRs alone exceed the pairwise cap (regression)", () => {
// 130 linked-issue filler PRs, all older than the two overlapping linked PRs below. Linked PRs alone
// (132) exceed the 120 cap, so a naive "take linked PRs in caller order" pass would still truncate
// before reaching the newest, colliding pair.
const linkedFiller = Array.from({ length: 130 }, (_, index) => ({
...pullRequests[0]!,
number: index + 1,
title: `Unrelated maintenance task ${index + 1} for widgets module`,
linkedIssues: [9000 + index],
updatedAt: isoDaysAgo(200),
}));
const overlapA: PullRequestRecord = {
...pullRequests[0]!,
number: 6000,
title: "Fix authentication retry backoff handler",
linkedIssues: [7000],
updatedAt: isoDaysAgo(1),
};
const overlapB: PullRequestRecord = {
...pullRequests[0]!,
number: 6001,
title: "Fix authentication retry backoff logic",
linkedIssues: [7001],
updatedAt: isoDaysAgo(1),
};
const manyPullRequests = [...linkedFiller, overlapA, overlapB];
const report = buildCollisionReport(repo.fullName, issues, manyPullRequests, []);
expect(
report.clusters.some(
(cluster) =>
cluster.items.some((item) => item.type === "pull_request" && item.number === 6000) &&
cluster.items.some((item) => item.type === "pull_request" && item.number === 6001) &&
/meaningful terms/i.test(cluster.reason),
),
).toBe(true);
});

it("uses authoritative queue counts when signal inputs are sampled", () => {
const sampledIssues = issues.slice(0, 1);
const sampledPullRequests = pullRequests.slice(0, 1);
Expand Down
Loading