From c5bbd852977baff7cc0137880657336e9849f0e5 Mon Sep 17 00:00:00 2001 From: Dallin Romney Date: Mon, 24 Aug 2026 15:53:57 -0700 Subject: [PATCH] qa: paginate Slack channel observations --- .../slack/slack-live.contracts.ts | 5 +++ .../slack/slack-live.observations.ts | 35 ++++++++++++------- .../slack/slack-live.runtime.test.ts | 29 +++++++++------ 3 files changed, 47 insertions(+), 22 deletions(-) diff --git a/extensions/qa-lab/src/live-transports/slack/slack-live.contracts.ts b/extensions/qa-lab/src/live-transports/slack/slack-live.contracts.ts index 36a79687e44a..a0eb1547ef48 100644 --- a/extensions/qa-lab/src/live-transports/slack/slack-live.contracts.ts +++ b/extensions/qa-lab/src/live-transports/slack/slack-live.contracts.ts @@ -328,6 +328,11 @@ export type SlackMessage = Omit, "ts"> export const slackHistorySchema = z.object({ ok: z.boolean().optional(), messages: z.array(slackHistoryMessageSchema).optional(), + response_metadata: z + .object({ + next_cursor: z.string().optional(), + }) + .optional(), }); export const slackRepliesSchema = z.object({ diff --git a/extensions/qa-lab/src/live-transports/slack/slack-live.observations.ts b/extensions/qa-lab/src/live-transports/slack/slack-live.observations.ts index b0cb798e775e..19bb7e642dd7 100644 --- a/extensions/qa-lab/src/live-transports/slack/slack-live.observations.ts +++ b/extensions/qa-lab/src/live-transports/slack/slack-live.observations.ts @@ -26,9 +26,10 @@ import { } from "./slack-live.contracts.js"; import { buildSlackInvalidBlocksTableProbe } from "./slack-live.invalid-blocks.js"; -// Isolated Slack flows share one live QA channel. Presentation sends precede -// their final markers, so retain enough history to survive concurrent traffic. -const SLACK_QA_CHANNEL_HISTORY_LIMIT = 200; +// Isolated flows share one channel, so presentation sends can move across pages +// before their final markers arrive. Cap traversal to bound each polling pass. +const SLACK_QA_CHANNEL_HISTORY_PAGE_LIMIT = 200; +const SLACK_QA_CHANNEL_HISTORY_MAX_PAGES = 5; export async function getSlackIdentity(token: string): Promise { const client = createSlackWebClient(token, { timeout: SLACK_QA_WEB_API_TIMEOUT_MS }); @@ -70,15 +71,25 @@ export async function listSlackMessages(params: { client: WebClient; oldestTs: string; }) { - const history = slackHistorySchema.parse( - await params.client.conversations.history({ - channel: params.channelId, - inclusive: true, - limit: SLACK_QA_CHANNEL_HISTORY_LIMIT, - oldest: params.oldestTs, - }), - ); - return history.messages ?? []; + const messages: SlackMessage[] = []; + let cursor: string | undefined; + for (let page = 0; page < SLACK_QA_CHANNEL_HISTORY_MAX_PAGES; page += 1) { + const history = slackHistorySchema.parse( + await params.client.conversations.history({ + ...(cursor ? { cursor } : {}), + channel: params.channelId, + inclusive: true, + limit: SLACK_QA_CHANNEL_HISTORY_PAGE_LIMIT, + oldest: params.oldestTs, + }), + ); + messages.push(...(history.messages ?? [])); + cursor = history.response_metadata?.next_cursor?.trim() || undefined; + if (!cursor) { + break; + } + } + return messages; } export async function listSlackThreadMessages(params: { diff --git a/extensions/qa-lab/src/live-transports/slack/slack-live.runtime.test.ts b/extensions/qa-lab/src/live-transports/slack/slack-live.runtime.test.ts index 95f03fb08d8a..f95eb664596c 100644 --- a/extensions/qa-lab/src/live-transports/slack/slack-live.runtime.test.ts +++ b/extensions/qa-lab/src/live-transports/slack/slack-live.runtime.test.ts @@ -993,12 +993,10 @@ describe("Slack live QA runtime helpers", () => { throw new Error("missing Slack chart scenario verifier"); } const accessibleText = renderExpectedSlackChartAccessibleText(summaryText); - const history = vi.fn(async (request: { limit: number }) => ({ - // Shared-channel concurrency can push the earlier chart beyond the old - // 50-message observation window before the final marker arrives. - messages: - request.limit >= 200 - ? [ + const history = vi.fn(async (request: { cursor?: string }) => + request.cursor === "page-2" + ? { + messages: [ { blocks: [ { @@ -1028,9 +1026,13 @@ describe("Slack live QA runtime helpers", () => { ts: "2.000000", user: "U999999999", }, - ] - : [], - })); + ], + } + : { + messages: [], + response_metadata: { next_cursor: "page-2" }, + }, + ); await expect( afterReply( @@ -1043,12 +1045,19 @@ describe("Slack live QA runtime helpers", () => { } as never, ), ).resolves.toBe("verified native data_visualization block and deterministic accessible text"); - expect(history).toHaveBeenCalledWith({ + expect(history).toHaveBeenNthCalledWith(1, { channel: "C123456789", inclusive: true, limit: 200, oldest: "1.000000", }); + expect(history).toHaveBeenNthCalledWith(2, { + channel: "C123456789", + cursor: "page-2", + inclusive: true, + limit: 200, + oldest: "1.000000", + }); }); it("rejects fallback-only Slack chart delivery", async () => {