import assert from "node:assert/strict"; import { test } from "node:test"; import { MAX_RECONNECT_ATTEMPTS, MAX_RECONNECT_DELAY_MS, ReconnectPolicy, reconnectDelayMs, } from "./reconnect.logic.ts"; /** Pin the jitter to the middle of its range, giving the base delay exactly. */ const noJitter = () => 0.5; test("delay doubles per attempt", () => { assert.equal(reconnectDelayMs(0, noJitter), 1_000); assert.equal(reconnectDelayMs(1, noJitter), 2_000); assert.equal(reconnectDelayMs(2, noJitter), 4_000); assert.equal(reconnectDelayMs(3, noJitter), 8_000); }); test("delay is capped", () => { assert.equal(reconnectDelayMs(6, noJitter), MAX_RECONNECT_DELAY_MS); assert.equal(reconnectDelayMs(20, noJitter), MAX_RECONNECT_DELAY_MS); // The exponent ceiling exists so a large attempt count cannot overflow into // Infinity and produce a NaN delay. assert.equal(reconnectDelayMs(1e6, noJitter), MAX_RECONNECT_DELAY_MS); assert.ok(Number.isFinite(reconnectDelayMs(1e6, noJitter))); }); test("jitter stays within twenty percent either side", () => { for (const attempt of [0, 1, 2, 3, 4, 5, 6, 10]) { const base = Math.min(1000 * 2 ** Math.min(attempt, 6), MAX_RECONNECT_DELAY_MS); for (const r of [0, 0.25, 0.5, 0.75, 0.999]) { const delay = reconnectDelayMs(attempt, () => r); assert.ok( delay >= base * 0.8 - 1 && delay <= base * 1.2 + 1, `attempt ${attempt} with random ${r} gave ${delay}, outside 20% of ${base}`, ); } } }); test("jitter never pushes the delay past the documented cap", () => { // +20% on a capped base is ~72s; the result must still honor MAX_RECONNECT_DELAY_MS. for (const attempt of [6, 10, 20, 1e6]) { assert.equal(reconnectDelayMs(attempt, () => 0.999), MAX_RECONNECT_DELAY_MS); } }); test("jitter actually spreads clients out", () => { // The point of jitter is that a fleet reconnecting after one deploy does not // arrive in lockstep. Identical delays would defeat it. const low = reconnectDelayMs(3, () => 0); const high = reconnectDelayMs(3, () => 0.999); assert.ok(high - low > 1_000, `spread was only ${high - low}ms`); }); test("delay is never negative", () => { assert.ok(reconnectDelayMs(0, () => 0) >= 0); }); test("policy backs off then gives up", () => { const policy = new ReconnectPolicy(noJitter); for (let i = 0; i < MAX_RECONNECT_ATTEMPTS; i++) { const action = policy.fail(); assert.equal(action.kind, "retry", `attempt ${i} should still retry`); } assert.equal(policy.fail().kind, "give-up"); }); test("a success resets the streak", () => { // A long-lived tab reconnects fine at every deploy. It must never accumulate // its way to the give-up ceiling across weeks of successful reconnects. const policy = new ReconnectPolicy(noJitter); for (let deploy = 0; deploy < 50; deploy++) { const action = policy.fail(); assert.equal(action.kind, "retry"); assert.equal(action.kind === "retry" && action.delayMs, 1_000, "always the first-step delay"); policy.succeed(); } assert.equal(policy.attempt, 0); }); test("delays escalate across consecutive failures", () => { const policy = new ReconnectPolicy(noJitter); const delays: number[] = []; for (let i = 0; i < 4; i++) { const action = policy.fail(); if (action.kind === "retry") delays.push(action.delayMs); } assert.deepEqual(delays, [1_000, 2_000, 4_000, 8_000]); }); test("regaining focus or network skips the accumulated wait", () => { // Waiting out a 60s backoff after the laptop opens makes chat look broken for // a minute for a reason that has nothing to do with the server. const policy = new ReconnectPolicy(noJitter); for (let i = 0; i < 5; i++) policy.fail(); assert.ok(policy.attempt > 0); policy.resetForImmediateRetry(); assert.equal(policy.attempt, 0); const action = policy.fail(); assert.equal(action.kind === "retry" && action.delayMs, 1_000); }); test("giving up is sticky until something resets it", () => { const policy = new ReconnectPolicy(noJitter); for (let i = 0; i < MAX_RECONNECT_ATTEMPTS; i++) policy.fail(); assert.equal(policy.fail().kind, "give-up"); assert.equal(policy.fail().kind, "give-up", "still given up"); policy.resetForImmediateRetry(); assert.equal(policy.fail().kind, "retry", "user action can revive it"); });