From 928c2e390554a2318e072a0f9b0cc1b79465abbe Mon Sep 17 00:00:00 2001 From: t Date: Sat, 19 Sep 2026 14:28:49 -0700 Subject: [PATCH] test(fresh-agent): deflake the b8ke handoff backoff window test with fake timers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The HANDOFF_IN_PROGRESS retry test asserted a 350ms wall-clock sleep landed mid-backoff (750ms). Under full-suite parallel load the act()-queueing gap before the sleep started could itself exceed the backoff, so the check landed after the timer legitimately fired — the base gate at origin/main hit exactly this (1 of 7,165). Advance the real timer clock deterministically instead (the file's snapshot-debounce deflake precedent), keyed on SESSION_HANDOFF_RETRY_BACKOFF_MS rather than a literal, and extend the tail: no stacked timer fires after the single re-invocation. Mutation-checked: backoff=0 fails the test. --- .../fresh-agent/FreshAgentView.test.tsx | 91 ++++++++++++------- 1 file changed, 57 insertions(+), 34 deletions(-) diff --git a/test/unit/client/components/fresh-agent/FreshAgentView.test.tsx b/test/unit/client/components/fresh-agent/FreshAgentView.test.tsx index a9017dc7b..541d36b77 100644 --- a/test/unit/client/components/fresh-agent/FreshAgentView.test.tsx +++ b/test/unit/client/components/fresh-agent/FreshAgentView.test.tsx @@ -26,6 +26,7 @@ import { updateTab } from '@/store/tabsSlice' import { handleFreshAgentMessage } from '@/lib/fresh-agent-ws' import { ApiError } from '@/lib/api' import { resetSnapshotSchedulerForTests, SNAPSHOT_DEBOUNCE_MS } from '@/lib/fresh-agent-snapshot-scheduler' +import { SESSION_HANDOFF_RETRY_BACKOFF_MS } from '@/lib/session-handoff' import { ROLLBACK_BUSY_REDO_NOTICE, ROLLBACK_BUSY_UNDO_NOTICE, @@ -10056,44 +10057,66 @@ describe('fresh-agent runtime-owner divergence recovery (kata b8ke)', () => { }) it('HANDOFF_IN_PROGRESS retry waits out the backoff before re-invoking (never an immediate tight loop)', async () => { - const store = createStore() - store.dispatch(initLayout({ tabId: 'tab-1', paneId: 'pane-1', content: divergencePaneContent() })) - store.dispatch(setPaneHandoffError({ - tabId: 'tab-1', - paneId: 'pane-1', - error: { - code: 'HANDOFF_IN_PROGRESS', - message: 'a lifecycle operation is in flight; retry after it settles', - retryable: true, - generation: 2, - }, - })) - // The retry's handoff fails again — the pane stays; only the timing of - // the single re-invocation is asserted. - apiMock.requestSessionHandoff.mockResolvedValue({ - ok: false, - error: { code: 'HANDOFF_IN_PROGRESS', message: 'still in flight', retryable: true, ownerGeneration: 2 }, - }) - render( - - - , - ) + // A wall-clock mid-window sleep (350ms < the 750ms backoff) races CPU + // contention under parallel suites — the act()-queueing gap before the + // sleep started could itself exceed the backoff, so the check landed + // after the timer legitimately fired (the snapshot-debounce sibling's + // note fixed the same class of flake). Advance the real timer clock + // deterministically instead: nothing wall-clock remains. + vi.useFakeTimers() + try { + const store = createStore() + store.dispatch(initLayout({ tabId: 'tab-1', paneId: 'pane-1', content: divergencePaneContent() })) + store.dispatch(setPaneHandoffError({ + tabId: 'tab-1', + paneId: 'pane-1', + error: { + code: 'HANDOFF_IN_PROGRESS', + message: 'a lifecycle operation is in flight; retry after it settles', + retryable: true, + generation: 2, + }, + })) + // The retry's handoff fails again — the pane stays; only the timing of + // the single re-invocation is asserted. + apiMock.requestSessionHandoff.mockResolvedValue({ + ok: false, + error: { code: 'HANDOFF_IN_PROGRESS', message: 'still in flight', retryable: true, ownerGeneration: 2 }, + }) + render( + + + , + ) + await act(async () => { await vi.advanceTimersByTimeAsync(0) }) - const banner = await screen.findByRole('alert') - const retry = within(banner).getByRole('button', { name: /retry reopening/i }) - fireEvent.click(retry) + const banner = screen.getByRole('alert') + const retry = within(banner).getByRole('button', { name: /retry reopening/i }) + fireEvent.click(retry) - // Synchronous and mid-backoff: nothing sent yet. The backoff (750ms) - // cannot have elapsed at 350ms wall-clock. - await act(async () => { await new Promise((resolve) => setTimeout(resolve, 350)) }) - expect(apiMock.requestSessionHandoff).not.toHaveBeenCalled() + // Synchronous: nothing sent on click itself (never a tight loop). + expect(apiMock.requestSessionHandoff).not.toHaveBeenCalled() - // After the backoff the single re-invocation leaves. - await waitFor(() => { + // Mid-backoff: one tick before the deadline the timer cannot have fired. + await act(async () => { + await vi.advanceTimersByTimeAsync(SESSION_HANDOFF_RETRY_BACKOFF_MS - 1) + }) + expect(apiMock.requestSessionHandoff).not.toHaveBeenCalled() + + // At the full backoff exactly one re-invocation leaves... + await act(async () => { + await vi.advanceTimersByTimeAsync(1) + }) expect(apiMock.requestSessionHandoff).toHaveBeenCalledTimes(1) - }, { timeout: 5_000 }) - expect(apiMock.requestSessionHandoff).toHaveBeenCalledTimes(1) + // ...and no stacked timer follows it: a full further window stays + // silent. + await act(async () => { + await vi.advanceTimersByTimeAsync(SESSION_HANDOFF_RETRY_BACKOFF_MS) + }) + expect(apiMock.requestSessionHandoff).toHaveBeenCalledTimes(1) + } finally { + vi.useRealTimers() + } }) it('every typed handoff-failure code composes: the banner renders the typed message with a Retry that re-invokes the same identity', async () => {