oficina / meet Public

Clone
git clone https://amadan.net/oficina/meet

Plain git — no account needed to clone.

Download

Download this file

1// room-quality.test.mjs — the anonymous quality report and "Report a
2// problem" (spec §15.26, 2026-10-03), gated without a browser.
3//
4// node --test test/browser/room-quality.test.mjs
5//
6// The report's builders are lifted out of the shipped room.js by name,
7// with the constants they read, as room-reconnect.test.mjs does; the
8// client's description and the sheet's answers out of report.js. The
9// fixtures are getStats walks as Chromium writes them, two windows
10// apart, with the fields a report must never carry — candidate
11// addresses and ports — present in them, so their absence from the
12// report is a finding and not an accident of the fixture.
13import { test } from 'node:test';
14import assert from 'node:assert/strict';
15import { readFileSync } from 'node:fs';
16import { fileURLToPath } from 'node:url';
17
18const roomSrc = readFileSync(fileURLToPath(new URL('../../internal/calls/static/room.js', import.meta.url)), 'utf8');
19const reportSrc = readFileSync(fileURLToPath(new URL('../../internal/calls/static/report.js', import.meta.url)), 'utf8');
20
21function lift(src, file, name) {
22 const start = src.indexOf('function ' + name + '(');
23 assert.notEqual(start, -1, name + ' is not in ' + file + ' any more — this test is stale, not passing');
24 let depth = 0;
25 for (let i = src.indexOf('{', start); i < src.length; i++) {
26 if (src[i] === '{') depth += 1;
27 else if (src[i] === '}') {
28 depth -= 1;
29 if (depth === 0) return src.slice(start, i + 1);
30 }
31 }
32 assert.fail('unbalanced braces reading ' + name);
33}
34function liftConst(name) {
35 const m = roomSrc.match(new RegExp('const ' + name + ' = ([0-9.e]+);'));
36 assert.ok(m, name + ' is not a numeric const in room.js any more');
37 return 'const ' + name + ' = ' + m[1] + ';';
38}
39
40const CONSTS = ['QUALITY_MAX_PEERS', 'QUALITY_MAX_SECONDS', 'QUALITY_MAX_MILLIS', 'QUALITY_MAX_COUNT',
41 'QUALITY_MAX_KBPS', 'QUALITY_MAX_PIXELS', 'QUALITY_MAX_FPS', 'QUALITY_LOOK_MAX'].map(liftConst).join('\n');
42const ROOM = ['qualityCeiling', 'qualitySet', 'qualityReading', 'qualityPeer', 'qualityWatchRecord',
43 'qualityWatch', 'qualityTake', 'qualityReport', 'deviceKind'];
44// eslint-disable-next-line no-new-func
45const q = new Function(CONSTS + '\n' + ROOM.map((n) => lift(roomSrc, 'room.js', n)).join('\n') +
46 '; return { ' + ROOM.join(', ') + ' };')();
47const REPORT = ['reportClient', 'problemAnswer', 'problemBody'];
48// eslint-disable-next-line no-new-func
49const r = new Function(REPORT.map((n) => lift(reportSrc, 'report.js', n)).join('\n') +
50 '; return { ' + REPORT.join(', ') + ' };')();
51
52// The bounds must be protocol/quality.go's, or the server refuses what
53// this clamps to.
54test('the bounds are the protocol package\'s', () => {
55 const go = readFileSync(fileURLToPath(new URL('../../protocol/quality.go', import.meta.url)), 'utf8');
56 const bound = (name) => {
57 const m = go.match(new RegExp(name + '\\s*=\\s*([0-9_.e]+)(?:\\s*<<\\s*([0-9]+))?'));
58 assert.ok(m, name + ' is not in protocol/quality.go');
59 return Number(m[1].replace(/_/g, '')) * 2 ** Number(m[2] || 0);
60 };
61 const room = (name) => Number(liftConst(name).match(/= ([0-9.e]+);/)[1]);
62 assert.equal(room('QUALITY_MAX_PEERS'), bound('MaxQualityPeers'));
63 assert.equal(room('QUALITY_MAX_SECONDS'), bound('MaxQualitySeconds'));
64 assert.equal(room('QUALITY_MAX_MILLIS'), bound('MaxQualityMillis'));
65 assert.equal(room('QUALITY_MAX_COUNT'), bound('MaxQualityCount'));
66 assert.equal(room('QUALITY_MAX_KBPS'), bound('MaxQualityKbps'));
67 assert.equal(room('QUALITY_MAX_PIXELS'), bound('MaxQualityPixels'));
68 assert.equal(room('QUALITY_MAX_FPS'), bound('MaxQualityFPS'));
69 assert.equal(room('QUALITY_MAX_BODY'), bound('MaxQualityBody'));
70});
71
72// ── fixtures ─────────────────────────────────────────────────────────
73
74const PEER = 'a1b2c3d4e5f60718293a4b5c6d7e8f90';
75const OTHER = '0f1e2d3c4b5a69788796a5b4c3d2e1f0';
76
77// One connection's walk, Chromium's shape. `t` scales the counters, so
78// window two is window one plus what happened in thirty seconds.
79function walk(t) {
80 const reports = [
81 { id: 'OA', type: 'outbound-rtp', kind: 'audio', mid: '0', ssrc: 11, bytesSent: 10000 + t * 4000, packetsSent: 100 + t * 50 },
82 { id: 'OV', type: 'outbound-rtp', kind: 'video', mid: '1', ssrc: 12, bytesSent: 500000 + t * 100000, packetsSent: 1000 + t * 300,
83 frameWidth: 640, frameHeight: 480, framesPerSecond: 29.97, qualityLimitationReason: t ? 'bandwidth' : 'none' },
84 { id: 'RIA', type: 'remote-inbound-rtp', kind: 'audio', localId: 'OA', ssrc: 11, packetsLost: 2 + t * 3, roundTripTime: 0.0423, jitter: 0.0021 },
85 { id: 'RIV', type: 'remote-inbound-rtp', kind: 'video', localId: 'OV', ssrc: 12, packetsLost: 5 + t * 7, roundTripTime: 0.05, jitter: 0.004 },
86 { id: 'IA', type: 'inbound-rtp', kind: 'audio', mid: '0', ssrc: 21, bytesReceived: 20000 + t * 3750, packetsReceived: 200 + t * 1500,
87 packetsLost: 1 + t * 15, jitter: 0.0031, totalSamplesReceived: 48000 + t * 1440000, concealedSamples: 100 + t * 4800,
88 insertedSamplesForDeceleration: 10 + t * 20, removedSamplesForAcceleration: 5 + t * 30,
89 jitterBufferDelay: 2880 + t * 86400, jitterBufferEmittedCount: 48000 + t * 1440000 },
90 { id: 'IV', type: 'inbound-rtp', kind: 'video', mid: '1', ssrc: 22, bytesReceived: 400000 + t * 3000000, packetsReceived: 900 + t * 3000,
91 packetsLost: 0 + t * 30, jitter: 0.012, freezeCount: 1 + t * 2, totalFreezesDuration: 0.5 + t * 1.25, framesDecoded: 300 + t * 900,
92 framesDropped: 2 + t * 4, frameWidth: 1280, frameHeight: 720, framesPerSecond: 30 },
93 { id: 'T', type: 'transport', selectedCandidatePairId: 'CP' },
94 { id: 'CP', type: 'candidate-pair', localCandidateId: 'L', remoteCandidateId: 'R', state: 'succeeded', nominated: true,
95 currentRoundTripTime: 0.031 },
96 { id: 'L', type: 'local-candidate', candidateType: 'relay', relayProtocol: 'tls', protocol: 'udp', address: '192.0.2.44', port: 54321,
97 ip: '192.0.2.44', url: 'turns:relay.example.net:443' },
98 { id: 'R', type: 'remote-candidate', candidateType: 'srflx', protocol: 'udp', address: '198.51.100.7', port: 61000, ip: '198.51.100.7' },
99 ];
100 if (t) {
101 // A screen arriving in window two only: new streams count from zero.
102 reports.push({ id: 'IS', type: 'inbound-rtp', kind: 'video', mid: '3', ssrc: 23, bytesReceived: 750000, packetsReceived: 800,
103 packetsLost: 4, jitter: 0.02, freezeCount: 3, totalFreezesDuration: 6, framesDecoded: 150, framesDropped: 9,
104 frameWidth: 1920, frameHeight: 1080, framesPerSecond: 5 });
105 }
106 return new Map(reports.map((x) => [x.id, x]));
107}
108const classify = (x) => (x.kind === 'audio' ? 'audio' : x.mid === '3' ? 'screen' : 'video');
109const FORBIDDEN = ['192.0.2.44', '198.51.100.7', '54321', '61000', 'relay.example.net', 'address', 'port', '"ip"', 'url'];
110
111// ── the report ───────────────────────────────────────────────────────
112
113test('two windows: deltas, rates over the window, gauges as they stand', () => {
114 const one = q.qualityReading(walk(0), classify);
115 const two = q.qualityReading(walk(1), classify);
116 const p = q.qualityPeer(one, two, 30);
117
118 assert.deepEqual(p.path, { local: 'relay', remote: 'srflx', protocol: 'tls' });
119 assert.equal(p.rtt, 31);
120
121 assert.deepEqual(p.out.audio, { kbps: 1.1, packets: 50, lost: 3, rtt: 42.3, jitter: 2.1 });
122 assert.deepEqual(p.out.video, { kbps: 26.7, packets: 300, width: 640, height: 480, fps: 30, limit: 'bandwidth', lost: 7, rtt: 50, jitter: 4 });
123
124 assert.deepEqual(p.in.audio, {
125 kbps: 1, packets: 1500, lost: 15, jitter: 3.1, samples: 1440000, concealed: 4800, inserted: 20, removed: 30,
126 // 86,400 s of buffer delay summed over 1,440,000 emitted samples: 60 ms each.
127 jitterBufferMs: 60,
128 });
129 assert.deepEqual(p.in.video, {
130 kbps: 800, packets: 3000, lost: 30, jitter: 12, freezes: 2, freezeSeconds: 1.3, decoded: 900, dropped: 4,
131 width: 1280, height: 720, fps: 30,
132 });
133 // The screen was new in window two: everything it has counted is this
134 // window's.
135 assert.deepEqual(p.in.screen, {
136 kbps: 200, packets: 800, lost: 4, jitter: 20, freezes: 3, freezeSeconds: 6, decoded: 150, dropped: 9,
137 width: 1920, height: 1080, fps: 5,
138 });
139});
140
141test('the first window counts from zero, and a counter that goes back counts nothing', () => {
142 const first = q.qualityPeer(null, q.qualityReading(walk(0), classify), 10);
143 assert.equal(first.out.audio.packets, 100);
144 assert.equal(first.in.video.decoded, 300);
145 const back = q.qualityPeer(q.qualityReading(walk(1), classify), q.qualityReading(walk(0), classify), 30);
146 assert.equal(back.out.audio.packets, 0);
147 assert.equal(back.in.audio.lost, 0);
148});
149
150test('never an address, a port or a URL, whatever the walk carries', () => {
151 const report = q.qualityReport({
152 seconds: 30,
153 client: { app: 'web', browser: 'chrome' },
154 devices: { input: 'bluetooth', output: 'built-in' },
155 peers: [{ id: PEER, prev: q.qualityReading(walk(0), classify), now: q.qualityReading(walk(1), classify) }],
156 });
157 const json = JSON.stringify(report);
158 for (const bad of FORBIDDEN) assert.ok(!json.includes(bad), 'the report carries ' + bad + ': ' + json);
159 assert.equal(report.v, 1);
160 assert.equal(report.seconds, 30);
161 assert.equal(report.peers[0].peer, PEER);
162 assert.equal(report.final, undefined);
163});
164
165test('clamped to the protocol\'s ceilings; NaN, infinite and negative left out', () => {
166 const now = {
167 streams: new Map([
168 ['in:x', { dir: 'in', kind: 'video', counters: { bytesReceived: 1e12, packetsReceived: 5e9, packetsLost: -3, framesDecoded: NaN },
169 gauges: { jitter: -0.5, frameWidth: 99999, frameHeight: Infinity, framesPerSecond: 1000 } }],
170 ['remote:y', { dir: 'remote', kind: 'audio', counters: { packetsLost: 2 }, gauges: { roundTripTime: 1e6, jitter: NaN } }],
171 ]),
172 path: { local: 'host', remote: 'host', protocol: 'udp', rtt: -1 },
173 };
174 const p = q.qualityPeer(null, now, 1);
175 assert.deepEqual(p.in.video, { kbps: 1e6, packets: 1e9, lost: 0, width: 16384, fps: 240 });
176 assert.deepEqual(p.out.audio, { lost: 2, rtt: 600000 });
177 assert.equal(p.rtt, undefined);
178 const report = q.qualityReport({ seconds: 5000, client: { app: 'web' }, peers: [] });
179 assert.equal(report.seconds, 600);
180 assert.equal(report.peers, undefined);
181});
182
183test('an unknown candidate type, an unknown transport, and Firefox\'s selected pair', () => {
184 const ff = new Map([
185 ['P', { id: 'P', type: 'candidate-pair', selected: true, localCandidateId: 'L', remoteCandidateId: 'R' }],
186 ['L', { id: 'L', type: 'local-candidate', candidateType: 'weird', protocol: 'sctp', address: '10.0.0.1' }],
187 ['R', { id: 'R', type: 'remote-candidate', candidateType: 'prflx', protocol: 'tcp' }],
188 ]);
189 const p = q.qualityPeer(null, q.qualityReading(ff, classify), 30);
190 assert.deepEqual(p.path, { local: 'unknown', remote: 'prflx', protocol: 'unknown' });
191 const tcpRelay = new Map([
192 ['T', { id: 'T', type: 'transport', selectedCandidatePairId: 'P' }],
193 ['P', { id: 'P', type: 'candidate-pair', localCandidateId: 'L', remoteCandidateId: 'R' }],
194 ['L', { id: 'L', type: 'local-candidate', candidateType: 'host', protocol: 'tcp' }],
195 ['R', { id: 'R', type: 'remote-candidate', candidateType: 'host' }],
196 ]);
197 assert.equal(q.qualityPeer(null, q.qualityReading(tcpRelay, classify), 30).path.protocol, 'tcp');
198});
199
200test('at most four peers, each a well-formed peer id, none with nothing to say', () => {
201 const now = q.qualityReading(walk(0), classify);
202 const ids = ['1', '2', '3', '4', '5', '6'].map((d) => d.repeat(32));
203 const report = q.qualityReport({
204 seconds: 30,
205 client: { app: 'web' },
206 peers: [
207 { id: 'not a peer id', prev: null, now },
208 { id: OTHER, prev: null, now: { streams: new Map(), path: null } },
209 ...ids.map((id) => ({ id, prev: null, now })),
210 ],
211 });
212 assert.deepEqual(report.peers.map((p) => p.peer), ids.slice(0, 4));
213});
214
215test('the screen ladder and the mending by person, and only what happened', () => {
216 const now = q.qualityReading(walk(1), classify);
217 const report = q.qualityReport({
218 seconds: 30,
219 final: true,
220 client: { app: 'web' },
221 events: { reconnecting: 1, reconnectingSeconds: 4.2 },
222 peers: [{ id: PEER, prev: null, now, ladder: { keyframes: 3, rebuilds: 1, reshares: 0, failures: 0 }, reattaches: 1,
223 events: { cantHearSeconds: 2.5, noSoundFromUs: true } }],
224 });
225 assert.equal(report.final, true);
226 assert.deepEqual(report.events, { reconnecting: 1, reconnectingSeconds: 4.2 });
227 const p = report.peers[0];
228 assert.deepEqual(p.out.screen, { keyframes: 3, rebuilds: 1 });
229 assert.equal(p.in.screen.reattaches, 1);
230 assert.deepEqual(p.events, { cantHearSeconds: 2.5, noSoundFromUs: true });
231});
232
233// ── the notices ──────────────────────────────────────────────────────
234
235test('the notices: counted once each time they appear, timed while they show', () => {
236 const rec = q.qualityWatchRecord();
237 const quiet = { reconnecting: false, cantHear: false };
238 const look = (t, call, peer) => q.qualityWatch(rec, t, { call, audioBlocked: false, peers: new Map([[PEER, peer]]) });
239 // Can't hear us for the first 1.4 s; then a call-wide "Reconnecting…"
240 // from 5 s to 8 s; then one on this person from 10 s to 11 s.
241 for (let t = 0; t <= 15000; t += 200) {
242 look(t, t >= 5000 && t < 8000, {
243 reconnecting: t >= 10000 && t < 11000,
244 cantHear: t < 1400,
245 noSound: t === 12000,
246 });
247 }
248 const got = q.qualityTake(rec);
249 assert.deepEqual(got.call, { reconnecting: 1, reconnectingSeconds: 3 });
250 assert.deepEqual(got.peers.get(PEER), { reconnecting: 1, reconnectingSeconds: 1, cantHearSeconds: 1.4, noSound: true });
251 // A quiet window says nothing.
252 look(15200, false, quiet);
253 look(15400, false, quiet);
254 const next = q.qualityTake(rec);
255 assert.deepEqual(next.call, {});
256 assert.equal(next.peers.size, 0);
257});
258
259test('a suspended tab\'s gap is not a notice someone saw, and someone gone is forgotten', () => {
260 const rec = q.qualityWatchRecord();
261 q.qualityWatch(rec, 0, { call: true, audioBlocked: true, peers: new Map([[PEER, { reconnecting: false, cantHear: true }]]) });
262 q.qualityWatch(rec, 60000, { call: true, audioBlocked: false, peers: new Map() });
263 const got = q.qualityTake(rec);
264 assert.deepEqual(got.call, { reconnecting: 1, reconnectingSeconds: 1, audioBlocked: true });
265 assert.deepEqual(got.peers.get(PEER), { cantHearSeconds: 1 });
266 assert.equal(rec.peers.size, 0);
267});
268
269// ── the devices and the client ───────────────────────────────────────
270
271test('a device\'s kind by its label, which never leaves', () => {
272 const cases = [
273 ['Paul’s AirPods Pro', 'bluetooth'],
274 ['Bose QC35 II', 'bluetooth'],
275 ['Galaxy Buds2 Pro (Bluetooth)', 'bluetooth'],
276 ['MacBook Pro Microphone (Built-in)', 'built-in'],
277 ['Default - MacBook Air Speakers (Built-in)', 'built-in'],
278 ['Microphone Array (Realtek(R) Audio)', 'built-in'],
279 ['Scarlett 2i2 USB', 'usb'],
280 ['USB Audio Device (0d8c:0014)', 'usb'],
281 ['External Headphones', 'wired'],
282 ['BlackHole 2ch (Virtual)', 'virtual'],
283 ['Paul’s iPhone Microphone', 'continuity'],
284 ['Living Room (AirPlay)', 'airplay'],
285 ['Studio Display Microphone', 'unknown'],
286 ['', 'unknown'],
287 [undefined, 'unknown'],
288 ];
289 for (const [label, kind] of cases) assert.equal(q.deviceKind(label), kind, String(label));
290});
291
292test('the client: enums and digits from the user agent, never the string', () => {
293 const MAC_CHROME = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.7204.50 Safari/537.36';
294 const cases = [
295 [MAC_CHROME, { platform: 'macOS', platformVersion: '15.5.0' }, 'dev', 0,
296 { app: 'web', version: 'dev', browser: 'chrome', browserVersion: '138.0', os: 'macos', osVersion: '15.5.0' }],
297 // No hints: macOS's frozen 10_15_7 is not a version.
298 [MAC_CHROME, null, 'de04e2d', 0, { app: 'web', version: 'de04e2d', browser: 'chrome', browserVersion: '138.0', os: 'macos' }],
299 ['Mozilla/5.0 (iPhone; CPU iPhone OS 17_4_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4.1 Mobile/15E148 Safari/604.1',
300 null, '', 5, { app: 'web', browser: 'safari', browserVersion: '17.4', os: 'ios', osVersion: '17.4.1' }],
301 // An iPad asking for the desktop site says Macintosh; its touch points say otherwise.
302 ['Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.2 Safari/605.1.15',
303 null, '', 5, { app: 'web', browser: 'safari', browserVersion: '18.2', os: 'ipados' }],
304 ['Mozilla/5.0 (iPad; CPU OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/120.0.6099.119 Mobile/15E148 Safari/604.1',
305 null, '', 5, { app: 'web', browser: 'chrome', browserVersion: '120.0', os: 'ipados', osVersion: '16.6' }],
306 ['Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36 Edg/138.0.3351.55',
307 { platform: 'Windows', platformVersion: '19.0.0' }, '', 0,
308 { app: 'web', browser: 'edge', browserVersion: '138.0', os: 'windows', osVersion: '19.0.0' }],
309 ['Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:139.0) Gecko/20100101 Firefox/139.0', null, '', 0,
310 { app: 'web', browser: 'firefox', browserVersion: '139.0', os: 'linux' }],
311 ['Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Mobile Safari/537.36', null, '', 5,
312 { app: 'web', browser: 'chrome', browserVersion: '137.0', os: 'android', osVersion: '10' }],
313 ['Mozilla/5.0 (X11; CrOS x86_64 14541.0.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36 OPR/120.0.0.0',
314 null, 'not a version!', 0, { app: 'web', browser: 'other', os: 'chromeos' }],
315 ['', null, '', 0, { app: 'web', browser: 'other', os: 'other' }],
316 ];
317 for (const [ua, hints, version, touch, want] of cases) {
318 const got = r.reportClient(ua, hints, version, touch);
319 assert.deepEqual(got, want, ua);
320 const json = JSON.stringify(got);
321 assert.ok(!json.includes('Mozilla') && !json.includes('AppleWebKit'), json);
322 }
323});
324
325// ── "Report a problem" ───────────────────────────────────────────────
326
327test('the sheet\'s answers', () => {
328 assert.deepEqual(r.problemAnswer(200, { ref: 'K7Q2M9PD' }), { ref: 'K7Q2M9PD', text: '' });
329 assert.equal(r.problemAnswer(200, { ref: '<b>x</b>' }).text, 'The report could not be sent.');
330 assert.equal(r.problemAnswer(429, null).text, 'Too many reports from this call — try again in a minute.');
331 assert.equal(r.problemAnswer(404, {}).text, 'This call can no longer be reported.');
332 assert.equal(r.problemAnswer(403, {}).text, 'The report could not be sent.');
333 assert.equal(r.problemAnswer(0, null).text, 'The report could not be sent.');
334});
335
336test('the problem body fits the server\'s 64 KB, cutting the diagnostics and never the note', () => {
337 const client = { app: 'web' };
338 const small = JSON.parse(r.problemBody('It froze', 'all fine', client, 65000));
339 assert.deepEqual(small, { note: 'It froze', diagnostics: 'all fine', client });
340 const big = r.problemBody('x'.repeat(1200), 'line\n'.repeat(40000), client, 65000);
341 assert.ok(new TextEncoder().encode(big).length <= 65000);
342 const parsed = JSON.parse(big);
343 assert.equal(parsed.note.length, 1000);
344 assert.ok(parsed.diagnostics.endsWith('[cut short to fit]'));
345});
346
347test('the diagnostics a problem report sends name nobody', () => {
348 const names = new Map([[PEER, 'Paul Campbell'], [OTHER, 'bea']]);
349 const pc = { connectionState: 'connected', signalingState: 'stable', iceConnectionState: 'connected' };
350 const pcs = new Map([[PEER, { pc, shareSend: { width: 1280, height: 720, fps: 30, framesEncoded: 9, framesSent: 9, keyFramesEncoded: 1 } },
351 ], [OTHER, { pc, shareSend: null, shareTx: {} }]]);
352 const env = {
353 names, pcs,
354 state: { admitted: true, stopped: false },
355 share: { stream: {}, track: {} },
356 shareMode: () => 'balanced',
357 screenAsks: new Map([[OTHER, { stage: 'keyframes' }]]),
358 shareCounts: { keyframes: 1, rebuilds: 0, reshares: 0, failures: 0 },
359 screenReadings: new Map([[PEER + '#share', { mid: '3', width: 1, height: 1 }]]),
360 readingSummary: () => 'a reading',
361 screenStalled: [PEER],
362 screenReattaches: 0,
363 screenLines: ['10:00:00.000 keyframe for ' + OTHER.slice(0, 6)],
364 SCREEN_LOG_LINES: 200,
365 SHARE_SUFFIX: '#share',
366 navigator: { userAgent: 'Test/1.0' },
367 };
368 // eslint-disable-next-line no-new-func
369 const callDiagnostics = new Function(...Object.keys(env), lift(roomSrc, 'room.js', 'callDiagnostics') +
370 '; return callDiagnostics;')(...Object.values(env));
371 const anonymous = callDiagnostics({ anonymous: true });
372 assert.ok(!/Paul|Campbell|bea\b/.test(anonymous), anonymous);
373 assert.ok(anonymous.includes('peer ' + PEER.slice(0, 6) + ': connection connected'), anonymous);
374 assert.ok(anonymous.includes('to peer ' + OTHER.slice(0, 6)), anonymous);
375 assert.ok(anonymous.includes('recovering peer ' + OTHER.slice(0, 6)), anonymous);
376 assert.ok(anonymous.includes('stalled here: peer ' + PEER.slice(0, 6)), anonymous);
377 // "Copy call diagnostics" keeps the names.
378 const named = callDiagnostics();
379 assert.ok(named.includes('Paul Campbell (' + PEER.slice(0, 6) + ')'), named);
380 assert.ok(named.includes('recovering bea'), named);
381});
382