src/ws/selfHeal.test.ts
v0.3.0 · 3.4 KB
import { describe, expect, it } from 'vitest';
import { createStalenessTracker } from './selfHeal.js';
const S = 1_000;
const TICK = 30 * S;
describe('createStalenessTracker', () => {
it('does not resubscribe during the grace period after track()', () => {
const t = createStalenessTracker();
t.track('u1', 0);
expect(t.tick(TICK)).toEqual([]);
expect(t.tick(3 * TICK)).toEqual([]); // 90 s is not beyond the threshold yet
});
it('resubscribes an address silent > 90 s after 2 stale ticks', () => {
const t = createStalenessTracker();
t.track('u1', 0);
expect(t.tick(91 * S)).toEqual([]); // stale tick 1
expect(t.tick(121 * S)).toEqual(['u1']); // stale tick 2
});
it('backs off exponentially: required ticks 2, 4, 8, ... capped at 20', () => {
const t = createStalenessTracker();
t.track('u1', 0);
const resubTicks: number[] = [];
for (let i = 1; i <= 80; i++) {
if (t.tick(61 * S + i * TICK).length > 0) resubTicks.push(i); // tick 1 at 91 s is already stale
}
const gaps = resubTicks.map((v, i) => v - (resubTicks[i - 1] ?? 0));
expect(gaps.slice(0, 6)).toEqual([2, 4, 8, 16, 20, 20]);
});
it('a fresh message resets both counters; a resubscribe does not refresh lastSeen', () => {
const t = createStalenessTracker();
t.track('u1', 0);
t.tick(91 * S);
expect(t.tick(121 * S)).toEqual(['u1']); // attempts = 1, next needs 4 ticks
expect(t.tick(151 * S)).toEqual([]); // still stale: resubscribe did not count as a message
t.markSeen('u1', 160 * S);
expect(t.tick(181 * S)).toEqual([]);
// silent again: back to 2 ticks because attempts were reset
expect(t.tick(251 * S)).toEqual([]);
expect(t.tick(281 * S)).toEqual(['u1']);
});
it('uses 15 min for confirmed-empty accounts and 90 s for accounts with positions', () => {
const t = createStalenessTracker();
t.track('empty', 0);
t.track('pos', 0);
t.markSeen('empty', 0, false);
t.markSeen('pos', 0, true);
expect(t.tick(100 * S)).toEqual([]);
expect(t.tick(130 * S)).toEqual(['pos']);
expect(t.tick(14 * 60 * S)).toEqual([]);
expect(t.tick(15 * 60 * S + 1)).toEqual([]);
expect(t.tick(15 * 60 * S + TICK)).toEqual(['empty']);
});
it('caps resubscribes per tick at 5', () => {
const t = createStalenessTracker();
for (let i = 0; i < 8; i++) t.track(`u${i}`, 0);
t.tick(91 * S);
expect(t.tick(121 * S)).toHaveLength(5);
expect(t.tick(151 * S)).toEqual(['u5', 'u6', 'u7']);
});
it('skips ineligible keys (unhealthy connection)', () => {
const t = createStalenessTracker({ resubAfterStaleTicks: 1 });
t.track('u1', 0);
expect(t.tick(91 * S, () => false)).toEqual([]);
expect(t.tick(92 * S, () => true)).toEqual(['u1']);
});
it('untrack clears everything; markSeen on unknown keys is ignored', () => {
const t = createStalenessTracker({ resubAfterStaleTicks: 1 });
t.track('u1', 0);
t.untrack('u1');
t.markSeen('u1', 5);
expect(t.tick(200 * S)).toEqual([]);
expect(t.stats(200 * S).tracked).toBe(0);
});
it('freshness stats count only real messages', () => {
const t = createStalenessTracker();
t.track('a', 0);
t.track('b', 0);
t.track('c', 0);
t.markSeen('a', 10 * S, true);
t.markSeen('b', 0, false);
expect(t.stats(10 * S)).toEqual({ tracked: 3, fresh90s: 2, fresh15m: 2, withPositions: 1 });
expect(t.stats(200 * S)).toEqual({ tracked: 3, fresh90s: 0, fresh15m: 2, withPositions: 1 });
});
});