Skip to content
Merged
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
80 changes: 67 additions & 13 deletions apps/server/src/pullRequest/GiteaPullRequestApi.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -917,7 +917,7 @@ layer("GiteaPullRequestApi", (it) => {
}),
);

it.effect("returns a page-boundary search match without requesting the page after the cap", () =>
it.effect("rejects a search match at the page cap when its continuation cannot advance", () =>
Effect.gen(function* () {
mockedRequest.mockImplementation((request) => {
if (request.path.includes("/pulls/100"))
Expand All @@ -932,30 +932,84 @@ layer("GiteaPullRequestApi", (it) => {
);
});
const api = yield* GiteaPullRequestApi.make;
const page = yield* api.listPullRequests({
host: "forge.example.test",
repository: "acme/web",
state: "open",
involvement: "all",
viewer: "reviewer",
limit: 1,
query: "match",
});
const error = yield* api
.listPullRequests({
host: "forge.example.test",
repository: "acme/web",
state: "open",
involvement: "all",
viewer: "reviewer",
limit: 1,
query: "match",
})
.pipe(Effect.flip);
const searchPages = mockedRequest.mock.calls
.map(([request]) => request.path)
.filter((path) => path.includes("/issues?"))
.map((path) =>
Number(new URL(path, "https://forge.example.test").searchParams.get("page")),
);

expect(page.items.map((item) => item.number)).toEqual([100]);
assert.strictEqual(page.consumed, 100);
assert.isTrue(page.truncated);
expect(error.detail).toContain("safe page limit");
expect(searchPages).toEqual(Array.from({ length: 100 }, (_, index) => index + 1));
assert.strictEqual(mockedRequest.mock.calls.length, 101);
}),
);

it.effect.each([false, true])(
"follows a search cursor to the final allowed page (more pages: %s)",
(hasMorePages) =>
Effect.gen(function* () {
mockedRequest.mockImplementation((request) => {
const url = new URL(request.path, "https://forge.example.test");
if (url.pathname.endsWith("/pulls/99"))
return Effect.succeed(response(rawPullRequest(99)));
if (url.pathname.endsWith("/pulls/100"))
return Effect.succeed(response(rawPullRequest(100)));
const page = Number(url.searchParams.get("page"));
return Effect.succeed(
response([{ number: page >= 99 ? page : "malformed" }], {
"x-total-count": hasMorePages ? "101" : "100",
}),
);
});
const api = yield* GiteaPullRequestApi.make;
const input = {
host: "forge.example.test",
repository: "acme/web",
state: "open" as const,
involvement: "all" as const,
viewer: "reviewer",
limit: 1,
query: "match",
};
const first = yield* api.listPullRequests(input);
expect(first.items.map((item) => item.number)).toEqual([99]);
expect(first.truncated).toBe(true);
const followUp = api.listPullRequests({
...input,
cursor: { delivered: first.consumed, updatedBefore: "2026-09-02T10:00:00.000Z" },
});
if (hasMorePages) {
const error = yield* followUp.pipe(Effect.flip);
expect(error.detail).toContain("safe page limit");
} else {
const last = yield* followUp;
expect(last.items.map((item) => item.number)).toEqual([100]);
expect(last.consumed).toBe(1);
expect(last.truncated).toBe(false);
}
expect(
mockedRequest.mock.calls.every(
([request]) =>
Number(
new URL(request.path, "https://forge.example.test").searchParams.get("page"),
) <= 100,
),
).toBe(true);
}),
);

it.effect("post-filters merged state and keeps authored search case-insensitive", () =>
Effect.gen(function* () {
mockedRequest
Expand Down
9 changes: 7 additions & 2 deletions apps/server/src/pullRequest/GiteaPullRequestApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -962,8 +962,13 @@ export const make = Effect.gen(function* () {
continue;
collected.push(pullRequest);
if (collected.length === wanted) {
// A raw-row offset can safely continue even when this is the last allowed page; a
// search that cannot fill its requested slice reaches the bounded failure below.
if (page === MAX_PAGINATION_PAGES && next !== null) {
return yield* new GiteaPullRequestApiError({
operation: "listPullRequests",
reason: "failed",
detail: "Gitea pull request pagination exceeded the safe page limit.",
});
}
return {
items: collected,
truncated: index < pageRows.length - 1 || next !== null,
Expand Down