From acd5c99d08a259ff2d549e8615934b12cb813995 Mon Sep 17 00:00:00 2001 From: RealDiligent Date: Sun, 26 Jul 2026 22:04:07 +0800 Subject: [PATCH] fix(integrations): raise the comment-marker search page cap to match comments.ts project-tracker-adapter.ts's marker search reused GITHUB_LIST_PAGE_LIMIT = 3, but comments.ts's sibling COMMENT_SEARCH_PAGE_LIMIT was bumped 3 -> 10 (#7232) precisely because a >300-comment thread let the marker hide and caused a duplicate post. This file was never updated, so maybeSuggestProjectOrMilestoneMatch could still double-post its marker comment on any PR/issue with >300 comments. Introduce a dedicated COMMENT_MARKER_SEARCH_PAGE_LIMIT = 10 for the marker search (the milestone/project list searches keep the 3-page bound). Closes #8889 Co-Authored-By: Claude Opus 4.8 --- src/integrations/project-tracker-adapter.ts | 7 +++- test/unit/project-tracker-adapter.test.ts | 39 +++++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/src/integrations/project-tracker-adapter.ts b/src/integrations/project-tracker-adapter.ts index 937531be72..e928e5a85f 100644 --- a/src/integrations/project-tracker-adapter.ts +++ b/src/integrations/project-tracker-adapter.ts @@ -55,6 +55,11 @@ type GitHubMilestone = { // (mirrors src/github/comments.ts's COMMENT_SEARCH_PAGE_LIMIT): 3 pages * 100 = 300 items is generously above // any realistic open-milestone/open-project/PR-comment count, while still bounding worst-case API calls. const GITHUB_LIST_PAGE_LIMIT = 3; +// #8889: the comment-MARKER search specifically must match comments.ts's COMMENT_SEARCH_PAGE_LIMIT, which was +// bumped 3 -> 10 (#7232, commit 18fe74628) because a >300-comment thread let the marker hide and caused a +// duplicate post. The milestone/project LIST searches keep the 3-page bound; only marker detection needs the +// deeper scan to stay double-post-safe. +const COMMENT_MARKER_SEARCH_PAGE_LIMIT = 10; /** A positive-integer milestone/issue number as a string, or null if `value` isn't one. Guards against a * malformed/forged `milestoneId` reaching GitHub's PATCH as `NaN` or a negative/zero number. */ @@ -399,7 +404,7 @@ export async function maybeSuggestProjectOrMilestoneMatch( const octokit = makeInstallationOctokit(ctx.env, token, "live", githubRateLimitAdmissionKeyForInstallation(ctx.installationId)); const botLogin = `${ctx.env.GITHUB_APP_SLUG}[bot]`; let alreadyPosted = false; - for (let page = 1; page <= GITHUB_LIST_PAGE_LIMIT && !alreadyPosted; page += 1) { + for (let page = 1; page <= COMMENT_MARKER_SEARCH_PAGE_LIMIT && !alreadyPosted; page += 1) { const existing = await octokit.request("GET /repos/{owner}/{repo}/issues/{issue_number}/comments", { owner, repo, diff --git a/test/unit/project-tracker-adapter.test.ts b/test/unit/project-tracker-adapter.test.ts index 979615d2bc..a547a1e2c5 100644 --- a/test/unit/project-tracker-adapter.test.ts +++ b/test/unit/project-tracker-adapter.test.ts @@ -639,6 +639,45 @@ describe("maybeSuggestProjectOrMilestoneMatch (#3183/#3184)", () => { expect(posted).toBe(false); }); + it("finds the marker on page 4 (past the old 3-page cap) and does not double-post (#8889)", async () => { + // A >300-comment thread: pages 1-3 are full of unrelated comments, the marker is on page 4. The old + // 3-page cap stopped before page 4, missed the marker, and double-posted. The raised cap must reach it. + const fullPage = Array.from({ length: 100 }, (_, i) => ({ body: `unrelated comment ${i}`, user: { type: "User", login: "someone" } })); + let posted = false; + const requestedPages: number[] = []; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/milestones")) return Response.json([{ number: 14, title: "Self-host reliability roadmap" }]); + if (url.endsWith("/graphql")) return Response.json(noOpenProjectsGraphQlBody()); + if (url.includes("/issues/4/comments") && method === "GET") { + const page = Number(new URL(url).searchParams.get("page") ?? "1"); + requestedPages.push(page); + if (page <= 3) return Response.json(fullPage); + if (page === 4) return Response.json([{ body: PROJECT_TRACKER_SUGGEST_COMMENT_MARKER, user: { type: "Bot", login: "loopover-orb[bot]" } }]); + return Response.json([]); + } + if (url.includes("/issues/4/comments") && method === "POST") { + posted = true; + return Response.json({ id: 1 }); + } + return new Response("unexpected", { status: 500 }); + }); + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), GITHUB_APP_SLUG: "loopover-orb" }); + const result = await maybeSuggestProjectOrMilestoneMatch( + { env, installationId: 123, repoFullName: "JSONbored/gittensory" }, + 4, + "Improve self-host reliability roadmap convergence", + "Follow-up on the self-host reliability roadmap work", + "github", + "https://github.com/JSONbored/gittensory/pull/4", + ); + expect(requestedPages).toEqual([1, 2, 3, 4]); + expect(result).toEqual({ suggested: false }); + expect(posted).toBe(false); + }); + it("does nothing when neither a milestone nor a project matches (never calls the comment POST endpoint)", async () => { let posted = false; vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => {