Use the history page's itemId in the 4a harness and name what a visible-state wait timed out on

This commit is contained in:
Brennan Benson
2026-11-08 23:58:03 -08:00
parent 6dadd80f21
commit 4e9402a441
3 changed files with 52 additions and 21 deletions
@@ -94,7 +94,7 @@ export async function historyRows(ctx: RunContext): Promise<void> {
continue
}
await sampled(ctx, row, n, () =>
read(ctx, 'agentSession.item', { sessionId, itemId: entry.item.id })
read(ctx, 'agentSession.item', { sessionId, itemId: entry.item.itemId })
)
}
}
@@ -94,15 +94,37 @@ export async function markerSeenAt(
marker: string,
timeoutMs = 120_000
): Promise<number> {
const handle = await page.waitForFunction(
(marker) =>
(window as unknown as { __4a: { seen: Map<string, number> } }).__4a.seen.get(marker) ?? false,
marker,
{ timeout: timeoutMs, polling: 16 }
)
const handle = await page
.waitForFunction(
(marker) =>
(window as unknown as { __4a: { seen: Map<string, number> } }).__4a.seen.get(marker) ??
false,
marker,
{ timeout: timeoutMs, polling: 16 }
)
.catch(async (error: unknown) => {
throw new Error(
`${marker} not visible in ${timeoutMs} ms (${await visibleChatState(page)})`,
{
cause: error
}
)
})
return (await handle.jsonValue()) as number
}
/** One line on the chat the user sees, for timeout messages. */
export function visibleChatState(page: Page): Promise<string> {
return page
.evaluate(() => {
const roots = [...document.querySelectorAll('[data-native-chat-root="true"]')]
const root = roots.find((candidate) => candidate.checkVisibility())
const text = root?.textContent ?? ''
return `${roots.length} roots, visible=${Boolean(root)}, working=${root?.getAttribute('data-native-chat-working')}, tail=${JSON.stringify(text.slice(-160))}`
})
.catch((error: unknown) => `state unreadable: ${String(error)}`)
}
/** Gaps between visible transcript updates inside [from, to], the live-update cadence a reader sees. */
export async function mutationGaps(page: Page, from: number, to: number): Promise<number[]> {
return page.evaluate(
@@ -4,9 +4,9 @@ import type {} from '../../../../tests/e2e/helpers/runtime-types'
import { randomUUID } from 'node:crypto'
import type { Page } from '@stablyai/playwright-test'
import { operationId, type Arm } from './native-chat-4a-arm'
import { rpc } from './native-chat-4a-probes'
import { rpc, visibleChatState } from './native-chat-4a-probes'
type HistoryItem = { id: string; body?: { kind?: string; role?: string; state?: string } }
type HistoryItem = { itemId: string; body?: { kind?: string; role?: string; state?: string } }
export type HistoryPage = {
epoch: string
fence?: number
@@ -155,7 +155,7 @@ export async function userItemIds(page: Page, sessionId: string, limit = 200): P
ids.unshift(
...read.page.items
.filter((item) => item.body?.kind === 'message' && item.body.role === 'user')
.map((item) => item.id)
.map((item) => item.itemId)
)
if (!read.page.hasOlder || ids.length >= limit) {
return ids.slice(-limit)
@@ -212,17 +212,26 @@ export async function waitForWorking(
working: boolean,
timeoutMs = 120_000
): Promise<number> {
const handle = await page.waitForFunction(
(working) => {
const root = [...document.querySelectorAll('[data-native-chat-root="true"]')].find(
(candidate) => candidate.checkVisibility()
const handle = await page
.waitForFunction(
(working) => {
const root = [...document.querySelectorAll('[data-native-chat-root="true"]')].find(
(candidate) => candidate.checkVisibility()
)
return root?.getAttribute('data-native-chat-working') === String(working)
? performance.now()
: false
},
working,
{ timeout: timeoutMs, polling: 16 }
)
.catch(async (error: unknown) => {
throw new Error(
`working=${working} not shown in ${timeoutMs} ms (${await visibleChatState(page)})`,
{
cause: error
}
)
return root?.getAttribute('data-native-chat-working') === String(working)
? performance.now()
: false
},
working,
{ timeout: timeoutMs, polling: 16 }
)
})
return (await handle.jsonValue()) as number
}