diff --git a/src/clients/conversations-client.test.ts b/src/clients/conversations-client.test.ts new file mode 100644 index 0000000..9256aa0 --- /dev/null +++ b/src/clients/conversations-client.test.ts @@ -0,0 +1,63 @@ +import { http, HttpResponse } from 'msw' +import { describe, expect, it } from 'vitest' + +import { CommsApi } from '../comms-api' +import { server } from '../testUtils/msw-setup' +import { + TEST_API_BASE_URL as BASE, + TEST_API_TOKEN, + TEST_CONVERSATION_ID, +} from '../testUtils/test-defaults' + +// Pins the wire shape of the `conversations/get` pagination args. `olderThan` +// is a `Date` that the client must convert itself — the transport's generic +// snake-casing would turn a `Date` into an empty object. + +describe('ConversationsClient — wire serialization', () => { + async function captureGetParams( + args: Parameters[0], + ) { + const capturedUrls: URL[] = [] + server.use( + http.get(`${BASE}/conversations/get`, ({ request }) => { + capturedUrls.push(new URL(request.url)) + return HttpResponse.json([]) + }), + ) + + const api = new CommsApi(TEST_API_TOKEN) + await api.conversations.getConversations(args) + + expect(capturedUrls).toHaveLength(1) + return (capturedUrls[0] as URL).searchParams + } + + it('getConversations sends the compound cursor as older_than_ts / before_id', async () => { + const olderThan = new Date('2026-06-25T15:53:53Z') + const params = await captureGetParams({ + workspaceId: 123, + archived: false, + olderThan, + beforeId: TEST_CONVERSATION_ID, + limit: 500, + }) + + expect(params.get('workspace_id')).toBe('123') + expect(params.get('archived')).toBe('false') + expect(params.get('older_than_ts')).toBe(String(Math.floor(olderThan.getTime() / 1000))) + // The Date arg must not leak through the generic snake-casing. + expect(params.has('older_than')).toBe(false) + expect(params.get('before_id')).toBe(TEST_CONVERSATION_ID) + expect(params.get('limit')).toBe('500') + }) + + it('getConversations keeps the minimal call shape unchanged', async () => { + const params = await captureGetParams({ workspaceId: 123 }) + + expect(params.get('workspace_id')).toBe('123') + expect(params.has('archived')).toBe(false) + expect(params.has('older_than_ts')).toBe(false) + expect(params.has('before_id')).toBe(false) + expect(params.has('limit')).toBe(false) + }) +}) diff --git a/src/clients/conversations-client.ts b/src/clients/conversations-client.ts index e762a9c..90c5d8e 100644 --- a/src/clients/conversations-client.ts +++ b/src/clients/conversations-client.ts @@ -47,26 +47,52 @@ export class ConversationsClient extends BaseClient { : ConversationListSchema /** - * Gets all conversations for a workspace. + * Gets a page of conversations for a workspace, newest activity first. + * The server returns at most 500 rows per request (20 by default); pass + * the last row's `lastActive`/`id` as `olderThan`/`beforeId` to fetch + * the next page. Paired, they form a strict compound boundary, so pages + * never repeat rows. Omitting `archived` returns active and archived + * conversations mixed. * * @param args - The arguments for getting conversations. * @param args.workspaceId - The workspace ID. - * @param args.archived - Optional flag to include archived conversations. + * @param args.archived - Optional flag to filter archived (true) or active (false) conversations. + * @param args.olderThan - Optional date to get conversations last active before. + * @param args.beforeId - Optional conversation id. Paired with olderThan it forms the + * strict compound cursor; alone it pages by conversation id order instead. + * @param args.limit - Optional page size (server default 20, max 500). * @returns An array of conversation objects. * * @example * ```typescript - * const conversations = await api.conversations.getConversations({ workspaceId: 123 }) - * conversations.forEach(c => console.log(c.title)) + * const page = await api.conversations.getConversations({ workspaceId: 123, limit: 500 }) + * const last = page.at(-1) + * const nextPage = last + * ? await api.conversations.getConversations({ + * workspaceId: 123, + * limit: 500, + * olderThan: last.lastActive, + * beforeId: last.id, + * }) + * : [] * ``` */ getConversations(args: GetConversationsArgs): Promise { + // Fields are picked explicitly (matching getThreads/getComments) so a + // future Date field can't silently reach the generic snake-casing, + // which would turn it into an empty object on the wire. + const params: Record = { workspaceId: args.workspaceId } + if (args.archived != null) params.archived = args.archived + if (args.olderThan) params.olderThanTs = Math.floor(args.olderThan.getTime() / 1000) + if (args.beforeId != null) params.beforeId = args.beforeId + if (args.limit != null) params.limit = args.limit + return request({ httpMethod: 'GET', baseUri: this.getBaseUri(), relativePath: `${ENDPOINT_CONVERSATIONS}/get`, apiToken: this.apiToken, - payload: args, + payload: params, customFetch: this.customFetch, }).then((response) => this.conversationListSchema.parse(response.data)) } diff --git a/src/types/requests.ts b/src/types/requests.ts index 1648489..9a41a39 100644 --- a/src/types/requests.ts +++ b/src/types/requests.ts @@ -143,6 +143,9 @@ export type GetCommentsArgs = z.infer export const GetConversationsArgsSchema = z.object({ workspaceId: z.number(), archived: z.boolean().nullable().optional(), + olderThan: z.date().nullable().optional(), + beforeId: z.string().nullable().optional(), + limit: z.number().nullable().optional(), }) export type GetConversationsArgs = z.infer