From 53732ac5508aa5087e75bcb257abaf907d7871af Mon Sep 17 00:00:00 2001 From: Anthony Guimard Date: Mon, 31 Aug 2026 15:57:45 +0200 Subject: [PATCH 1/2] fix(agent-bff): document the default page size in the openapi document --- packages/agent-bff/src/openapi/schemas.ts | 14 ++++++++++++-- .../test/openapi/openapi-document.test.ts | 14 ++++++++++++++ .../test/openapi/openapi-unfolded.test.ts | 10 ++++++++++ 3 files changed, 36 insertions(+), 2 deletions(-) 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..adee080c33 100644 --- a/packages/agent-bff/test/openapi/openapi-document.test.ts +++ b/packages/agent-bff/test/openapi/openapi-document.test.ts @@ -528,6 +528,20 @@ describe('the documented search inputs', () => { it('should warn that a search query can filter on a collection the BFF does not expose', () => { expect(schemas.Search.description).toContain('does not expose'); }); + + 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'); + 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', () => { 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[] } }; From 2c427c9b526c3eefaf8e92500e899a10edc5e2f9 Mon Sep 17 00:00:00 2001 From: Anthony Guimard Date: Mon, 31 Aug 2026 17:06:27 +0200 Subject: [PATCH 2/2] test(agent-bff): name each documented page behavior in its own test --- packages/agent-bff/test/openapi/openapi-document.test.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/agent-bff/test/openapi/openapi-document.test.ts b/packages/agent-bff/test/openapi/openapi-document.test.ts index adee080c33..80a88ea3fc 100644 --- a/packages/agent-bff/test/openapi/openapi-document.test.ts +++ b/packages/agent-bff/test/openapi/openapi-document.test.ts @@ -528,12 +528,17 @@ describe('the documented search inputs', () => { it('should warn that a search query can filter on a collection the BFF does not expose', () => { expect(schemas.Search.description).toContain('does not expose'); }); +}); +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']); });