fix(mobile): wait through relay reconnect cutover

This commit is contained in:
Brennan Benson
2026-08-01 15:29:31 -07:00
parent 09e264600b
commit 6152cb3dd2
2 changed files with 64 additions and 2 deletions
@@ -2,8 +2,12 @@ import { afterEach, describe, expect, it, vi } from 'vitest'
import type { RpcClient, SendRequestOptions } from './rpc-client'
import type { ConnectionState, RpcResponse } from './types'
import { verifyForceReconnectRpcHealth } from './force-reconnect-rpc-health'
import { waitForMobileRelayRpcConnected } from './mobile-relay-rpc-connect-wait'
import { markRpcDeliveryUnknown } from './rpc-delivery-ambiguity'
import { LogicalClientCutoverError } from './stable-logical-rpc-client'
import {
createStableLogicalRpcClient,
LogicalClientCutoverError
} from './stable-logical-rpc-client'
import { RpcApplicationResponsiveness } from './rpc-application-responsiveness'
describe('Force Reconnect RPC health', () => {
@@ -183,4 +187,58 @@ describe('Force Reconnect RPC health', () => {
await expect(verification).resolves.toBeUndefined()
expect(sendRequest).toHaveBeenCalledTimes(2)
})
it('waits for stable-client migration when Relay disconnects before probe delivery', async () => {
vi.useFakeTimers()
let relayDeliveries = 0
const relaySendRequest = vi.fn(async () => {
await waitForMobileRelayRpcConnected({
getState: () => 'disconnected',
subscribe: () => () => {},
timeoutMs: 15_000
})
relayDeliveries += 1
return { id: 'rpc-old', ok: true, result: {} } as RpcResponse
})
const relay = {
sendRequest: relaySendRequest,
getState: () => 'disconnected' as const,
onStateChange: () => () => {},
close: vi.fn()
} as unknown as RpcClient
const replacementSendRequest = vi.fn(
async () => ({ id: 'rpc-new', ok: true, result: {} }) as RpcResponse
)
const replacement = {
sendRequest: replacementSendRequest,
getState: () => 'connected' as const,
onStateChange: () => () => {},
close: vi.fn()
} as unknown as RpcClient
const logical = createStableLogicalRpcClient(relay, 'relay')
let outcome = 'pending'
const verification = verifyForceReconnectRpcHealth(logical).then(
() => {
outcome = 'resolved'
},
(error: Error) => {
outcome = error.message
}
)
await vi.advanceTimersByTimeAsync(14_000)
expect(outcome).toBe('pending')
await logical.migrateTo(replacement, 'relay')
await verification
expect(outcome).toBe('resolved')
expect(relayDeliveries).toBe(0)
expect(relaySendRequest).toHaveBeenCalledOnce()
expect(replacementSendRequest).toHaveBeenCalledOnce()
expect(replacementSendRequest).toHaveBeenCalledWith(
'worktree.ps',
{ limit: 1 },
expect.objectContaining({ timeoutMs: 1_000, strictDeadline: true })
)
})
})
@@ -53,7 +53,11 @@ function isRecoverableHealthError(
isLogicalClientCutoverError(error) ||
isRpcDeliveryUnknown(error) ||
(error instanceof Error &&
['Connection interrupted', 'relay session not connected'].includes(error.message))
[
'Connection interrupted',
'relay session disconnected',
'relay session not connected'
].includes(error.message))
)
}