diff --git a/apps/server/src/pullRequest/GiteaPullRequestApi.test.ts b/apps/server/src/pullRequest/GiteaPullRequestApi.test.ts index 3e9ffc237bb3..1ce0cd1d346a 100644 --- a/apps/server/src/pullRequest/GiteaPullRequestApi.test.ts +++ b/apps/server/src/pullRequest/GiteaPullRequestApi.test.ts @@ -2015,8 +2015,8 @@ layer("GiteaPullRequestApi", (it) => { expect(mockedRequest.mock.calls.map((call) => call[0].path)).toEqual([ "/settings/api", "/repos/acme/web/issues/7/reactions?page=1&limit=50", - "/repos/acme/web/issues/comments/12/reactions?page=1&limit=50", - "/repos/acme/web/issues/comments/34/reactions?page=1&limit=50", + "/repos/acme/web/issues/comments/12/reactions", + "/repos/acme/web/issues/comments/34/reactions", ]); }), ); @@ -2074,6 +2074,74 @@ layer("GiteaPullRequestApi", (it) => { }), ); + it.effect.each([50, 200, 201])( + "retains all %i native comment reactions with one unpaginated read", + (count) => + Effect.gen(function* () { + mockedRequest.mockImplementation((input) => { + if (input.path === "/settings/api") return Effect.succeed(response({ features: [] })); + if (input.path.startsWith("/repos/acme/web/issues/7/reactions?")) + return Effect.succeed(response([])); + if (input.path === "/repos/acme/web/issues/comments/12/reactions") + return Effect.succeed( + response( + Array.from({ length: count }, (_, index) => ({ + content: "+1", + user: { login: `reader-${index}` }, + })), + ), + ); + return Effect.die(`unexpected request: ${input.path}`); + }); + const api = yield* GiteaPullRequestApi.make; + const reactions = yield* api.listConversationReactions({ + host: "forge.example.test", + repository: "acme/web", + number: 7, + viewer: "reader-0", + subjectIds: ["issue:12"], + }); + + expect(reactions.bySubjectId.get("issue:12")).toEqual([ + expect.objectContaining({ content: "thumbs-up", count, viewerHasReacted: true }), + ]); + expect(mockedRequest).toHaveBeenCalledTimes(3); + }), + ); + + it.effect.each([false, true])( + "bounds paginated issue reactions with pagination evidence: %s", + (hasEvidence) => + Effect.gen(function* () { + mockedRequest.mockImplementation((input) => { + if (input.path === "/settings/api") return Effect.succeed(response({ features: [] })); + const page = Number( + new URL(input.path, "https://forge.example.test").searchParams.get("page"), + ); + return Effect.succeed( + response( + Array.from({ length: 50 }, (_, index) => ({ + content: "+1", + user: { login: `reader-${page}-${index}` }, + })), + hasEvidence ? { "x-total-count": "201" } : {}, + ), + ); + }); + const api = yield* GiteaPullRequestApi.make; + const reactions = yield* api.listConversationReactions({ + host: "forge.example.test", + repository: "acme/web", + number: 7, + viewer: "reader", + subjectIds: [], + }); + + expect(reactions.pullRequest).toEqual([]); + expect(mockedRequest).toHaveBeenCalledTimes(hasEvidence ? 5 : 2); + }), + ); + it.effect("reports Gitea's missing review-summary reaction route", () => Effect.gen(function* () { mockedRequest.mockReturnValueOnce(Effect.succeed(response({ features: [] }))); diff --git a/apps/server/src/pullRequest/GiteaPullRequestApi.ts b/apps/server/src/pullRequest/GiteaPullRequestApi.ts index 260b63d9cf8f..a8eb58a60cdc 100644 --- a/apps/server/src/pullRequest/GiteaPullRequestApi.ts +++ b/apps/server/src/pullRequest/GiteaPullRequestApi.ts @@ -1051,8 +1051,8 @@ export const make = Effect.gen(function* () { repository: string; path: string; limit: number; - requirePaginationEvidence?: boolean; nullAsEmpty?: boolean; + requirePaginationEvidence?: boolean; }) { const rows: Array = []; let path = input.path; @@ -1781,31 +1781,30 @@ export const make = Effect.gen(function* () { } const reactions = yield* Effect.all( targets.map((entry) => - readUnknownSlice({ - operation: "listConversationReactions", - host: input.host, - repository: input.repository, - path: - entry.target.kind === "pull-request" - ? query(`${basePath(input.repository)}/issues/${input.number}/reactions`, { - page: 1, - limit: PAGE_SIZE, - }) - : entry.target.kind === "review" - ? query( - `${basePath(input.repository)}/pulls/${input.number}/reviews/${entry.target.id}/reactions`, - { page: 1, limit: PAGE_SIZE }, - ) - : query( - `${basePath(input.repository)}/issues/comments/${entry.target.id}/reactions`, - { - page: 1, - limit: PAGE_SIZE, - }, - ), - limit: PAGE_SIZE * MAX_PAGINATION_PAGES, - nullAsEmpty: true, - }).pipe( + (entry.target.kind === "comment" + ? // Gitea's comment-reaction handler returns the whole list and ignores pagination. + readUnknownPage({ + operation: "listConversationReactions", + host: input.host, + repository: input.repository, + path: `${basePath(input.repository)}/issues/comments/${entry.target.id}/reactions`, + nullAsEmpty: true, + }).pipe(Effect.map(({ rows }) => ({ rows, truncated: false }))) + : readUnknownSlice({ + operation: "listConversationReactions", + host: input.host, + repository: input.repository, + path: query( + entry.target.kind === "review" + ? `${basePath(input.repository)}/pulls/${input.number}/reviews/${entry.target.id}/reactions` + : `${basePath(input.repository)}/issues/${input.number}/reactions`, + { page: 1, limit: PAGE_SIZE }, + ), + limit: PAGE_SIZE * CONVERSATION_PAGES, + requirePaginationEvidence: true, + nullAsEmpty: true, + }) + ).pipe( Effect.map((result) => ({ subjectId: entry.subjectId, reactions: result.truncated