import { test } from 'node:test'; import assert from 'node:assert/strict'; import { ClockSkewMonitor, clockFixHint, describeSkewVsServer, detectClockFixPlatform, formatSkew, } from './clockSkew'; // A live event received when the local clock is `skew` ms ahead of the server: // origin_server_ts = T (server clock), age = a, localTimestamp = (T + a + skew) - a. const feed = (m: ClockSkewMonitor, skew: number, age = 500, t = 1_700_000_000_000) => m.sample(t, age, t + skew); test('needs three samples, then reports the median with direction', () => { const m = new ClockSkewMonitor(); assert.equal(feed(m, 60_000).skewMs, null); assert.equal(feed(m, 61_000).skewMs, null); const s = feed(m, 59_000); assert.equal(s.skewMs, 60_000); assert.equal(s.warning, true); assert.equal(formatSkew(s.skewMs!), '60 seconds ahead'); }); test('one bad sample cannot trip the warning (median) and hysteresis clears only under 15 s', () => { const m = new ClockSkewMonitor(); feed(m, 1000); feed(m, 1500); assert.equal(feed(m, 90_000).warning, false); // outlier assert.equal(m.getState().skewMs, 1500); const w = new ClockSkewMonitor(); [40_000, 41_000, 39_000, 40_000, 40_000].forEach((s) => feed(w, s)); assert.equal(w.getState().warning, true); // drifting down to 20 s: still >= 15 s → stays on [20_000, 20_000, 20_000, 20_000, 20_000].forEach((s) => feed(w, s)); assert.equal(w.getState().warning, true); [10_000, 10_000, 10_000, 10_000, 10_000].forEach((s) => feed(w, s)); assert.equal(w.getState().warning, false); }); test('stale or missing age is ignored (cache replay must not read as skew)', () => { const m = new ClockSkewMonitor(); const t = 1_700_000_000_000; m.sample(t, undefined, t + 3_600_000); m.sample(t, 40 * 24 * 60 * 60 * 1000, t + 3_600_000); m.sample(t, -5, t); assert.equal(m.getState().skewMs, null); }); test('subscribe fires on change only; reset clears', () => { const m = new ClockSkewMonitor(); const seen: (number | null)[] = []; m.subscribe((s) => seen.push(s.skewMs)); feed(m, -120_000); feed(m, -120_000); feed(m, -120_000); feed(m, -120_000); assert.deepEqual(seen, [-120_000]); assert.equal(formatSkew(-120_000), '2 minutes behind'); m.reset(); assert.deepEqual(seen, [-120_000, null]); }); test('formatSkew picks a sensible unit', () => { assert.equal(formatSkew(45_000), '45 seconds ahead'); assert.equal(formatSkew(-14 * 60_000), '14 minutes behind'); assert.equal(formatSkew(3 * 3_600_000), '3 hours ahead'); assert.equal(formatSkew(2 * 86_400_000), '2 days ahead'); assert.equal(describeSkewVsServer(-3 * 3_600_000), '3 hours behind the server'); assert.equal(describeSkewVsServer(14 * 60_000), '14 minutes ahead of the server'); }); test('platform hint', () => { assert.equal(detectClockFixPlatform('Mozilla/5.0 (Windows NT 10.0; Win64; x64)'), 'windows'); assert.equal( detectClockFixPlatform('Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X)'), 'ios', ); assert.equal(detectClockFixPlatform('Mozilla/5.0 (Macintosh; Intel Mac OS X 14_0)'), 'mac'); assert.match(clockFixHint('windows'), /Sync now/); });