From 2893d065336f42ecffa54cfd960b33f8b740d205 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Tue, 27 Oct 2026 19:13:52 -0700 Subject: [PATCH] test(native-chat): name the stalled step in the live-history watcher test (#34663) * test(native-chat): name the stalled step in the live-history watcher test A CI run hit the 30 s test timeout with no step named. Each wait now has its own 8 s deadline that reports the stalled step with what the watcher had delivered, and the file sets IS_REACT_ACT_ENVIRONMENT so act() flushes. * test(native-chat): share one 27 s budget across the live-history waits Per-step 8 s deadlines were stricter than the old 30 s test timeout; a shared deadline keeps the total unchanged and only improves the failure message. * test(native-chat): use an AbortSignal budget instead of the wall clock Date.now trips the flaky-test wall-clock rule; AbortSignal.timeout gives the same shared 27 s budget without reading the clock. --- .../e2e/native-chat-live-history.unit.test.ts | 43 ++++++++++++++++--- 1 file changed, 38 insertions(+), 5 deletions(-) diff --git a/tests/e2e/native-chat-live-history.unit.test.ts b/tests/e2e/native-chat-live-history.unit.test.ts index 013eacc759c..71b125194df 100644 --- a/tests/e2e/native-chat-live-history.unit.test.ts +++ b/tests/e2e/native-chat-live-history.unit.test.ts @@ -67,6 +67,8 @@ describe('useNativeChatLiveSession — file watcher pagination composition', () } beforeEach(() => { + // Without it React warns on every act() and may not flush updates inside it. + vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true) useAppStore.setState({ agentStatusByPaneKey: {} }) }) @@ -76,10 +78,13 @@ describe('useNativeChatLiveSession — file watcher pagination composition', () } latest = null vi.clearAllMocks() + vi.unstubAllGlobals() resetTransport() }) it('keeps the original watcher append after a real 300-to-500 loadEarlier', async () => { + // One budget shared by every step, just under the 30 s test timeout, so only the message changes. + const budget = AbortSignal.timeout(27_000) const { mkdtemp, writeFile, appendFile, rm } = await import('node:fs/promises') const { tmpdir } = await import('node:os') const { join } = await import('node:path') @@ -120,6 +125,28 @@ describe('useNativeChatLiveSession — file watcher pagination composition', () sessionId: SESSION, runtimeEnvironmentId: 'env-history' }) + // A CI run hit the 30 s test timeout with no step named; each wait names its own stall. + const delivered: string[] = [] + const within = (step: string, promise: Promise): Promise => + Promise.race([ + promise, + new Promise((_, reject) => { + const stalled = (): void => { + const rows = latest ? ids().length : null + const limits = transport.readSession.mock.calls.map((call) => call[2]) + reject( + new Error( + `Stalled at ${step}: ${JSON.stringify({ delivered, rows, readSessionLimits: limits })}` + ) + ) + } + if (budget.aborted) { + stalled() + } else { + budget.addEventListener('abort', stalled, { once: true }) + } + }) + ]) const initialSnapshotCommitted = Promise.withResolvers() const appendCommitted = Promise.withResolvers() const subscription = await subscribeNativeChatTranscript({ @@ -131,28 +158,34 @@ describe('useNativeChatLiveSession — file watcher pagination composition', () debounceMs: 5, reconciliationIntervalMs: 20, onInitialSnapshot: (messages, hasMore) => { + delivered.push(`snapshot:${messages.length}`) void act(async () => { transport.emit({ type: 'snapshot', messages, hasMore }) }).then(initialSnapshotCommitted.resolve, initialSnapshotCommitted.reject) }, onAppend: (messages) => { + delivered.push(`append:${messages.length}`) void act(async () => { transport.emit({ type: 'appended', messages }) }).then(appendCommitted.resolve, appendCommitted.reject) }, - onReplace: (messages, hasMore) => - act(() => transport.emit({ type: 'replacement', messages, hasMore })) + onReplace: (messages, hasMore) => { + delivered.push(`replace:${messages.length}`) + return act(() => transport.emit({ type: 'replacement', messages, hasMore })) + } }) try { - await initialSnapshotCommitted.promise + await within('initial snapshot', initialSnapshotCommitted.promise) expect(ids()).toEqual(Array.from({ length: 300 }, (_, i) => String(i + 201).padStart(4, '0'))) await act(async () => { - await expect(session().loadEarlier()).resolves.toBe('applied') + await expect(within('loadEarlier 300 to 500', session().loadEarlier())).resolves.toBe( + 'applied' + ) }) expect(transport.readSession).toHaveBeenLastCalledWith(AGENT, SESSION, 500, undefined) expect(ids()).toEqual(Array.from({ length: 500 }, (_, i) => String(i + 1).padStart(4, '0'))) await appendFile(filePath, line(501)) - await appendCommitted.promise + await within('watcher append of line 501', appendCommitted.promise) expect(ids()).toEqual(Array.from({ length: 501 }, (_, i) => String(i + 1).padStart(4, '0'))) expect(ids().at(-1)).toBe('0501') expect(transport.subscribe).toHaveBeenCalledOnce()