import { describe, expect, it } from 'vitest' import { RECONNECT, reconnectDelay } from '../src/shared/session-protocol' /** The nominal delay before jitter, for a given 1-based attempt. */ function nominal(attempt: number): number { return Math.min( RECONNECT.maxDelayMs, RECONNECT.initialDelayMs * 2 ** Math.max(0, attempt - 1), ) } describe('reconnectDelay', () => { it('stays within the jitter band on every attempt', () => { for (let attempt = 1; attempt <= RECONNECT.maxAttempts + 5; attempt++) { const delay = reconnectDelay(attempt) const base = nominal(attempt) expect(delay).toBeGreaterThanOrEqual(Math.round(base * (1 - RECONNECT.jitter)) - 1) expect(delay).toBeLessThanOrEqual(Math.round(base * (1 + RECONNECT.jitter)) + 1) } }) it('starts short enough that a blip is barely noticed', () => { // A first retry the user has to wait through feels like a broken app. expect(reconnectDelay(1)).toBeLessThanOrEqual(1_500) }) it('climbs, and does not exceed the ceiling', () => { expect(nominal(2)).toBeGreaterThan(nominal(1)) for (let attempt = 1; attempt <= 30; attempt++) { expect(reconnectDelay(attempt)).toBeLessThanOrEqual( Math.round(RECONNECT.maxDelayMs * (1 + RECONNECT.jitter)) + 1, ) } }) it('is never zero or negative', () => { // A zero delay would turn the retry loop into a busy loop against a server // that is already having a bad day. for (let attempt = 1; attempt <= 30; attempt++) { expect(reconnectDelay(attempt)).toBeGreaterThan(0) } }) it('varies between calls, so sessions that drop together do not retry in lockstep', () => { // This is the whole reason for the jitter: a network failure takes every // session down at once, and identical delays would have them all hit the // server on the same tick. const samples = new Set(Array.from({ length: 40 }, () => reconnectDelay(4))) expect(samples.size).toBeGreaterThan(1) }) it('handles a non-positive attempt without producing nonsense', () => { expect(reconnectDelay(0)).toBeGreaterThan(0) expect(reconnectDelay(-3)).toBeGreaterThan(0) }) }) describe('RECONNECT limits', () => { it('gives up eventually rather than retrying forever', () => { expect(RECONNECT.maxAttempts).toBeGreaterThan(0) expect(RECONNECT.maxAttempts).toBeLessThanOrEqual(50) }) it('spans roughly five minutes of trying', () => { // Long enough to ride out a reboot or a flaky link, short enough that a // session pointed at a decommissioned host stops rather than living // forever in a retry loop. let total = 0 for (let attempt = 1; attempt <= RECONNECT.maxAttempts; attempt++) { total += nominal(attempt) } expect(total).toBeGreaterThan(60_000) expect(total).toBeLessThan(15 * 60_000) }) })