Skip to content

Commit 3d9f527

Browse files
committed
fix(github): report the raw page length from list_repository_issues
list_repository_issues fetches one GitHub page and filters pull requests out of it, which destroys the only pagination signal page-number callers have: the filtered array's length says nothing about the raw page length. A short page may be a full page with PRs mixed in, and an empty page may be 100 consecutive PRs — so any paginating consumer that stops on a short or empty page silently drops every later issue, and no sound termination rule can be built from the filtered response alone. The response now carries pageInfo.fetched — the number of items GitHub returned before filtering — declared in the output schema and in the action description: callers must continue paginating while fetched equals the requested page size, even when issues comes back short or empty. The PR filtering behavior itself is unchanged. Tests cover the mixed page (fetched=3, issues=[1,3]), the all-pull-requests page (fetched=2, issues=[]), and the schema declaration.
1 parent 14eb198 commit 3d9f527

3 files changed

Lines changed: 81 additions & 1 deletion

File tree

src/providers/github/actions.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -757,7 +757,8 @@ export const githubActions: ActionDefinition[] = [
757757
}),
758758
action({
759759
name: "list_repository_issues",
760-
description: "List issues for a GitHub repository. Pull requests are filtered out from the response.",
760+
description:
761+
"List issues for a GitHub repository. Pull requests are filtered out of the response; pageInfo.fetched reports the raw page length before filtering, so paginating callers must continue while fetched equals the requested page size even when the issues array comes back short or empty.",
761762
requiredScopes: githubRepoScopes,
762763
inputSchema: s.object({
763764
owner: nonEmptyString,
@@ -771,6 +772,16 @@ export const githubActions: ActionDefinition[] = [
771772
}),
772773
outputSchema: s.object({
773774
issues: s.array(githubIssueSchema),
775+
pageInfo: s.requiredObject(
776+
"Pagination signals from the raw GitHub page, before pull requests are filtered out.",
777+
{
778+
fetched: s.integer({
779+
minimum: 0,
780+
description:
781+
"Number of items GitHub returned on this page before filtering. Continue paginating while this equals the requested page size.",
782+
}),
783+
},
784+
),
774785
}),
775786
}),
776787
action({
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { describe, expect, it } from "vitest";
2+
import { provider } from "./definition.ts";
3+
import { issueActionHandlers } from "./runtime-issue.ts";
4+
5+
function pageFetcher(items: unknown[]): typeof fetch {
6+
return async () =>
7+
new Response(JSON.stringify(items), {
8+
status: 200,
9+
headers: { "content-type": "application/json" },
10+
});
11+
}
12+
13+
describe("list_repository_issues pagination signal", () => {
14+
it("reports the raw page length before pull requests are filtered out", async () => {
15+
// A raw GitHub page of 3 items where one is a pull request. The
16+
// filtered `issues` array alone would read as a short page and stop
17+
// page-number pagination early; `pageInfo.fetched` preserves the raw
18+
// length so callers can keep paginating correctly.
19+
const result = (await issueActionHandlers.list_repository_issues(
20+
{ owner: "acme", repo: "widgets", perPage: 3 },
21+
{
22+
accessToken: "token",
23+
fetcher: pageFetcher([
24+
{ id: 1, number: 10, title: "real issue" },
25+
{ id: 2, number: 11, title: "a pull request", pull_request: { url: "https://example.test" } },
26+
{ id: 3, number: 12, title: "another issue" },
27+
]),
28+
},
29+
)) as { issues: Array<{ id: number }>; pageInfo: { fetched: number } };
30+
31+
expect(result.issues.map((issue) => issue.id)).toEqual([1, 3]);
32+
expect(result.pageInfo.fetched).toBe(3);
33+
});
34+
35+
it("reports fetched on an all-pull-request page whose issues array is empty", async () => {
36+
const result = (await issueActionHandlers.list_repository_issues(
37+
{ owner: "acme", repo: "widgets", perPage: 2 },
38+
{
39+
accessToken: "token",
40+
fetcher: pageFetcher([
41+
{ id: 1, pull_request: {} },
42+
{ id: 2, pull_request: {} },
43+
]),
44+
},
45+
)) as { issues: unknown[]; pageInfo: { fetched: number } };
46+
47+
expect(result.issues).toEqual([]);
48+
expect(result.pageInfo.fetched).toBe(2);
49+
});
50+
51+
it("declares pageInfo.fetched in the action's output schema", () => {
52+
interface ObjectSchema {
53+
properties?: Record<string, ObjectSchema>;
54+
required?: string[];
55+
type?: string;
56+
}
57+
const action = provider.actions.find((entry) => entry.name === "list_repository_issues");
58+
const pageInfo = (action?.outputSchema as ObjectSchema | undefined)?.properties?.pageInfo;
59+
60+
expect(pageInfo?.properties?.fetched?.type).toBe("integer");
61+
expect(pageInfo?.required).toContain("fetched");
62+
});
63+
});

src/providers/github/runtime-issue.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,7 +289,13 @@ async function listRepositoryIssues(input: Record<string, unknown>, accessToken:
289289
});
290290

291291
return {
292+
// The raw GitHub page mixes issues and pull requests; filtering PRs out
293+
// destroys the only pagination signal page-number callers have (the raw
294+
// page length). `pageInfo.fetched` preserves it: a caller must continue
295+
// paginating while `fetched` equals the requested page size, even when
296+
// `issues` comes back short or empty.
292297
issues: issues.filter((issue) => issue.pull_request == null),
298+
pageInfo: { fetched: issues.length },
293299
};
294300
}
295301

0 commit comments

Comments
 (0)