diff --git a/packages/cli/src/commands/dashboard/revisions.ts b/packages/cli/src/commands/dashboard/revisions.ts index 0a14c3f91..7cc551bac 100644 --- a/packages/cli/src/commands/dashboard/revisions.ts +++ b/packages/cli/src/commands/dashboard/revisions.ts @@ -169,7 +169,6 @@ export const revisionsCommand = buildCommand({ contextKey, ); - const perPage = Math.min(flags.limit, API_MAX_PER_PAGE); const results: DashboardRevision[] = []; let cursor = rawCursor; let nextCursor: string | undefined; @@ -187,7 +186,12 @@ export const revisionsCommand = buildCommand({ for (let page = 0; page < MAX_PAGINATION_PAGES; page++) { const { data, nextCursor: nc } = await listDashboardRevisionsPaginated(orgSlug, dashboardId, { - perPage, + // Ask only for the remaining budget so the last page's + // cursor points at the first row we did not return. + perPage: Math.min( + flags.limit - results.length, + API_MAX_PER_PAGE, + ), cursor, }); results.push(...data); @@ -202,8 +206,10 @@ export const revisionsCommand = buildCommand({ ); const trimmed = results.slice(0, flags.limit); - const hasMore = results.length > flags.limit || !!nextCursor; - const cursorToStore = hasMore ? nextCursor : undefined; + // A page larger than requested would make the server cursor skip the + // trimmed rows, so drop it (same as autoPaginate). + const cursorToStore = results.length > flags.limit ? undefined : nextCursor; + const hasMore = cursorToStore !== undefined; advancePaginationState( PAGINATION_KEY, diff --git a/packages/cli/test/commands/dashboard/revisions.test.ts b/packages/cli/test/commands/dashboard/revisions.test.ts index ec2f9e260..1d6b3a442 100644 --- a/packages/cli/test/commands/dashboard/revisions.test.ts +++ b/packages/cli/test/commands/dashboard/revisions.test.ts @@ -294,6 +294,55 @@ describe("dashboard revisions command", () => { expect(parsed.hasMore).toBe(false); }); + test("auto-pagination: later pages request only the remaining budget", async () => { + listDashboardRevisionsPaginatedSpy + .mockResolvedValueOnce({ + data: [REVISION_A, REVISION_B], + nextCursor: "cursor-page-2", + }) + .mockResolvedValueOnce({ + data: [REVISION_C], + nextCursor: "cursor-page-3", + }); + + const { context, stdoutWrite } = createMockContext(); + const func = await revisionsCommand.loader(); + await func.call(context, defaultFlags({ json: true, limit: 3 }), "123"); + + expect(listDashboardRevisionsPaginatedSpy).toHaveBeenLastCalledWith( + "test-org", + "123", + { perPage: 1, cursor: "cursor-page-2" }, + ); + const parsed = JSON.parse(stdoutWrite.mock.calls.map((c) => c[0]).join("")); + expect(parsed.data).toHaveLength(3); + expect(parsed.hasMore).toBe(true); + expect(parsed.nextCursor).toBe("cursor-page-3"); + }); + + test("auto-pagination: drops the cursor when a page overshoots the limit", async () => { + // An endpoint that ignores per_page returns more rows than asked for; its + // cursor would skip the trimmed rows on `-c next`. + listDashboardRevisionsPaginatedSpy + .mockResolvedValueOnce({ + data: [REVISION_A, REVISION_B], + nextCursor: "cursor-page-2", + }) + .mockResolvedValueOnce({ + data: [REVISION_C, REVISION_A], + nextCursor: "cursor-page-3", + }); + + const { context, stdoutWrite } = createMockContext(); + const func = await revisionsCommand.loader(); + await func.call(context, defaultFlags({ json: true, limit: 3 }), "123"); + + const parsed = JSON.parse(stdoutWrite.mock.calls.map((c) => c[0]).join("")); + expect(parsed.data).toHaveLength(3); + expect(parsed.hasMore).toBe(false); + expect(parsed.nextCursor).toBeUndefined(); + }); + // ------------------------------------------------------------------------- // Org/dashboard argument parsing // -------------------------------------------------------------------------