// room-quality.test.mjs — the anonymous quality report and "Report a // problem" (spec §15.26, 2026-10-03), gated without a browser. // // node --test test/browser/room-quality.test.mjs // // The report's builders are lifted out of the shipped room.js by name, // with the constants they read, as room-reconnect.test.mjs does; the // client's description and the sheet's answers out of report.js. The // fixtures are getStats walks as Chromium writes them, two windows // apart, with the fields a report must never carry — candidate // addresses and ports — present in them, so their absence from the // report is a finding and not an accident of the fixture. import { test } from 'node:test'; import assert from 'node:assert/strict'; import { readFileSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; const roomSrc = readFileSync(fileURLToPath(new URL('../../internal/calls/static/room.js', import.meta.url)), 'utf8'); const reportSrc = readFileSync(fileURLToPath(new URL('../../internal/calls/static/report.js', import.meta.url)), 'utf8'); function lift(src, file, name) { const start = src.indexOf('function ' + name + '('); assert.notEqual(start, -1, name + ' is not in ' + file + ' any more — this test is stale, not passing'); let depth = 0; for (let i = src.indexOf('{', start); i < src.length; i++) { if (src[i] === '{') depth += 1; else if (src[i] === '}') { depth -= 1; if (depth === 0) return src.slice(start, i + 1); } } assert.fail('unbalanced braces reading ' + name); } function liftConst(name) { const m = roomSrc.match(new RegExp('const ' + name + ' = ([0-9.e]+);')); assert.ok(m, name + ' is not a numeric const in room.js any more'); return 'const ' + name + ' = ' + m[1] + ';'; } const CONSTS = ['QUALITY_MAX_PEERS', 'QUALITY_MAX_SECONDS', 'QUALITY_MAX_MILLIS', 'QUALITY_MAX_COUNT', 'QUALITY_MAX_KBPS', 'QUALITY_MAX_PIXELS', 'QUALITY_MAX_FPS', 'QUALITY_LOOK_MAX'].map(liftConst).join('\n'); const ROOM = ['qualityCeiling', 'qualitySet', 'qualityReading', 'qualityPeer', 'qualityWatchRecord', 'qualityWatch', 'qualityTake', 'qualityReport', 'deviceKind']; // eslint-disable-next-line no-new-func const q = new Function(CONSTS + '\n' + ROOM.map((n) => lift(roomSrc, 'room.js', n)).join('\n') + '; return { ' + ROOM.join(', ') + ' };')(); const REPORT = ['reportClient', 'problemAnswer', 'problemBody']; // eslint-disable-next-line no-new-func const r = new Function(REPORT.map((n) => lift(reportSrc, 'report.js', n)).join('\n') + '; return { ' + REPORT.join(', ') + ' };')(); // The bounds must be protocol/quality.go's, or the server refuses what // this clamps to. test('the bounds are the protocol package\'s', () => { const go = readFileSync(fileURLToPath(new URL('../../protocol/quality.go', import.meta.url)), 'utf8'); const bound = (name) => { const m = go.match(new RegExp(name + '\\s*=\\s*([0-9_.e]+)(?:\\s*<<\\s*([0-9]+))?')); assert.ok(m, name + ' is not in protocol/quality.go'); return Number(m[1].replace(/_/g, '')) * 2 ** Number(m[2] || 0); }; const room = (name) => Number(liftConst(name).match(/= ([0-9.e]+);/)[1]); assert.equal(room('QUALITY_MAX_PEERS'), bound('MaxQualityPeers')); assert.equal(room('QUALITY_MAX_SECONDS'), bound('MaxQualitySeconds')); assert.equal(room('QUALITY_MAX_MILLIS'), bound('MaxQualityMillis')); assert.equal(room('QUALITY_MAX_COUNT'), bound('MaxQualityCount')); assert.equal(room('QUALITY_MAX_KBPS'), bound('MaxQualityKbps')); assert.equal(room('QUALITY_MAX_PIXELS'), bound('MaxQualityPixels')); assert.equal(room('QUALITY_MAX_FPS'), bound('MaxQualityFPS')); assert.equal(room('QUALITY_MAX_BODY'), bound('MaxQualityBody')); }); // ── fixtures ───────────────────────────────────────────────────────── const PEER = 'a1b2c3d4e5f60718293a4b5c6d7e8f90'; const OTHER = '0f1e2d3c4b5a69788796a5b4c3d2e1f0'; // One connection's walk, Chromium's shape. `t` scales the counters, so // window two is window one plus what happened in thirty seconds. function walk(t) { const reports = [ { id: 'OA', type: 'outbound-rtp', kind: 'audio', mid: '0', ssrc: 11, bytesSent: 10000 + t * 4000, packetsSent: 100 + t * 50 }, { id: 'OV', type: 'outbound-rtp', kind: 'video', mid: '1', ssrc: 12, bytesSent: 500000 + t * 100000, packetsSent: 1000 + t * 300, frameWidth: 640, frameHeight: 480, framesPerSecond: 29.97, qualityLimitationReason: t ? 'bandwidth' : 'none' }, { id: 'RIA', type: 'remote-inbound-rtp', kind: 'audio', localId: 'OA', ssrc: 11, packetsLost: 2 + t * 3, roundTripTime: 0.0423, jitter: 0.0021 }, { id: 'RIV', type: 'remote-inbound-rtp', kind: 'video', localId: 'OV', ssrc: 12, packetsLost: 5 + t * 7, roundTripTime: 0.05, jitter: 0.004 }, { id: 'IA', type: 'inbound-rtp', kind: 'audio', mid: '0', ssrc: 21, bytesReceived: 20000 + t * 3750, packetsReceived: 200 + t * 1500, packetsLost: 1 + t * 15, jitter: 0.0031, totalSamplesReceived: 48000 + t * 1440000, concealedSamples: 100 + t * 4800, insertedSamplesForDeceleration: 10 + t * 20, removedSamplesForAcceleration: 5 + t * 30, jitterBufferDelay: 2880 + t * 86400, jitterBufferEmittedCount: 48000 + t * 1440000 }, { id: 'IV', type: 'inbound-rtp', kind: 'video', mid: '1', ssrc: 22, bytesReceived: 400000 + t * 3000000, packetsReceived: 900 + t * 3000, packetsLost: 0 + t * 30, jitter: 0.012, freezeCount: 1 + t * 2, totalFreezesDuration: 0.5 + t * 1.25, framesDecoded: 300 + t * 900, framesDropped: 2 + t * 4, frameWidth: 1280, frameHeight: 720, framesPerSecond: 30 }, { id: 'T', type: 'transport', selectedCandidatePairId: 'CP' }, { id: 'CP', type: 'candidate-pair', localCandidateId: 'L', remoteCandidateId: 'R', state: 'succeeded', nominated: true, currentRoundTripTime: 0.031 }, { id: 'L', type: 'local-candidate', candidateType: 'relay', relayProtocol: 'tls', protocol: 'udp', address: '192.0.2.44', port: 54321, ip: '192.0.2.44', url: 'turns:relay.example.net:443' }, { id: 'R', type: 'remote-candidate', candidateType: 'srflx', protocol: 'udp', address: '198.51.100.7', port: 61000, ip: '198.51.100.7' }, ]; if (t) { // A screen arriving in window two only: new streams count from zero. reports.push({ id: 'IS', type: 'inbound-rtp', kind: 'video', mid: '3', ssrc: 23, bytesReceived: 750000, packetsReceived: 800, packetsLost: 4, jitter: 0.02, freezeCount: 3, totalFreezesDuration: 6, framesDecoded: 150, framesDropped: 9, frameWidth: 1920, frameHeight: 1080, framesPerSecond: 5 }); } return new Map(reports.map((x) => [x.id, x])); } const classify = (x) => (x.kind === 'audio' ? 'audio' : x.mid === '3' ? 'screen' : 'video'); const FORBIDDEN = ['192.0.2.44', '198.51.100.7', '54321', '61000', 'relay.example.net', 'address', 'port', '"ip"', 'url']; // ── the report ─────────────────────────────────────────────────────── test('two windows: deltas, rates over the window, gauges as they stand', () => { const one = q.qualityReading(walk(0), classify); const two = q.qualityReading(walk(1), classify); const p = q.qualityPeer(one, two, 30); assert.deepEqual(p.path, { local: 'relay', remote: 'srflx', protocol: 'tls' }); assert.equal(p.rtt, 31); assert.deepEqual(p.out.audio, { kbps: 1.1, packets: 50, lost: 3, rtt: 42.3, jitter: 2.1 }); assert.deepEqual(p.out.video, { kbps: 26.7, packets: 300, width: 640, height: 480, fps: 30, limit: 'bandwidth', lost: 7, rtt: 50, jitter: 4 }); assert.deepEqual(p.in.audio, { kbps: 1, packets: 1500, lost: 15, jitter: 3.1, samples: 1440000, concealed: 4800, inserted: 20, removed: 30, // 86,400 s of buffer delay summed over 1,440,000 emitted samples: 60 ms each. jitterBufferMs: 60, }); assert.deepEqual(p.in.video, { kbps: 800, packets: 3000, lost: 30, jitter: 12, freezes: 2, freezeSeconds: 1.3, decoded: 900, dropped: 4, width: 1280, height: 720, fps: 30, }); // The screen was new in window two: everything it has counted is this // window's. assert.deepEqual(p.in.screen, { kbps: 200, packets: 800, lost: 4, jitter: 20, freezes: 3, freezeSeconds: 6, decoded: 150, dropped: 9, width: 1920, height: 1080, fps: 5, }); }); test('the first window counts from zero, and a counter that goes back counts nothing', () => { const first = q.qualityPeer(null, q.qualityReading(walk(0), classify), 10); assert.equal(first.out.audio.packets, 100); assert.equal(first.in.video.decoded, 300); const back = q.qualityPeer(q.qualityReading(walk(1), classify), q.qualityReading(walk(0), classify), 30); assert.equal(back.out.audio.packets, 0); assert.equal(back.in.audio.lost, 0); }); test('never an address, a port or a URL, whatever the walk carries', () => { const report = q.qualityReport({ seconds: 30, client: { app: 'web', browser: 'chrome' }, devices: { input: 'bluetooth', output: 'built-in' }, peers: [{ id: PEER, prev: q.qualityReading(walk(0), classify), now: q.qualityReading(walk(1), classify) }], }); const json = JSON.stringify(report); for (const bad of FORBIDDEN) assert.ok(!json.includes(bad), 'the report carries ' + bad + ': ' + json); assert.equal(report.v, 1); assert.equal(report.seconds, 30); assert.equal(report.peers[0].peer, PEER); assert.equal(report.final, undefined); }); test('clamped to the protocol\'s ceilings; NaN, infinite and negative left out', () => { const now = { streams: new Map([ ['in:x', { dir: 'in', kind: 'video', counters: { bytesReceived: 1e12, packetsReceived: 5e9, packetsLost: -3, framesDecoded: NaN }, gauges: { jitter: -0.5, frameWidth: 99999, frameHeight: Infinity, framesPerSecond: 1000 } }], ['remote:y', { dir: 'remote', kind: 'audio', counters: { packetsLost: 2 }, gauges: { roundTripTime: 1e6, jitter: NaN } }], ]), path: { local: 'host', remote: 'host', protocol: 'udp', rtt: -1 }, }; const p = q.qualityPeer(null, now, 1); assert.deepEqual(p.in.video, { kbps: 1e6, packets: 1e9, lost: 0, width: 16384, fps: 240 }); assert.deepEqual(p.out.audio, { lost: 2, rtt: 600000 }); assert.equal(p.rtt, undefined); const report = q.qualityReport({ seconds: 5000, client: { app: 'web' }, peers: [] }); assert.equal(report.seconds, 600); assert.equal(report.peers, undefined); }); test('an unknown candidate type, an unknown transport, and Firefox\'s selected pair', () => { const ff = new Map([ ['P', { id: 'P', type: 'candidate-pair', selected: true, localCandidateId: 'L', remoteCandidateId: 'R' }], ['L', { id: 'L', type: 'local-candidate', candidateType: 'weird', protocol: 'sctp', address: '10.0.0.1' }], ['R', { id: 'R', type: 'remote-candidate', candidateType: 'prflx', protocol: 'tcp' }], ]); const p = q.qualityPeer(null, q.qualityReading(ff, classify), 30); assert.deepEqual(p.path, { local: 'unknown', remote: 'prflx', protocol: 'unknown' }); const tcpRelay = new Map([ ['T', { id: 'T', type: 'transport', selectedCandidatePairId: 'P' }], ['P', { id: 'P', type: 'candidate-pair', localCandidateId: 'L', remoteCandidateId: 'R' }], ['L', { id: 'L', type: 'local-candidate', candidateType: 'host', protocol: 'tcp' }], ['R', { id: 'R', type: 'remote-candidate', candidateType: 'host' }], ]); assert.equal(q.qualityPeer(null, q.qualityReading(tcpRelay, classify), 30).path.protocol, 'tcp'); }); test('at most four peers, each a well-formed peer id, none with nothing to say', () => { const now = q.qualityReading(walk(0), classify); const ids = ['1', '2', '3', '4', '5', '6'].map((d) => d.repeat(32)); const report = q.qualityReport({ seconds: 30, client: { app: 'web' }, peers: [ { id: 'not a peer id', prev: null, now }, { id: OTHER, prev: null, now: { streams: new Map(), path: null } }, ...ids.map((id) => ({ id, prev: null, now })), ], }); assert.deepEqual(report.peers.map((p) => p.peer), ids.slice(0, 4)); }); test('the screen ladder and the mending by person, and only what happened', () => { const now = q.qualityReading(walk(1), classify); const report = q.qualityReport({ seconds: 30, final: true, client: { app: 'web' }, events: { reconnecting: 1, reconnectingSeconds: 4.2 }, peers: [{ id: PEER, prev: null, now, ladder: { keyframes: 3, rebuilds: 1, reshares: 0, failures: 0 }, reattaches: 1, events: { cantHearSeconds: 2.5, noSoundFromUs: true } }], }); assert.equal(report.final, true); assert.deepEqual(report.events, { reconnecting: 1, reconnectingSeconds: 4.2 }); const p = report.peers[0]; assert.deepEqual(p.out.screen, { keyframes: 3, rebuilds: 1 }); assert.equal(p.in.screen.reattaches, 1); assert.deepEqual(p.events, { cantHearSeconds: 2.5, noSoundFromUs: true }); }); // ── the notices ────────────────────────────────────────────────────── test('the notices: counted once each time they appear, timed while they show', () => { const rec = q.qualityWatchRecord(); const quiet = { reconnecting: false, cantHear: false }; const look = (t, call, peer) => q.qualityWatch(rec, t, { call, audioBlocked: false, peers: new Map([[PEER, peer]]) }); // Can't hear us for the first 1.4 s; then a call-wide "Reconnecting…" // from 5 s to 8 s; then one on this person from 10 s to 11 s. for (let t = 0; t <= 15000; t += 200) { look(t, t >= 5000 && t < 8000, { reconnecting: t >= 10000 && t < 11000, cantHear: t < 1400, noSound: t === 12000, }); } const got = q.qualityTake(rec); assert.deepEqual(got.call, { reconnecting: 1, reconnectingSeconds: 3 }); assert.deepEqual(got.peers.get(PEER), { reconnecting: 1, reconnectingSeconds: 1, cantHearSeconds: 1.4, noSound: true }); // A quiet window says nothing. look(15200, false, quiet); look(15400, false, quiet); const next = q.qualityTake(rec); assert.deepEqual(next.call, {}); assert.equal(next.peers.size, 0); }); test('a suspended tab\'s gap is not a notice someone saw, and someone gone is forgotten', () => { const rec = q.qualityWatchRecord(); q.qualityWatch(rec, 0, { call: true, audioBlocked: true, peers: new Map([[PEER, { reconnecting: false, cantHear: true }]]) }); q.qualityWatch(rec, 60000, { call: true, audioBlocked: false, peers: new Map() }); const got = q.qualityTake(rec); assert.deepEqual(got.call, { reconnecting: 1, reconnectingSeconds: 1, audioBlocked: true }); assert.deepEqual(got.peers.get(PEER), { cantHearSeconds: 1 }); assert.equal(rec.peers.size, 0); }); // ── the devices and the client ─────────────────────────────────────── test('a device\'s kind by its label, which never leaves', () => { const cases = [ ['Paul’s AirPods Pro', 'bluetooth'], ['Bose QC35 II', 'bluetooth'], ['Galaxy Buds2 Pro (Bluetooth)', 'bluetooth'], ['MacBook Pro Microphone (Built-in)', 'built-in'], ['Default - MacBook Air Speakers (Built-in)', 'built-in'], ['Microphone Array (Realtek(R) Audio)', 'built-in'], ['Scarlett 2i2 USB', 'usb'], ['USB Audio Device (0d8c:0014)', 'usb'], ['External Headphones', 'wired'], ['BlackHole 2ch (Virtual)', 'virtual'], ['Paul’s iPhone Microphone', 'continuity'], ['Living Room (AirPlay)', 'airplay'], ['Studio Display Microphone', 'unknown'], ['', 'unknown'], [undefined, 'unknown'], ]; for (const [label, kind] of cases) assert.equal(q.deviceKind(label), kind, String(label)); }); test('the client: enums and digits from the user agent, never the string', () => { 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'; const cases = [ [MAC_CHROME, { platform: 'macOS', platformVersion: '15.5.0' }, 'dev', 0, { app: 'web', version: 'dev', browser: 'chrome', browserVersion: '138.0', os: 'macos', osVersion: '15.5.0' }], // No hints: macOS's frozen 10_15_7 is not a version. [MAC_CHROME, null, 'de04e2d', 0, { app: 'web', version: 'de04e2d', browser: 'chrome', browserVersion: '138.0', os: 'macos' }], ['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', null, '', 5, { app: 'web', browser: 'safari', browserVersion: '17.4', os: 'ios', osVersion: '17.4.1' }], // An iPad asking for the desktop site says Macintosh; its touch points say otherwise. ['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', null, '', 5, { app: 'web', browser: 'safari', browserVersion: '18.2', os: 'ipados' }], ['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', null, '', 5, { app: 'web', browser: 'chrome', browserVersion: '120.0', os: 'ipados', osVersion: '16.6' }], ['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', { platform: 'Windows', platformVersion: '19.0.0' }, '', 0, { app: 'web', browser: 'edge', browserVersion: '138.0', os: 'windows', osVersion: '19.0.0' }], ['Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:139.0) Gecko/20100101 Firefox/139.0', null, '', 0, { app: 'web', browser: 'firefox', browserVersion: '139.0', os: 'linux' }], ['Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Mobile Safari/537.36', null, '', 5, { app: 'web', browser: 'chrome', browserVersion: '137.0', os: 'android', osVersion: '10' }], ['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', null, 'not a version!', 0, { app: 'web', browser: 'other', os: 'chromeos' }], ['', null, '', 0, { app: 'web', browser: 'other', os: 'other' }], ]; for (const [ua, hints, version, touch, want] of cases) { const got = r.reportClient(ua, hints, version, touch); assert.deepEqual(got, want, ua); const json = JSON.stringify(got); assert.ok(!json.includes('Mozilla') && !json.includes('AppleWebKit'), json); } }); // ── "Report a problem" ─────────────────────────────────────────────── test('the sheet\'s answers', () => { assert.deepEqual(r.problemAnswer(200, { ref: 'K7Q2M9PD' }), { ref: 'K7Q2M9PD', text: '' }); assert.equal(r.problemAnswer(200, { ref: 'x' }).text, 'The report could not be sent.'); assert.equal(r.problemAnswer(429, null).text, 'Too many reports from this call — try again in a minute.'); assert.equal(r.problemAnswer(404, {}).text, 'This call can no longer be reported.'); assert.equal(r.problemAnswer(403, {}).text, 'The report could not be sent.'); assert.equal(r.problemAnswer(0, null).text, 'The report could not be sent.'); }); test('the problem body fits the server\'s 64 KB, cutting the diagnostics and never the note', () => { const client = { app: 'web' }; const small = JSON.parse(r.problemBody('It froze', 'all fine', client, 65000)); assert.deepEqual(small, { note: 'It froze', diagnostics: 'all fine', client }); const big = r.problemBody('x'.repeat(1200), 'line\n'.repeat(40000), client, 65000); assert.ok(new TextEncoder().encode(big).length <= 65000); const parsed = JSON.parse(big); assert.equal(parsed.note.length, 1000); assert.ok(parsed.diagnostics.endsWith('[cut short to fit]')); }); test('the diagnostics a problem report sends name nobody', () => { const names = new Map([[PEER, 'Paul Campbell'], [OTHER, 'bea']]); const pc = { connectionState: 'connected', signalingState: 'stable', iceConnectionState: 'connected' }; const pcs = new Map([[PEER, { pc, shareSend: { width: 1280, height: 720, fps: 30, framesEncoded: 9, framesSent: 9, keyFramesEncoded: 1 } }, ], [OTHER, { pc, shareSend: null, shareTx: {} }]]); const env = { names, pcs, state: { admitted: true, stopped: false }, share: { stream: {}, track: {} }, shareMode: () => 'balanced', screenAsks: new Map([[OTHER, { stage: 'keyframes' }]]), shareCounts: { keyframes: 1, rebuilds: 0, reshares: 0, failures: 0 }, screenReadings: new Map([[PEER + '#share', { mid: '3', width: 1, height: 1 }]]), readingSummary: () => 'a reading', screenStalled: [PEER], screenReattaches: 0, screenLines: ['10:00:00.000 keyframe for ' + OTHER.slice(0, 6)], SCREEN_LOG_LINES: 200, SHARE_SUFFIX: '#share', navigator: { userAgent: 'Test/1.0' }, }; // eslint-disable-next-line no-new-func const callDiagnostics = new Function(...Object.keys(env), lift(roomSrc, 'room.js', 'callDiagnostics') + '; return callDiagnostics;')(...Object.values(env)); const anonymous = callDiagnostics({ anonymous: true }); assert.ok(!/Paul|Campbell|bea\b/.test(anonymous), anonymous); assert.ok(anonymous.includes('peer ' + PEER.slice(0, 6) + ': connection connected'), anonymous); assert.ok(anonymous.includes('to peer ' + OTHER.slice(0, 6)), anonymous); assert.ok(anonymous.includes('recovering peer ' + OTHER.slice(0, 6)), anonymous); assert.ok(anonymous.includes('stalled here: peer ' + PEER.slice(0, 6)), anonymous); // "Copy call diagnostics" keeps the names. const named = callDiagnostics(); assert.ok(named.includes('Paul Campbell (' + PEER.slice(0, 6) + ')'), named); assert.ok(named.includes('recovering bea'), named); });