diff --git a/mobile/src/transport/mobile-relay-credential-refresh.test.ts b/mobile/src/transport/mobile-relay-credential-refresh.test.ts index 0d423a4ed62..3fcc428a07e 100644 --- a/mobile/src/transport/mobile-relay-credential-refresh.test.ts +++ b/mobile/src/transport/mobile-relay-credential-refresh.test.ts @@ -12,8 +12,9 @@ const rotated = { relay: { v: 1, directorUrl: 'https://relay.example', cellUrl: 'https://c1.relay.example' } } const rotate = vi.hoisted(() => vi.fn()) +const needsRotation = vi.hoisted(() => vi.fn(() => true)) vi.mock('./mobile-relay-credential-rotation', () => ({ - mobileRelayCredentialNeedsRotation: () => true, + mobileRelayCredentialNeedsRotation: needsRotation, rotateMobileRelayCredential: rotate })) @@ -40,6 +41,14 @@ function fixture(overrides: { persistResolvedRelay?: () => Promise } = {}) } describe('MobileRelayCredentialRefresh', () => { + it('does nothing unforced while the credential is still fresh', async () => { + needsRotation.mockReturnValueOnce(false) + const { refresh, order } = fixture() + await refresh.run(false) + expect(order).toEqual([]) + expect(rotate).not.toHaveBeenCalled() + }) + it('lifts the gate and starts the relay race only after the endpoint is durable', async () => { const { refresh, order } = fixture() await refresh.run(true) diff --git a/mobile/src/transport/rpc-session-liveness-watchdog.test.ts b/mobile/src/transport/rpc-session-liveness-watchdog.test.ts index ca6599f213e..0ceaad449b7 100644 --- a/mobile/src/transport/rpc-session-liveness-watchdog.test.ts +++ b/mobile/src/transport/rpc-session-liveness-watchdog.test.ts @@ -276,14 +276,13 @@ describe('RpcSessionLivenessWatchdog', () => { }) watchdog.start(identity) + // Resumes every 1.5 s on a black-holed socket: each lands inside the 2 s urgent + // window, so none may re-arm the deadline or the verdict never comes. watchdog.probeNow(identity, 'resume') - await vi.advanceTimersByTimeAsync(2_000) - expect(terminate).not.toHaveBeenCalled() - // The second resume restarts the 2 s clock on the miss already booked. - watchdog.probeNow(identity, 'resume') - await vi.advanceTimersByTimeAsync(1_000) - watchdog.probeNow(identity, 'resume') - await vi.advanceTimersByTimeAsync(2_000) + for (let elapsed = 0; elapsed < 6_000; elapsed += 1_500) { + await vi.advanceTimersByTimeAsync(1_500) + watchdog.probeNow(identity, 'resume') + } expect(terminate).toHaveBeenCalledOnce() }) diff --git a/mobile/src/transport/rpc-session-liveness-watchdog.ts b/mobile/src/transport/rpc-session-liveness-watchdog.ts index 4b487766a20..cf8f40a67be 100644 --- a/mobile/src/transport/rpc-session-liveness-watchdog.ts +++ b/mobile/src/transport/rpc-session-liveness-watchdog.ts @@ -127,9 +127,12 @@ export class RpcSessionLivenessWatchdog { this.lastVoluntaryProbeAt = now // Why: a resume is a new observation on a cold radio, so it starts the urgent window // with a clean budget (startProbe zeroes the count on a profile switch). A resume that - // lands while an urgent probe is already in flight restarts the clock but keeps the - // count: otherwise repeated resumes, or a user tapping reconnect on a dead socket, - // zero the budget on every tap and the verdict never lands. + // lands while an urgent probe is already in flight keeps that probe's deadline and + // count: re-arming the 2 s clock on every tap would let a user tapping reconnect, or + // an AppState flap, hold a dead socket open for as long as they keep tapping. + if (urgent && this.probing && this.profile === this.urgentProfile) { + return + } this.startProbe(identity, urgent ? this.urgentProfile : this.ordinaryProfile) }