qa: paginate Slack channel observations

This commit is contained in:
Dallin Romney
2026-08-24 15:53:57 -07:00
parent bc0cfeba6c
commit c5bbd85297
3 changed files with 47 additions and 22 deletions
@@ -328,6 +328,11 @@ export type SlackMessage = Omit<z.infer<typeof slackHistoryMessageSchema>, "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({
@@ -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<SlackAuthIdentity> {
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: {
@@ -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 () => {