From 32f0bff07f7f7369a5a6bd1632dc52578ef854ee Mon Sep 17 00:00:00 2001 From: Kalven Schraut Date: Sat, 5 Sep 2026 16:26:17 -0500 Subject: [PATCH 1/2] fix(pull-requests): retain uncertain Gitea comment pagination --- .../pullRequest/GiteaPullRequestApi.test.ts | 42 +++++++++++++++++++ .../src/pullRequest/GiteaPullRequestApi.ts | 11 ++++- 2 files changed, 51 insertions(+), 2 deletions(-) diff --git a/apps/server/src/pullRequest/GiteaPullRequestApi.test.ts b/apps/server/src/pullRequest/GiteaPullRequestApi.test.ts index 06b2a9904034..5da1e39c707f 100644 --- a/apps/server/src/pullRequest/GiteaPullRequestApi.test.ts +++ b/apps/server/src/pullRequest/GiteaPullRequestApi.test.ts @@ -1337,6 +1337,48 @@ layer("GiteaPullRequestApi", (it) => { }), ); + it.effect.each([1, 4])( + "marks inline comments incomplete when page %i has no pagination evidence", + (pageCount) => + Effect.gen(function* () { + mockedRequest.mockReturnValueOnce( + Effect.succeed( + response([ + { id: 21, body: "Review", state: "COMMENT", submitted_at: "2026-09-03T11:00:00Z" }, + ]), + ), + ); + for (let page = 1; page <= pageCount; page += 1) { + mockedRequest.mockReturnValueOnce( + Effect.succeed( + response( + Array.from({ length: 50 }, (_, index) => ({ + id: page * 50 + index, + body: `Comment ${index + 1}`, + path: "src/a.ts", + position: index + 1, + created_at: "2026-09-03T11:01:00Z", + })), + page === pageCount ? {} : { "x-total-count": "201" }, + ), + ), + ); + } + const api = yield* GiteaPullRequestApi.make; + const result = yield* api.listReviews({ + host: "forge.example.test", + repository: "acme/web", + number: 7, + }); + + assert.isTrue(result.truncated); + expect(result.comments.filter((comment) => comment.kind === "review-comment")).toHaveLength( + 50 * pageCount, + ); + expect(mockedRequest).toHaveBeenCalledTimes(1 + pageCount); + }), + ); + it.effect("does not repeat an unpaginated native review-comment response at the page size", () => Effect.gen(function* () { mockedRequest diff --git a/apps/server/src/pullRequest/GiteaPullRequestApi.ts b/apps/server/src/pullRequest/GiteaPullRequestApi.ts index eb4bc15fd7dd..4e33b5011273 100644 --- a/apps/server/src/pullRequest/GiteaPullRequestApi.ts +++ b/apps/server/src/pullRequest/GiteaPullRequestApi.ts @@ -1079,13 +1079,20 @@ export const make = Effect.gen(function* () { nextLink(result.headers) !== null || totalCount(result.headers) !== null; const paginationNext = input.requirePaginationEvidence && !hasPaginationEvidence ? null : next; + // Native unpaginated endpoints can return more than the requested page size. Exactly + // one requested page is ambiguous when headers do not establish whether more rows exist. + const paginationUncertain = + input.requirePaginationEvidence === true && + !hasPaginationEvidence && + result.rows.length === PAGE_SIZE; if (result.rows.length > remaining || rows.length >= input.limit) { return { rows, - truncated: result.rows.length > remaining || paginationNext !== null, + truncated: + result.rows.length > remaining || paginationNext !== null || paginationUncertain, }; } - if (paginationNext === null) return { rows, truncated: false }; + if (paginationNext === null) return { rows, truncated: paginationUncertain }; path = paginationNext; } return { rows, truncated: true }; From 6114cf39dac017bd665908effd538e5f4f8d5764 Mon Sep 17 00:00:00 2001 From: Kalven Schraut Date: Sat, 5 Sep 2026 16:35:04 -0500 Subject: [PATCH 2/2] fix(pull-requests): share the inline comment budget across reviews --- .../pullRequest/GiteaPullRequestApi.test.ts | 45 +++++++++++++++++++ .../src/pullRequest/GiteaPullRequestApi.ts | 8 +++- 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/apps/server/src/pullRequest/GiteaPullRequestApi.test.ts b/apps/server/src/pullRequest/GiteaPullRequestApi.test.ts index 5da1e39c707f..49540f2078ad 100644 --- a/apps/server/src/pullRequest/GiteaPullRequestApi.test.ts +++ b/apps/server/src/pullRequest/GiteaPullRequestApi.test.ts @@ -1467,6 +1467,51 @@ layer("GiteaPullRequestApi", (it) => { }), ); + it.effect("shares the raw inline-comment budget across reviews", () => + Effect.gen(function* () { + mockedRequest + .mockReturnValueOnce( + Effect.succeed( + response([ + { id: 21, body: "First review", submitted_at: "2026-09-03T11:00:00Z" }, + { id: 22, body: "Second review", submitted_at: "2026-09-03T12:00:00Z" }, + { id: 23, body: "Third review", submitted_at: "2026-09-03T13:00:00Z" }, + ]), + ), + ) + .mockReturnValueOnce( + Effect.succeed( + response([ + ...Array.from({ length: 199 }, (_, index) => ({ + id: index + 31, + body: `Comment ${index + 1}`, + path: "src/a.ts", + position: index + 1, + created_at: "2026-09-03T11:01:00Z", + })), + { id: "malformed" }, + ]), + ), + ); + const api = yield* GiteaPullRequestApi.make; + const result = yield* api.listReviews({ + host: "forge.example.test", + repository: "acme/web", + number: 7, + }); + + expect( + result.comments.filter((comment) => comment.kind === "review").map((comment) => comment.id), + ).toEqual(["review:21", "review:22", "review:23"]); + expect(result.comments.filter((comment) => comment.kind === "review-comment")).toHaveLength( + 199, + ); + assert.isTrue(result.truncated); + expect(mockedRequest).toHaveBeenCalledTimes(2); + expect(callAt(1).path).toContain("/reviews/21/comments?"); + }), + ); + it.effect("follows pagination links when Gitea caps comment pages below the limit", () => Effect.gen(function* () { mockedRequest diff --git a/apps/server/src/pullRequest/GiteaPullRequestApi.ts b/apps/server/src/pullRequest/GiteaPullRequestApi.ts index 4e33b5011273..260b63d9cf8f 100644 --- a/apps/server/src/pullRequest/GiteaPullRequestApi.ts +++ b/apps/server/src/pullRequest/GiteaPullRequestApi.ts @@ -1375,6 +1375,7 @@ export const make = Effect.gen(function* () { const comments: Array = []; const threads: Array = []; let commentsTruncated = reviewsTruncated; + let remainingReviewCommentRows = PAGE_SIZE * CONVERSATION_PAGES; for (const row of reviewRows) { const review = decodeReview(row); if (Option.isNone(review)) continue; @@ -1391,6 +1392,10 @@ export const make = Effect.gen(function* () { reviewState: review.value.state?.toLowerCase().replaceAll("_", " ") ?? null, }); } + if (remainingReviewCommentRows === 0) { + commentsTruncated = true; + continue; + } const codeRows = yield* readUnknownSlice({ operation: "listReviewComments", ...input, @@ -1398,9 +1403,10 @@ export const make = Effect.gen(function* () { `${basePath(input.repository)}/pulls/${input.number}/reviews/${review.value.id}/comments`, { page: 1, limit: PAGE_SIZE }, ), - limit: PAGE_SIZE * CONVERSATION_PAGES, + limit: remainingReviewCommentRows, requirePaginationEvidence: true, }); + remainingReviewCommentRows -= codeRows.rows.length; commentsTruncated ||= codeRows.truncated; const grouped = new Map< string,