fix(pr-gardening): report truncated issue matches

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-07-16 14:00:13 +00:00
1 parent da549123cc
commit a8814a87db
2 files changed
+58 -4

No files matched your search

@@ -33,7 +33,7 @@ export async function findCandidates(options) {
const matchesPerIssue = 200;
const issueMap = new Map();
let offset = 0;
let truncated = false;
const truncatedIssues = [];
while (true) {
const query = new URLSearchParams({
@@ -47,12 +47,16 @@ export async function findCandidates(options) {
});
const page = await getPaperclip(`/companies/${companyId}/search/extract?${query}`, { apiUrl, apiKey });
for (const issue of page.results) issueMap.set(issue.issueId, issue);
truncated ||= page.results.some((issue) => issue.matchesTruncated);
for (const issue of page.results) {
if (issue.matchesTruncated) truncatedIssues.push({ issueId: issue.issueId, identifier: issue.identifier });
}
if (!page.hasMore) break;
offset += limit;
if (offset > 5000) throw new Error("Extract-search pagination exceeded the supported 5000 issue offset");
}
if (truncated) throw new Error("Extract-search truncated one or more issue match sets; refusing an incomplete candidate report");
if (truncatedIssues.length > 0) {
console.warn(`Warning: ${truncatedIssues.length} issue(s) had truncated match sets (>${matchesPerIssue} PR URL matches). These issues may contribute incomplete PR discovery: ${truncatedIssues.map((i) => i.identifier ?? i.issueId).join(", ")}. Proceeding with available data.`);
}
const pullRequests = new Map();
for (const issue of issueMap.values()) {
@@ -148,7 +152,8 @@ export async function findCandidates(options) {
openPullRequestCount: candidates.length,
droppedClosedPullRequests: closed,
droppedUnavailablePullRequests: unavailable,
truncated: false,
truncatedIssues,
truncated: truncatedIssues.length > 0,
},
candidates,
};
@@ -94,6 +94,55 @@ test("candidate discovery deduplicates mentions and drops closed PRs", async ()
assert.deepEqual(result.source.droppedUnavailablePullRequests.map((pullRequest) => pullRequest.number), [3]);
});
test("candidate discovery reports truncated issue matches without discarding the report", async () => {
const warnings = [];
const originalWarn = console.warn;
console.warn = (message) => warnings.push(message);
try {
const result = await findCandidates({
repo: "paperclipai/paperclip",
api_url: "http://paperclip.test",
api_key: "test-key",
company_id: "company-1",
paperclip_get: async (path) => {
if (path.includes("search/extract")) {
return {
hasMore: false,
results: [
{
issueId: "issue-truncated",
identifier: "PAP-TRUNCATED",
title: "Large PR thread",
status: "done",
assigneeAgentId: "agent-1",
updatedAt: "2026-07-13T00:00:00Z",
matchesTruncated: true,
matches: [],
},
],
};
}
throw new Error(`Unexpected Paperclip path: ${path}`);
},
gh_json: async () => {
throw new Error("GitHub should not be queried without discovered pull requests");
},
});
assert.equal(result.source.truncated, true);
assert.deepEqual(result.source.truncatedIssues, [
{ issueId: "issue-truncated", identifier: "PAP-TRUNCATED" },
]);
assert.deepEqual(result.candidates, []);
assert.equal(warnings.length, 1);
assert.match(warnings[0], /PAP-TRUNCATED/);
assert.match(warnings[0], /Proceeding with available data/);
} finally {
console.warn = originalWarn;
}
});
test("missing-PR detection matches only deleted/nonexistent PR signals", () => {
// gh's real signals for a deleted/nonexistent PR: GraphQL resolution failure and REST 404.
assert.equal(isMissingPullRequestError(new Error("GraphQL: Could not resolve to a PullRequest with the number of 3")), true);