diff --git a/packages/agent-bff/src/openapi/schemas.ts b/packages/agent-bff/src/openapi/schemas.ts index 23dacfc4c6..9f1aa14cb0 100644 --- a/packages/agent-bff/src/openapi/schemas.ts +++ b/packages/agent-bff/src/openapi/schemas.ts @@ -53,7 +53,12 @@ export const SortClauseSchema = SortClauseInput.openapi('SortClause', { export const PageSchema = PageInput.openapi('Page', { description: 'The agent paginates by page number, so `offset` must be a whole multiple of `limit`. ' + - 'Any other offset is rejected with 400 invalid_request.', + 'Any other offset is rejected with 400 invalid_request. `limit` and `offset` are both ' + + 'required once `page` is sent, but the object itself is optional on every list — and ' + + 'omitting it is not a request for the whole collection. The BFF then forwards no ' + + 'pagination, so the agent applies its own default: the first page (offset 0), a limit ' + + 'of 15 records on the Node agent. Anything past that page is silently missing. Send ' + + '`page` and walk it to read a collection in full.', }); export const TimezoneSchema = TimezoneInput.openapi('Timezone', { @@ -161,7 +166,12 @@ export const ListResponseSchema = z .openapi('ListResponse', { description: 'Records are flat, each carrying a `__forest` envelope. The list never carries a total: ' + - 'call the count endpoint for that, which is why `countStatus` is always `not_requested`.', + 'call the count endpoint for that, which is why `countStatus` is always `not_requested`. ' + + 'It is always one page, not guaranteed to be the whole collection: a request that omitted ' + + "`page` still gets a page, the agent's default (up to 15 records from the start on the " + + 'Node agent), so a response of exactly that default length is probably truncated, and ' + + 'nothing in the body says so. Send `page` and walk it. Count gives the total unless the ' + + 'collection disables it.', }); export const CountResponseSchema = z diff --git a/packages/agent-bff/test/openapi/openapi-document.test.ts b/packages/agent-bff/test/openapi/openapi-document.test.ts index eb62e23b72..80a88ea3fc 100644 --- a/packages/agent-bff/test/openapi/openapi-document.test.ts +++ b/packages/agent-bff/test/openapi/openapi-document.test.ts @@ -530,6 +530,25 @@ describe('the documented search inputs', () => { }); }); +describe('the documented pagination inputs', () => { + it('should publish the default page applied when page is absent', () => { + expect(schemas.Page.description).toContain('the object itself is optional'); + expect(schemas.Page.description).toContain('the first page (offset 0)'); + expect(schemas.Page.description).toContain('a limit of 15 records on the Node agent'); + expect(schemas.Page.description).toContain('silently missing'); + }); + + it('should require both limit and offset once page is sent', () => { + expect(schemas.Page.required).toEqual(['limit', 'offset']); + }); + + it('should warn on the response that a page-less list is one page, not the collection', () => { + expect(schemas.ListResponse.description).toContain('not guaranteed to be the whole collection'); + expect(schemas.ListResponse.description).toContain('up to 15 records from the start'); + expect(schemas.ListResponse.description).toContain('probably truncated'); + }); +}); + describe('serializeOpenApi', () => { it('should produce indented JSON that parses back to the same document', () => { const serialized = serializeOpenApi(document); diff --git a/packages/agent-bff/test/openapi/openapi-unfolded.test.ts b/packages/agent-bff/test/openapi/openapi-unfolded.test.ts index 1c9de502d1..5a54b8d71f 100644 --- a/packages/agent-bff/test/openapi/openapi-unfolded.test.ts +++ b/packages/agent-bff/test/openapi/openapi-unfolded.test.ts @@ -192,6 +192,16 @@ describe('the unfolded document', () => { expect(request.properties.sort.items?.$ref).toBe('#/components/schemas/SortClause_My_Coll'); }); + it('should leave page optional here too, pointing at the shared Page component', () => { + const request = requestSchema('My%20Coll/list') as unknown as { + required?: string[]; + properties: Record; + }; + + expect(request.required ?? []).not.toContain('page'); + expect(request.properties.page.$ref).toBe('#/components/schemas/Page'); + }); + it('should make every leaf and the branch mutually exclusive, which the runtime enforces', () => { const branch = branchOf('Filter_My_Coll') as unknown as { not: { required: string[] } };