Viewing is open to everyone. Downloading and starring need a GitHub sign-in. Sign in with GitHub

telemetry-sender.test.mjs

v0.1.0-build.85 Latest Commit ede38f5

Size
5.8 KB
Lines
169
Language
javascript
SHA-256
b2ccb31935d0…
View on GitHub

Sign in with GitHub to download. Each download is recorded with your GitHub username.

1// Pure-JS tests: telemetry sender scheduling against a fake core that
2// mirrors the C core's batch contract (one batch in flight, report once).
3import test from 'node:test';
4import assert from 'node:assert/strict';
5import { TelemetrySender, CKEY_TELEMETRY_ENDPOINT, installBackgroundSend, utcDay } from '../js/telemetry.mjs';
6
7function fakeCore({ due = true } = {}) {
8 const core = {
9 due,
10 nextId: 1,
11 inflight: 0,
12 results: [],
13 takes: [],
14 shouldSend(now) {
15 core.lastNow = now;
16 return core.due && !core.inflight;
17 },
18 takeBatch(day) {
19 if (core.inflight) return null;
20 core.inflight = core.nextId++;
21 core.takes.push(day);
22 return { json: `{"schema":1,"day":"${day}"}`, id: core.inflight };
23 },
24 batchResult(id, status, now) {
25 if (id !== core.inflight) return false;
26 core.results.push({ id, status, now });
27 core.inflight = 0;
28 return true;
29 },
30 };
31 return core;
32}
33
34function fakeTimers() {
35 const t = { intervals: [], timeouts: [] };
36 t.setInterval = (fn, ms) => (t.intervals.push({ fn, ms }), t.intervals.length);
37 t.clearInterval = (id) => (t.intervals[id - 1] = null);
38 t.setTimeout = (fn) => (t.timeouts.push(fn), t.timeouts.length);
39 t.runTimeouts = () => t.timeouts.splice(0).forEach((f) => f());
40 return t;
41}
42
43test('posts one batch with the documented request shape and reports the status', async () => {
44 const core = fakeCore();
45 const calls = [];
46 let clock = 1000;
47 const sender = new TelemetrySender({
48 core,
49 fetch: async (url, init) => (calls.push({ url, init }), { status: 204 }),
50 now: () => clock,
51 day: () => '2026-11-05',
52 timers: fakeTimers(),
53 });
54 const p = sender.trySend();
55 clock = 1500;
56 assert.equal(await p, 204);
57 assert.equal(calls.length, 1);
58 assert.equal(calls[0].url, CKEY_TELEMETRY_ENDPOINT);
59 assert.equal(calls[0].init.method, 'POST');
60 assert.equal(calls[0].init.keepalive, true);
61 assert.deepEqual(calls[0].init.headers, { 'content-type': 'application/json' });
62 assert.equal(calls[0].init.body, '{"schema":1,"day":"2026-11-05"}');
63 assert.equal(calls[0].init.credentials, 'omit');
64 assert.equal(calls[0].init.referrerPolicy, 'no-referrer');
65 assert.deepEqual(core.results, [{ id: 1, status: 204, now: 1500 }]);
66 assert.equal(core.lastNow, 1000);
67});
68
69test('network errors report status 0 and never reject', async () => {
70 const core = fakeCore();
71 const sender = new TelemetrySender({ core, fetch: async () => { throw new TypeError('offline'); }, timers: fakeTimers() });
72 assert.equal(await sender.trySend(), 0);
73 assert.equal(core.results[0].status, 0);
74 const odd = new TelemetrySender({ core, fetch: async () => ({}), timers: fakeTimers() });
75 assert.equal(await odd.trySend(), 0);
76});
77
78test('nothing is sent when the core says no, or without fetch', async () => {
79 const core = fakeCore({ due: false });
80 let n = 0;
81 const sender = new TelemetrySender({ core, fetch: async () => (n++, { status: 204 }), timers: fakeTimers() });
82 assert.equal(await sender.trySend(), null);
83 assert.equal(n, 0);
84 const off = new TelemetrySender({ core: fakeCore(), fetch: null, timers: fakeTimers() });
85 assert.equal(await off.trySend(), null);
86});
87
88test('concurrent attempts share one in-flight request', async () => {
89 const core = fakeCore();
90 let release;
91 let n = 0;
92 const sender = new TelemetrySender({
93 core,
94 fetch: () => (n++, new Promise((r) => (release = () => r({ status: 429 })))),
95 timers: fakeTimers(),
96 });
97 const a = sender.trySend();
98 const b = sender.trySend();
99 assert.equal(a, b);
100 release();
101 assert.equal(await a, 429);
102 assert.equal(n, 1);
103 assert.equal(core.results.length, 1);
104});
105
106test('a stale batch result (mode switched while in flight) is ignored', async () => {
107 const core = fakeCore();
108 const sender = new TelemetrySender({
109 core,
110 fetch: async () => { core.inflight = 0; return { status: 204 }; }, // the core dropped it
111 timers: fakeTimers(),
112 });
113 assert.equal(await sender.trySend(), 204);
114 assert.deepEqual(core.results, []);
115});
116
117test('arm schedules periodic checks once; trySendSoon defers to a later task', async () => {
118 const timers = fakeTimers();
119 const core = fakeCore();
120 let n = 0;
121 const sender = new TelemetrySender({ core, fetch: async () => (n++, { status: 204 }), timers, checkIntervalMs: 60000 });
122 sender.arm();
123 sender.arm();
124 assert.equal(timers.intervals.length, 1);
125 assert.equal(timers.intervals[0].ms, 60000);
126 sender.trySendSoon();
127 assert.equal(n, 0, 'never synchronous');
128 timers.runTimeouts();
129 await sender.inFlight;
130 assert.equal(n, 1);
131 timers.intervals[0].fn();
132 await sender.inFlight;
133 assert.equal(n, 2);
134 sender.disarm();
135 assert.equal(timers.intervals[0], null);
136});
137
138test('throwing core never escapes', async () => {
139 const core = { shouldSend() { throw new Error('boom'); }, takeBatch() {}, batchResult() {} };
140 const sender = new TelemetrySender({ core, fetch: async () => ({ status: 204 }), timers: fakeTimers() });
141 assert.equal(await sender.trySend(), null);
142});
143
144test('background send on visibilitychange hidden and pagehide', async () => {
145 const listeners = {};
146 const target = () => ({
147 addEventListener: (t, f) => (listeners[t] = f),
148 removeEventListener: (t) => delete listeners[t],
149 });
150 const doc = Object.assign(target(), { visibilityState: 'visible' });
151 const win = target();
152 let n = 0;
153 const sender = { trySend: () => (n++, Promise.resolve(null)) };
154 const remove = installBackgroundSend(sender, doc, win);
155 listeners.visibilitychange();
156 assert.equal(n, 0);
157 doc.visibilityState = 'hidden';
158 listeners.visibilitychange();
159 assert.equal(n, 1);
160 listeners.pagehide();
161 assert.equal(n, 2);
162 remove();
163 assert.deepEqual(Object.keys(listeners), []);
164 assert.doesNotThrow(() => installBackgroundSend(sender, undefined, undefined)());
165});
166
167test('utc day', () => {
168 assert.equal(utcDay(new Date('2026-11-05T23:59:59-05:00')), '2026-11-06');
169});