| 1 | // media.test.mjs — the browser gate's own observation helper, gated |
| 2 | // without a browser. |
| 3 | // |
| 4 | // node --test test/browser/media.test.mjs |
| 5 | // |
| 6 | // `observe` is the thing every media assertion in call.mjs now stands |
| 7 | // on, and it is the one piece of the gate that can be wrong in a way |
| 8 | // that makes the gate quieter rather than louder. The helper it |
| 9 | // replaced was two early-return waits in a row — a deadline, not an |
| 10 | // observation — and counters reading 0, 1, 2, 3 and then frozen for |
| 11 | // ever satisfied it, because it stopped looking the moment it saw an |
| 12 | // increase (Codex S4-14). The failure this slice is about is a tile |
| 13 | // that runs for a moment after a renegotiation and then stops, so "it |
| 14 | // moved once" is exactly the answer that must not count. |
| 15 | // |
| 16 | // Lifted out of call.mjs by name, the way the room.js tests lift room.js, |
| 17 | // so what is under test is the shipped helper and not a copy of it. |
| 18 | import { test } from 'node:test'; |
| 19 | import assert from 'node:assert/strict'; |
| 20 | import { readFileSync } from 'node:fs'; |
| 21 | import { fileURLToPath } from 'node:url'; |
| 22 | |
| 23 | const callJS = fileURLToPath(new URL('./call.mjs', import.meta.url)); |
| 24 | const src = readFileSync(callJS, 'utf8'); |
| 25 | |
| 26 | function lift(source, name) { |
| 27 | let start = source.indexOf('function ' + name + '('); |
| 28 | assert.notEqual(start, -1, name + ' is not in call.mjs any more — this test is stale, not passing'); |
| 29 | if (source.slice(start - 6, start) === 'async ') start -= 6; |
| 30 | let depth = 0; |
| 31 | for (let i = source.indexOf('{', start); i < source.length; i++) { |
| 32 | if (source[i] === '{') depth += 1; |
| 33 | else if (source[i] === '}') { |
| 34 | depth -= 1; |
| 35 | if (depth === 0) return source.slice(start, i + 1); |
| 36 | } |
| 37 | } |
| 38 | assert.fail('unbalanced braces reading ' + name + ' out of call.mjs'); |
| 39 | } |
| 40 | |
| 41 | function readNumber(name) { |
| 42 | const m = new RegExp('const ' + name + ' = (\\d+);').exec(src); |
| 43 | assert.notEqual(m, null, name + ' is not a number constant in call.mjs any more'); |
| 44 | return Number(m[1]); |
| 45 | } |
| 46 | |
| 47 | const MEDIA_WINDOWS = readNumber('MEDIA_WINDOWS'); |
| 48 | const MEDIA_WINDOW_MS = readNumber('MEDIA_WINDOW_MS'); |
| 49 | |
| 50 | // eslint-disable-next-line no-new-func |
| 51 | const observe = new Function( |
| 52 | 'MEDIA_WINDOWS', 'MEDIA_WINDOW_MS', 'sleep', |
| 53 | lift(src, 'observe') + '\nreturn observe;', |
| 54 | )(MEDIA_WINDOWS, MEDIA_WINDOW_MS, async () => {}); |
| 55 | |
| 56 | // counters plays a script: one entry per read, each a map of key to |
| 57 | // total. Nothing sleeps — the helper's own waiting is stubbed out, so |
| 58 | // these tests are about what it *requires*, not how long it takes. |
| 59 | function counters(script) { |
| 60 | let at = 0; |
| 61 | return async () => script[Math.min(at++, script.length - 1)]; |
| 62 | } |
| 63 | |
| 64 | test('the shape of an observation is what call.mjs ships', () => { |
| 65 | assert.ok(MEDIA_WINDOWS >= 2, 'one window is a deadline, not an observation'); |
| 66 | assert.ok(MEDIA_WINDOW_MS >= 1000, 'a window shorter than a second says little about video'); |
| 67 | }); |
| 68 | |
| 69 | test('a stream that keeps arriving passes', async () => { |
| 70 | const seen = await observe(counters([{ a: 0 }, { a: 10 }, { a: 20 }, { a: 30 }]), ['a']); |
| 71 | assert.equal(seen.ok, true, JSON.stringify(seen)); |
| 72 | assert.deepEqual(seen.stalled, []); |
| 73 | assert.deepEqual(seen.series, ['a 0->10->20->30']); |
| 74 | }); |
| 75 | |
| 76 | test('a burst and then a freeze does not pass', async () => { |
| 77 | // The control, and Codex's own reproduction: 0, 1, 2, 3 and then 3 |
| 78 | // for ever satisfied the helper this replaced. |
| 79 | const seen = await observe(counters([{ a: 0 }, { a: 1 }, { a: 2 }, { a: 3 }, { a: 3 }, { a: 3 }]), |
| 80 | ['a'], { windows: 5 }); |
| 81 | assert.equal(seen.ok, false, 'a stream that stopped was called healthy'); |
| 82 | assert.deepEqual(seen.stalled, [['a', 4]], 'the window it stopped in is what a reader needs'); |
| 83 | }); |
| 84 | |
| 85 | test('a stream that stops in the last window does not pass', async () => { |
| 86 | const seen = await observe(counters([{ a: 0 }, { a: 10 }, { a: 20 }, { a: 20 }]), ['a']); |
| 87 | assert.equal(seen.ok, false); |
| 88 | assert.deepEqual(seen.stalled, [['a', 3]]); |
| 89 | }); |
| 90 | |
| 91 | test('a stream that stops and starts again does not pass', async () => { |
| 92 | // Growth in every window, not growth overall: a tile that freezes for |
| 93 | // a second and a half in the middle of a call is a tile that froze. |
| 94 | const seen = await observe(counters([{ a: 0 }, { a: 10 }, { a: 10 }, { a: 30 }]), ['a']); |
| 95 | assert.equal(seen.ok, false); |
| 96 | assert.deepEqual(seen.stalled, [['a', 2]]); |
| 97 | }); |
| 98 | |
| 99 | test('every stream is watched, and each is named when it stops', async () => { |
| 100 | const seen = await observe(counters([ |
| 101 | { a: 0, b: 0, c: 0 }, |
| 102 | { a: 1, b: 1, c: 1 }, |
| 103 | { a: 2, b: 1, c: 2 }, |
| 104 | { a: 3, b: 1, c: 2 }, |
| 105 | ]), ['a', 'b', 'c']); |
| 106 | assert.equal(seen.ok, false); |
| 107 | assert.deepEqual(seen.stalled, [['b', 2], ['c', 3]], 'one stream answered for another'); |
| 108 | assert.equal(seen.series.length, 3); |
| 109 | }); |
| 110 | |
| 111 | test('a counter that goes backwards is not growth', async () => { |
| 112 | const seen = await observe(counters([{ a: 30 }, { a: 10 }, { a: 20 }, { a: 30 }]), ['a']); |
| 113 | assert.equal(seen.ok, false); |
| 114 | assert.deepEqual(seen.stalled, [['a', 1]]); |
| 115 | }); |
| 116 | |
| 117 | test('expecting nothing is a failure, not a pass', async () => { |
| 118 | // A check that walks the screens a page says it is showing would |
| 119 | // otherwise pass vacuously for a screen that never arrived, which is |
| 120 | // why the caller names its keys. |
| 121 | const seen = await observe(counters([{}]), []); |
| 122 | assert.equal(seen.ok, false); |
| 123 | assert.equal(seen.series.length, 0); |
| 124 | }); |
| 125 | |
| 126 | test('a stream nobody is sending reads as zero throughout, and fails', async () => { |
| 127 | const seen = await observe(counters([{ a: 5 }, { a: 5 }, { a: 5 }, { a: 5 }]), ['a', 'missing']); |
| 128 | assert.equal(seen.ok, false); |
| 129 | assert.deepEqual(seen.stalled, [['a', 1], ['missing', 1]]); |
| 130 | assert.deepEqual(seen.series, ['a 5->5->5->5', 'missing 0->0->0->0']); |
| 131 | }); |
| 132 | |
| 133 | test('the observation waits the window it says it does', async () => { |
| 134 | const waited = []; |
| 135 | const timed = new Function( |
| 136 | 'MEDIA_WINDOWS', 'MEDIA_WINDOW_MS', 'sleep', |
| 137 | lift(src, 'observe') + '\nreturn observe;', |
| 138 | )(MEDIA_WINDOWS, MEDIA_WINDOW_MS, async (ms) => { waited.push(ms); }); |
| 139 | await timed(counters([{ a: 0 }, { a: 1 }, { a: 2 }, { a: 3 }]), ['a']); |
| 140 | assert.deepEqual(waited, new Array(MEDIA_WINDOWS).fill(MEDIA_WINDOW_MS), |
| 141 | 'the windows are not the duration the constants name'); |
| 142 | }); |
| 143 | |