import { test } from "node:test"; import assert from "node:assert/strict"; import { backoffInterval, formatInterval, recoverInterval, type CadenceParams, } from "./audioCheckCadence"; // Mirrors the production defaults: 60s ceiling, 10s floor, halve, +15s recovery // after 2 clean checkpoints. const P: CadenceParams = { ceilingMs: 60_000, floorMs: 10_000, factor: 0.5, recoverStepMs: 15_000, recoverAfterClean: 2, }; test("backoff halves the interval toward the floor and stops there", () => { assert.equal(backoffInterval(60_000, P), 30_000); assert.equal(backoffInterval(30_000, P), 15_000); // 15000 * 0.5 = 7500, clamped up to the 10000 floor. assert.equal(backoffInterval(15_000, P), 10_000); // Already at the floor: no further shrink. assert.equal(backoffInterval(10_000, P), 10_000); }); test("backoff is a no-op for a degenerate factor (>= 1)", () => { assert.equal(backoffInterval(60_000, { ...P, factor: 1 }), 60_000); assert.equal(backoffInterval(60_000, { ...P, factor: 1.5 }), 60_000); }); test("backoff rounds to whole milliseconds", () => { // 2001 * 0.5 = 1000.5 -> 1001; well above a 200ms floor. assert.equal(backoffInterval(2001, { ...P, floorMs: 200 }), 1001); }); test("recovery needs a clean streak before stepping up", () => { // Start reduced at 15s. First clean: streak 1 < 2, no step, streak carried. let r = recoverInterval(15_000, 0, P); assert.deepEqual(r, { intervalMs: 15_000, cleanStreak: 1 }); // Second clean: streak reaches 2 -> +15s and streak resets. r = recoverInterval(15_000, 1, P); assert.deepEqual(r, { intervalMs: 30_000, cleanStreak: 0 }); }); test("recovery caps at the configured ceiling and is a no-op there", () => { // 50s + 15s step would be 65s, capped to the 60s ceiling. assert.deepEqual(recoverInterval(50_000, 1, P), { intervalMs: 60_000, cleanStreak: 0, }); // Already at the ceiling: streak still counts but interval is unchanged. assert.deepEqual(recoverInterval(60_000, 1, P), { intervalMs: 60_000, cleanStreak: 2, }); }); test("AIMD is asymmetric: fast multiplicative decrease, slow additive increase", () => { // Three malformed checkpoints drive 60 -> 30 -> 15 -> 10 (floor). let interval = 60_000; interval = backoffInterval(interval, P); interval = backoffInterval(interval, P); interval = backoffInterval(interval, P); assert.equal(interval, 10_000); // Recovery from the floor climbs additively, one +15s step per 2 cleans, and // never overshoots the ceiling. let streak = 0; const steps: number[] = []; for (let i = 0; i < 12; i += 1) { const r = recoverInterval(interval, streak, P); interval = r.intervalMs; streak = r.cleanStreak; steps.push(interval); } // 10 -> (clean,clean)25 -> (clean,clean)40 -> 55 -> 60 (capped), then steady. assert.deepEqual(steps, [ 10_000, 25_000, 25_000, 40_000, 40_000, 55_000, 55_000, 60_000, 60_000, 60_000, 60_000, 60_000, ]); }); test("a malformed checkpoint mid-recovery re-tightens immediately", () => { // Recovered partway to 40s, then a malformed check halves straight back down. assert.equal(backoffInterval(40_000, P), 20_000); }); test("test-scale params (300ms interval, 50ms floor) still shrink", () => { const T: CadenceParams = { ceilingMs: 300, floorMs: 50, factor: 0.5, recoverStepMs: 100, recoverAfterClean: 2, }; assert.equal(backoffInterval(300, T), 150); assert.equal(backoffInterval(150, T), 75); // 75 * 0.5 = 37.5, clamped up to the 50ms floor. assert.equal(backoffInterval(75, T), 50); assert.equal(backoffInterval(50, T), 50); }); test("formatInterval prints seconds at/above 1s and milliseconds below", () => { assert.equal(formatInterval(60_000), "60s"); assert.equal(formatInterval(10_000), "10s"); assert.equal(formatInterval(1_000), "1s"); assert.equal(formatInterval(300), "300ms"); assert.equal(formatInterval(50), "50ms"); });