diff --git a/packages/loopover-miner/lib/self-review-context.js b/packages/loopover-miner/lib/self-review-context.js index b186641e7d..bc5682ef54 100644 --- a/packages/loopover-miner/lib/self-review-context.js +++ b/packages/loopover-miner/lib/self-review-context.js @@ -203,8 +203,11 @@ async function fetchRepositoryRecord(target, resolved) { }; } -// Mirrors src/db/repositories.ts's extractLinkedPrNumbers exactly. -const LINKED_PR_PATTERN = /\b(?:PR|pull request)\s+#(\d+)\b/gi; +// Mirrors src/db/repositories.ts's extractLinkedPrNumbers: a real link needs a CLOSING KEYWORD, not a bare +// mention (#6769). Without the keyword prefix, an incidental "similar to what we saw in PR #501" in an issue +// body counted as a linked PR, so the issue-quality report read the issue as "already references a PR" and the +// miner skipped an available issue (the host's own #issue-body-pr-mention-pollution fix, never ported here). +const LINKED_PR_PATTERN = /\b(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s+(?:PR|pull request)\s+#(\d+)\b/gi; function extractLinkedPrNumbers(body) { const numbers = []; for (const match of body.matchAll(LINKED_PR_PATTERN)) { diff --git a/test/unit/miner-self-review-context.test.ts b/test/unit/miner-self-review-context.test.ts index 1a65178d06..3ae49eece4 100644 --- a/test/unit/miner-self-review-context.test.ts +++ b/test/unit/miner-self-review-context.test.ts @@ -246,6 +246,36 @@ describe("fetchSelfReviewContext (#5145)", () => { expect(result.issueQuality?.issues[0]?.warnings.join(" ")).not.toMatch(/bounty/i); expect("bounties" in result).toBe(false); }); + + // #6769: a real linked PR needs a CLOSING KEYWORD, matching the host's extractLinkedPrNumbers. The miner's + // copy had a bare `PR #N` pattern, so an incidental mention made the issue-quality report read the issue as + // "already references a PR" — and the miner skipped an issue that was actually available. + it("REGRESSION (#6769): a bare 'PR #N' mention in an issue body does NOT count as a linked PR", async () => { + const fetchImpl = routedFetch({ + "/repos/acme/widgets/issues": () => + jsonResponse([issuePayload({ body: "Uploads fail. This looks similar to what we saw in PR #501, worth a look." })]), + "/repos/acme/widgets/pulls": () => jsonResponse([]), + "/repos/acme/widgets": () => jsonResponse(REPO_PAYLOAD), + "raw.githubusercontent.com": () => jsonResponse(null, 404), + "api.gittensor.io/miners": () => jsonResponse([]), + }); + + const result = await fetchSelfReviewContext("acme/widgets", { fetchImpl: fetchImpl as never }); + expect(result.issues[0]?.linkedPrs).toEqual([]); + }); + + it("REGRESSION (#6769): a closing-keyword 'Closes PR #N' DOES count as a linked PR", async () => { + const fetchImpl = routedFetch({ + "/repos/acme/widgets/issues": () => jsonResponse([issuePayload({ body: "Closes PR #501" })]), + "/repos/acme/widgets/pulls": () => jsonResponse([]), + "/repos/acme/widgets": () => jsonResponse(REPO_PAYLOAD), + "raw.githubusercontent.com": () => jsonResponse(null, 404), + "api.gittensor.io/miners": () => jsonResponse([]), + }); + + const result = await fetchSelfReviewContext("acme/widgets", { fetchImpl: fetchImpl as never }); + expect(result.issues[0]?.linkedPrs).toEqual([501]); + }); it("returns false for inDuplicateCluster when no linkedIssues are supplied", async () => { const fetchImpl = routedFetch({ "/repos/acme/widgets/issues": () => jsonResponse([issuePayload()]),