Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 132 additions & 1 deletion apps/server/src/pullRequest/GiteaPullRequestApi.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -942,7 +942,138 @@ layer("GiteaPullRequestApi", (it) => {
],
}),
]);
expect(callAt(1).path).toBe("/repos/acme/web/pulls/7/reviews/21/comments");
expect(callAt(1).path).toBe("/repos/acme/web/pulls/7/reviews/21/comments?page=1&limit=50");
}),
);

it.effect(
"marks review activity truncated when nested review comments exceed the conversation bound",
() =>
Effect.gen(function* () {
mockedRequest.mockReturnValueOnce(
Effect.succeed(
response([
{ id: 21, body: "Review", state: "COMMENT", submitted_at: "2026-09-03T11:00:00Z" },
]),
),
);
for (let page = 0; page < 4; page += 1) {
mockedRequest.mockReturnValueOnce(
Effect.succeed(
response(
Array.from({ length: 50 }, (_, index) => ({
id: 31 + page * 50 + index,
body: "Comment",
path: "src/a.ts",
position: 1,
created_at: "2026-09-03T11:01:00Z",
})),
{ "x-total-count": "501" },
),
),
);
}
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).toContainEqual(
expect.objectContaining({ id: "review-comment:31" }),
);
expect(result.comments).toHaveLength(201);
expect(callAt(4).path).toContain("page=4");
expect(mockedRequest).toHaveBeenCalledTimes(5);
}),
);

it.effect("does not repeat an unpaginated native review-comment response at the page size", () =>
Effect.gen(function* () {
mockedRequest
.mockReturnValueOnce(
Effect.succeed(
response([
{
id: 21,
body: "Review",
state: "COMMENT",
submitted_at: "2026-09-03T11:00:00Z",
},
]),
),
)
.mockReturnValueOnce(
Effect.succeed(
response(
Array.from({ length: 51 }, (_, index) => ({
id: index + 31,
body: `Comment ${index + 1}`,
path: "src/a.ts",
position: index + 1,
created_at: "2026-09-03T11:01:00Z",
})),
),
),
);
const api = yield* GiteaPullRequestApi.make;
const result = yield* api.listReviews({
host: "forge.example.test",
repository: "acme/web",
number: 7,
});

assert.isFalse(result.truncated);
assert.strictEqual(
result.comments.filter((comment) => comment.kind === "review-comment").length,
51,
);
assert.strictEqual(mockedRequest.mock.calls.length, 2);
}),
);

it.effect("does not mark an exact unpaginated review-comment safety bound as truncated", () =>
Effect.gen(function* () {
mockedRequest
.mockReturnValueOnce(
Effect.succeed(
response([
{
id: 21,
body: "Review",
state: "COMMENT",
submitted_at: "2026-09-03T11:00:00Z",
},
]),
),
)
.mockReturnValueOnce(
Effect.succeed(
response(
Array.from({ length: 200 }, (_, index) => ({
id: index + 31,
body: `Comment ${index + 1}`,
path: "src/a.ts",
position: index + 1,
created_at: "2026-09-03T11:01:00Z",
})),
),
),
);
const api = yield* GiteaPullRequestApi.make;
const result = yield* api.listReviews({
host: "forge.example.test",
repository: "acme/web",
number: 7,
});

assert.isFalse(result.truncated);
assert.strictEqual(
result.comments.filter((comment) => comment.kind === "review-comment").length,
200,
);
assert.strictEqual(mockedRequest.mock.calls.length, 2);
}),
);

Expand Down
25 changes: 18 additions & 7 deletions apps/server/src/pullRequest/GiteaPullRequestApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -957,6 +957,7 @@ export const make = Effect.gen(function* () {
repository: string;
path: string;
limit: number;
requirePaginationEvidence?: boolean;
}) {
const rows: Array<unknown> = [];
let path = input.path;
Expand All @@ -978,14 +979,18 @@ export const make = Effect.gen(function* () {
rowsSeen,
headers: result.headers,
});
const hasPaginationEvidence =
nextLink(result.headers) !== null || totalCount(result.headers) !== null;
const paginationNext =
input.requirePaginationEvidence && !hasPaginationEvidence ? null : next;
if (result.rows.length > remaining || rows.length >= input.limit) {
return {
rows,
truncated: result.rows.length > remaining || next !== null,
truncated: result.rows.length > remaining || paginationNext !== null,
};
}
if (next === null) return { rows, truncated: false };
path = next;
if (paginationNext === null) return { rows, truncated: false };
path = paginationNext;
}
return { rows, truncated: true };
});
Expand Down Expand Up @@ -1183,7 +1188,7 @@ export const make = Effect.gen(function* () {
}
const comments: Array<PullRequestComment> = [];
const threads: Array<PullRequestReviewThread> = [];
const commentsTruncated = reviewsTruncated;
let commentsTruncated = reviewsTruncated;
for (const row of reviewRows) {
const review = decodeReview(row);
if (Option.isNone(review)) continue;
Expand All @@ -1200,11 +1205,17 @@ export const make = Effect.gen(function* () {
reviewState: review.value.state?.toLowerCase().replaceAll("_", " ") ?? null,
});
}
const codeRows = yield* readUnknownArray({
const codeRows = yield* readUnknownSlice({
operation: "listReviewComments",
...input,
path: `${basePath(input.repository)}/pulls/${input.number}/reviews/${review.value.id}/comments`,
path: query(
`${basePath(input.repository)}/pulls/${input.number}/reviews/${review.value.id}/comments`,
{ page: 1, limit: PAGE_SIZE },
),
limit: PAGE_SIZE * CONVERSATION_PAGES,
requirePaginationEvidence: true,
});
commentsTruncated ||= codeRows.truncated;
const grouped = new Map<
string,
Array<{
Expand All @@ -1216,7 +1227,7 @@ export const make = Effect.gen(function* () {
readonly comment: PullRequestReviewThread["comments"][number];
}>
>();
for (const codeRow of codeRows) {
for (const codeRow of codeRows.rows) {
const decoded = decodeReviewComment(codeRow);
if (Option.isNone(decoded)) continue;
const mapped = decoded.value;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ const response = (value: unknown) => ({
headers: {},
});
const failure = () =>
Effect.fail(new GiteaApi.GiteaApiError({ operation: "test", reason: "failed", detail: "offline" }));
Effect.fail(
new GiteaApi.GiteaApiError({ operation: "test", reason: "failed", detail: "offline" }),
);

const pull = {
number: 7,
Expand Down Expand Up @@ -58,7 +60,7 @@ function route(viewerFails: boolean, reactionsFail: boolean) {
return Effect.succeed(
response([{ id: 2, body: "summary", submitted_at: "2026-01-01T00:00:00Z" }]),
);
if (input.path === "/repos/acme/web/pulls/7/reviews/2/comments")
if (input.path === "/repos/acme/web/pulls/7/reviews/2/comments?page=1&limit=50")
return Effect.succeed(
response([
{ id: 3, body: "inline", created_at: "2026-01-01T00:00:00Z", path: "a.ts", position: 1 },
Expand Down