// negotiation.test.mjs — the one piece of room.js that can be gated // without a browser. // // node --test test/browser/negotiation.test.mjs // // room.js is an IIFE that wants a document, so it cannot be imported. // `offerCollision` is a pure function of two arguments, though, and its // source is lifted out of the file by name and evaluated here. That // keeps the thing under test the shipped source — not a copy of it that // can drift — and it fails loudly if the function is renamed or // reshaped rather than passing on a stale duplicate. // // What it gates is spec §5.1's perfect-negotiation decision, exactly as // the W3C writes it: // // readyForOffer = !makingOffer && // (signalingState === "stable" || isSettingRemoteAnswerPending) // offerCollision = !readyForOffer // // The `isSettingRemoteAnswerPending` half was missing (Codex S2-7): an // offer arriving while an answer was still being applied read as glare, // and the impolite side discarded a legitimate offer. import { test } from 'node:test'; import assert from 'node:assert/strict'; import { readFileSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; const roomJS = fileURLToPath(new URL('../../internal/calls/static/room.js', import.meta.url)); // lift returns the source of one top-level-ish `function name(...) {...}` // by matching braces from its declaration. function lift(source, name) { let start = source.indexOf('function ' + name + '('); assert.notEqual(start, -1, name + ' is not in room.js any more — this test is stale, not passing'); // `async` is part of the declaration, and lifting a function that // awaits without it is a syntax error rather than a test. if (source.slice(start - 6, start) === 'async ') start -= 6; let depth = 0; for (let i = source.indexOf('{', start); i < source.length; i++) { if (source[i] === '{') depth += 1; else if (source[i] === '}') { depth -= 1; if (depth === 0) return source.slice(start, i + 1); } } assert.fail('unbalanced braces reading ' + name + ' out of room.js'); } const src = readFileSync(roomJS, 'utf8'); // eslint-disable-next-line no-new-func const offerCollision = new Function(lift(src, 'offerCollision') + '; return offerCollision;')(); const entry = (makingOffer, isSettingRemoteAnswerPending) => ({ makingOffer, isSettingRemoteAnswerPending }); test('a stable connection is ready for an offer', () => { assert.equal(offerCollision(entry(false, false), 'stable'), false); }); test('an offer of our own in flight is glare', () => { assert.equal(offerCollision(entry(true, false), 'stable'), true); assert.equal(offerCollision(entry(true, false), 'have-local-offer'), true); }); test('our own unanswered offer is glare', () => { assert.equal(offerCollision(entry(false, false), 'have-local-offer'), true); }); test('an answer being applied is not glare, whatever the signaling state says', () => { // The regression itself: have-local-offer with an answer half applied // is the state a legitimate follow-up offer arrives in. assert.equal(offerCollision(entry(false, true), 'have-local-offer'), false); assert.equal(offerCollision(entry(false, true), 'stable'), false); }); test('an answer being applied while we are making an offer is still glare', () => { assert.equal(offerCollision(entry(true, true), 'have-local-offer'), true); }); test('a remote offer already applied is glare', () => { assert.equal(offerCollision(entry(false, false), 'have-remote-offer'), true); }); // The decision above is only half of it: the flag has to be *set*, by // the answer handler, around the await that makes the window. So the two // handlers are lifted out too and driven against a fake connection whose // setRemoteDescription can be held open — an answer still being applied // when the next offer arrives, which is the race Codex S2-7 named and // which the six cases above would pass without. const handlers = new Function( 'accept', 'send', 'describe', 'drainCandidates', [lift(src, 'offerCollision'), lift(src, 'onOffer'), lift(src, 'onAnswer'), lift(src, 'answeredExchange')].join('\n') + '\nreturn { onOffer, onAnswer };', ); // answeredExchange is lifted with them because onOffer calls it: an // exchange this side answered is what moves the negotiation generation // and hands the polite side its yield back (Codex S4-4). Its own // behaviour is gated in room-lane.test.mjs, next to `negotiate`. // fakePC is the part of RTCPeerConnection these two handlers touch. The // answer's setRemoteDescription is deliberately slow, and the state it // leaves behind while it is in flight is the real one: signalingState // stays at have-local-offer until the answer is applied. function fakePC() { const applied = []; let release = null; const pc = { signalingState: 'have-local-offer', remoteDescription: null, localDescription: { type: 'answer', sdp: 'local' }, applied, hold: () => new Promise((resolve) => { release = resolve; }), async setRemoteDescription(desc) { applied.push(desc.type); if (desc.type === 'answer') { await new Promise((resolve) => { release = resolve; }); pc.signalingState = 'stable'; } else { pc.signalingState = 'have-remote-offer'; } pc.remoteDescription = desc; }, async setLocalDescription() { pc.signalingState = 'stable'; }, finishAnswer() { release(); }, }; return pc; } test('an offer arriving while an answer is being applied is answered, not discarded', async () => { const pc = fakePC(); const e = { pc, polite: false, // the impolite side: the one that discards on glare makingOffer: false, ignoreOffer: false, isSettingRemoteAnswerPending: false, yields: 0, negotiation: 0, pending: [], descSeq: 0, }; const sent = []; const { onOffer, onAnswer } = handlers( () => e, (to, type) => sent.push(type), (d) => d, async () => {}, ); // The answer goes in and is left half-applied: its promise is still // pending, and signalingState is still have-local-offer. const answering = onAnswer({ from: 'other', seq: 1, payload: { description: { type: 'answer', sdp: 'a' } } }); assert.equal(e.isSettingRemoteAnswerPending, true, 'the answer handler must raise the flag before it awaits'); assert.equal(pc.signalingState, 'have-local-offer'); // And the next offer arrives in exactly that window. await onOffer({ from: 'other', seq: 2, payload: { description: { type: 'offer', sdp: 'o' } } }); assert.equal(e.ignoreOffer, false, 'the impolite side treated a legitimate offer as glare'); assert.ok(pc.applied.includes('offer'), 'the offer was never applied: ' + JSON.stringify(pc.applied)); assert.deepEqual(sent, ['answer'], 'the offer was not answered: ' + JSON.stringify(sent)); pc.finishAnswer(); await answering; assert.equal(e.isSettingRemoteAnswerPending, false, 'the flag must be cleared once the answer is applied'); }); // The recovery ladder, lifted the same way. This one is not about // negotiation at all — it lives here because this file is where room.js // can be run without a browser, and because a browser scenario cannot // see which internal path a client took, only what it did next. // // What it pins is §4.6's budget: a diagnosis that runs out of time must // release its target rather than returning quietly, because a quiet // return leaves the queue in place, the pump restarts it, its expired // head calls `unreachable`, and that opens a reset with a fresh thirty // seconds — recovery restarting the clock it was supposed to be bounded // by (Codex S2-17). const recovery = new Function( 'state', 'probe', 'refused', 'terminal', 'enterAfresh', 'staleTarget', 'sqid', 'SEND_DEADLINE', 'STREAM_BACKOFF_MAX', 'BACKOFF_MIN', [lift(src, 'recoverFromNotFound'), lift(src, 'waitWithin'), lift(src, 'sleep'), lift(src, 'retryAfter')].join('\n') + '\nreturn { recoverFromNotFound };', ); test('a diagnosis that runs out of time releases its target and asks for nothing', async () => { const released = []; const enteredAfresh = []; const terminals = []; const { recoverFromNotFound } = recovery( { stopped: false, peer: 'mypeer', boot: '' }, // A probe that never answers, exactly as the AbortController leaves // it: a rejected promise. async () => { throw new Error('aborted'); }, () => false, (reason) => terminals.push(reason), () => enteredAfresh.push(true), (to, fromReset) => released.push([to, fromReset]), 'room', 30000, 10000, 500, ); // A budget of fifty milliseconds, so the test costs fifty // milliseconds. The path is the same one thirty seconds takes. await recoverFromNotFound('', 'target', { deadline: Date.now() + 50 }); assert.deepEqual(released, [['target', true]], 'the target must be released exactly once, and never with a reset behind it'); assert.deepEqual(enteredAfresh, [], 'nothing here says this participation ended'); assert.deepEqual(terminals, [], 'nor that the call did'); }); // The case above cannot prove the answer handler clears the flag on its // own, because onOffer clears it too (round-3 note). So: an answer with // no offer behind it. test('the answer handler clears its own flag', async () => { const pc = fakePC(); const e = { pc, polite: false, makingOffer: false, ignoreOffer: false, isSettingRemoteAnswerPending: false, yields: 0, negotiation: 0, pending: [], descSeq: 0, }; const { onAnswer } = handlers(() => e, () => {}, (d) => d, async () => {}); const answering = onAnswer({ from: 'other', seq: 1, payload: { description: { type: 'answer', sdp: 'a' } } }); assert.equal(e.isSettingRemoteAnswerPending, true); pc.finishAnswer(); await answering; assert.equal(e.isSettingRemoteAnswerPending, false, 'nothing else was there to clear it'); }); // ── §5.3's renewal proof ────────────────────────────────────────────── // // The other piece of room.js that can be decided without a browser: what // counts as proof that an ICE restart landed. §5.3 step 2 is exact about // it — the exchange completed, `signalingState` stable, and a selected, // succeeded candidate pair whose local candidate carries a *different* // ICE username fragment — because the obvious signal is a false one: a // `completed` transport moves to `connected` merely by starting a // restart, so iceConnectionState proves nothing at all. // // Both functions are lifted together: the decision calls the reader. const proof = new Function( [lift(src, 'renewedUfrag'), lift(src, 'selectedPairUfrag')].join('\n') + '\nreturn { renewedUfrag, selectedPairUfrag };', ); const { renewedUfrag, selectedPairUfrag } = proof(); // report builds a Chromium-shaped getStats answer: a transport naming // the selected pair, that pair, and its local candidate. function report(ufrag, opts) { const o = opts || {}; return [ { type: 'transport', id: 'T', selectedCandidatePairId: o.unselected ? 'other' : 'P' }, { type: 'candidate-pair', id: 'P', state: o.state || 'succeeded', nominated: o.nominated !== false, localCandidateId: 'L', }, { type: 'local-candidate', id: 'L', usernameFragment: ufrag }, { type: 'inbound-rtp', id: 'I', kind: 'video' }, ]; } test('a new username fragment on the selected pair is the proof', () => { assert.equal(renewedUfrag(report('new1'), 'old0', 'stable', true), 'new1'); }); test('the same fragment is the same generation, not a renewal', () => { assert.equal(renewedUfrag(report('old0'), 'old0', 'stable', true), null); }); test('an exchange that has not completed is never proof', () => { // The half that iceConnectionState cannot see: the restart's answer // has not been applied, so whatever the candidate pairs say, the new // generation is not up yet. assert.equal(renewedUfrag(report('new1'), 'old0', 'stable', false), null); }); test('a signaling state that is not stable is never proof', () => { assert.equal(renewedUfrag(report('new1'), 'old0', 'have-local-offer', true), null); }); test('a pair that is not succeeded, or not selected, is not proof', () => { assert.equal(renewedUfrag(report('new1', { state: 'in-progress' }), 'old0', 'stable', true), null); assert.equal(renewedUfrag(report('new1', { nominated: false, unselected: true }), 'old0', 'stable', true), null); }); // Round-1 review, item 2. Chromium keeps the previous generation's pair // in the report for a while after a restart — still succeeded, still // nominated — so a scan for "a succeeded nominated pair" could be // answered by the very connection the restart replaced, and a renewal // would read as proven by its own predecessor. Where the browser names // the selected pair, that answer is the only one. test('the transport names the selected pair, and a stale nominated one does not overrule it', () => { const reports = [ // Listed first, so a scan in report order would find this one. { type: 'candidate-pair', id: 'stale', state: 'succeeded', nominated: true, localCandidateId: 'Lold' }, { type: 'transport', id: 'T', selectedCandidatePairId: 'fresh' }, { type: 'candidate-pair', id: 'fresh', state: 'succeeded', nominated: true, localCandidateId: 'Lnew' }, { type: 'local-candidate', id: 'Lold', usernameFragment: 'old0' }, { type: 'local-candidate', id: 'Lnew', usernameFragment: 'new1' }, ]; assert.equal(selectedPairUfrag(reports), 'new1', 'the stale pair answered for the fresh one'); assert.equal(renewedUfrag(reports, 'old0', 'stable', true), 'new1'); }); test('a transport-selected pair needs no nomination flag of its own', () => { assert.equal(selectedPairUfrag([ { type: 'transport', id: 'T', selectedCandidatePairId: 'P' }, { type: 'candidate-pair', id: 'P', state: 'succeeded', localCandidateId: 'L' }, { type: 'local-candidate', id: 'L', usernameFragment: 'u1' }, ]), 'u1'); }); test('a transport that has selected nothing, or selected a pair that has not succeeded, is not a selection', () => { const pair = (state) => ([ { type: 'transport', id: 'T', selectedCandidatePairId: state ? 'P' : '' }, { type: 'candidate-pair', id: 'P', state: state || 'succeeded', nominated: true, localCandidateId: 'L' }, { type: 'local-candidate', id: 'L', usernameFragment: 'u1' }, ]); assert.equal(selectedPairUfrag(pair('')), null, 'a transport with no selection must not fall back to nominated'); assert.equal(selectedPairUfrag(pair('in-progress')), null); }); test('without a transport report, nomination is the selection', () => { // Firefox's shape: no transport, `selected` or `nominated` on the // pair itself. The fallback has to stay, or the proof never lands // there at all. assert.equal(selectedPairUfrag([ { type: 'candidate-pair', id: 'P', state: 'succeeded', nominated: true, localCandidateId: 'L' }, { type: 'local-candidate', id: 'L', usernameFragment: 'ff1' }, ]), 'ff1'); }); test('the fallback prefers a nominated pair that names a fragment over one that does not', () => { // Two nominated pairs and only the second names its fragment. Taking // the first would answer '' — "a pair, but no generation" — which // renewedUfrag reads as proof for a connection that had nothing // recorded before the restart. The rule is: keep looking. assert.equal(selectedPairUfrag([ { type: 'candidate-pair', id: 'P1', state: 'succeeded', nominated: true, localCandidateId: 'L1' }, { type: 'candidate-pair', id: 'P2', state: 'succeeded', nominated: true, localCandidateId: 'L2' }, { type: 'local-candidate', id: 'L1' }, { type: 'local-candidate', id: 'L2', usernameFragment: 'ff2' }, ]), 'ff2'); }); test('nothing recorded before the restart is not proof of anything', () => { // This used to be the fallback: with no baseline, any succeeded // selected pair counted. But an unchanged pair looks exactly like a // new one when there is nothing to compare it with, so that reading // could clear the warning and stamp a connection as renewed onto a // credential it had never gathered on (Codex S3-3). A rejected // getStats before the restart arrived here the same way. The poller // takes its own baseline from the first reading after the restart // instead — see "the proof takes a baseline after the restart". assert.equal(renewedUfrag(report('new1'), null, 'stable', true), null); assert.equal(renewedUfrag(report(''), null, 'stable', true), null); assert.equal(renewedUfrag(report('new1'), '', 'stable', true), null); }); test('a pair the browser will not name is not evidence', () => { // '' says "there is a pair", not "there is a new one". assert.equal(renewedUfrag(report(''), 'old0', 'stable', true), null); }); test('the reader tells "no such pair" from "a pair with no fragment"', () => { assert.equal(selectedPairUfrag([]), null, 'no pair at all must be null, not empty'); assert.equal(selectedPairUfrag(report('')), '', 'a pair with no fragment is proof of a pair'); assert.equal(selectedPairUfrag(report('u1')), 'u1'); // Firefox marks the pair with `selected` and names no transport. assert.equal(selectedPairUfrag([ { type: 'candidate-pair', id: 'P', state: 'succeeded', selected: true, localCandidateId: 'L' }, { type: 'local-candidate', id: 'L', usernameFragment: 'ff1' }, ]), 'ff1'); }); // And the poller around that decision: it must keep asking until the // report changes, and stop at its own deadline rather than at the first // disappointing answer. // RENEW_POLL is passed in rather than lifted: the shipped half second // between polls is the right interval for a real ICE restart and the // wrong one for a test, and the interval is not what is under test here. const prover = new Function( 'state', 'pcs', 'RENEW_POLL', [lift(src, 'proveRenewed'), lift(src, 'renewedUfrag'), lift(src, 'selectedPairUfrag'), lift(src, 'statsList'), lift(src, 'sleep')].join('\n') + '\nreturn { proveRenewed };', ); // fakeStatsPC answers a Map-shaped report, as RTCStatsReport is, and // counts how many times it was asked. function fakeStatsPC(reports) { let n = 0; return { signalingState: 'stable', asked: () => n, getStats() { const r = reports[Math.min(n, reports.length - 1)]; n += 1; return Promise.resolve(new Map(r.map((x) => [x.id, x]))); }, }; } // bounded runs a poll against a hard cap, so that a proof which stops // at its own deadline is a pass and one that does not is a *failure* // rather than a test run that never ends. The cap's own timer is // unref'd, and `stop` is what ends the runaway poll behind it: an // unbounded loop with a sleep in it holds the event loop open for ever, // so without this the assertion would fail and the process would still // never exit (round-1 review, item 10). function bounded(promise, ms, stop) { return Promise.race([promise, new Promise((resolve) => { const t = setTimeout(() => resolve('NEVER STOPPED'), ms); if (t.unref) t.unref(); })]).finally(stop); } test('the proof polls until the restart lands', async () => { const pc = fakeStatsPC([report('old0'), report('old0'), report('new1')]); const entry = { pc, desc: 1 }; const st = { stopped: false }; const { proveRenewed } = prover(st, new Map([['peer', entry]]), 10); const got = await bounded(proveRenewed('peer', entry, 'old0', 0, Date.now() + 5000), 3000, () => { st.stopped = true; }); assert.equal(got, 'new1'); assert.ok(pc.asked() >= 3, 'it must have kept asking: ' + pc.asked()); }); test('the proof gives up at its deadline, and says so with null', async () => { const pc = fakeStatsPC([report('old0')]); const entry = { pc, desc: 1 }; const st = { stopped: false }; const { proveRenewed } = prover(st, new Map([['peer', entry]]), 10); const got = await bounded(proveRenewed('peer', entry, 'old0', 0, Date.now() + 60), 3000, () => { st.stopped = true; }); assert.equal(got, null, 'a restart that never landed must not read as renewed'); }); test('the proof stops when the connection is replaced under it', async () => { // A `pair`, a roster or an unreachable target rebuilds the // connection: the entry in `pcs` is a different object, and thirty // more seconds of polling the old one prove nothing about either. const pc = fakeStatsPC([report('old0')]); const entry = { pc, desc: 1 }; const pcs = new Map([['peer', { pc: fakeStatsPC([report('new1')]), desc: 9 }]]); const st = { stopped: false }; const { proveRenewed } = prover(st, pcs, 10); const got = await bounded(proveRenewed('peer', entry, 'old0', 0, Date.now() + 30000), 3000, () => { st.stopped = true; }); assert.equal(got, null); assert.equal(pc.asked(), 0, 'it must not even have asked the connection that is gone'); }); // ── §5.3 step 3's watchdog ──────────────────────────────────────────── // // Round-1 review, item 1. The must-succeed-by exists for one path: the // fetch keeps failing, the old credential's deadline passes, and the // connections still on it are owed the sentence. `ice.fetchedAt` only // advances on a *successful* fetch, so on exactly that path it is still // the expiring credential's own stamp — and a watchdog that exempted // "built at or after ice.fetchedAt" exempted every connection in the // call and marked nothing, ever. The stamp is captured before the fetch // and passed in instead. // Through the real marking functions, not a stub for them: the last // round's version passed a spy for `markRelayExpired` and so proved // nothing about what the shipped one does when it gets there — which // was to return without a word, because an earlier success had left // `record.renewed` true for the rest of the call (Codex S3-8). The // sentence a person would read is lifted too, and asserted. const watchdog = new Function( 'state', 'pcs', 'renewals', 'renderTiles', 'relayConfigured', 'ICE_EXPIRED', 'ICE_STUCK', 'FAILED_RELAY', 'FAILED_PLAIN', [lift(src, 'expireUnrenewed'), lift(src, 'markRelayExpired'), lift(src, 'markTrouble'), lift(src, 'renewalRecord'), lift(src, 'tileNote')].join('\n') + '\nreturn { expireUnrenewed, tileNote };', ); const EXPIRED_SENTENCE = 'The relay credential expired for this connection.'; function watching(pcs, renewals) { return watchdog({ stopped: false }, pcs, renewals, () => {}, true, EXPIRED_SENTENCE, 'stuck sentence', 'relay sentence', 'plain sentence'); } test('the must-succeed-by marks the connections that credential was about, and only those', () => { const expiring = 1000; // the fetch stamp of the credential that ran out const pcs = new Map([ ['on-it', { listAt: expiring }], ['older', { listAt: 400 }], ['joined-since', { listAt: expiring + 1 }], ['restarting', { listAt: expiring }], ['proven-onto-the-new-one', { listAt: expiring + 1 }], ]); const renewals = new Map([ ['restarting', { running: true, renewed: false }], ['proven-onto-the-new-one', { running: false, renewed: true }], ]); const { expireUnrenewed } = watching(pcs, renewals); expireUnrenewed(expiring); const marked = Array.from(renewals.entries()) .filter(([, r]) => r.trouble === 'expired').map(([id]) => id); assert.deepEqual(marked.sort(), ['older', 'on-it'], 'a connection built on the expiring credential, with nothing running, is the whole point'); }); test('a renewal in an earlier cycle does not exempt a connection from a later one', () => { // Codex S3-1. `renewed` is a boolean about the past: it goes true on // the first successful renewal and nothing ever puts it back, so a // watchdog that read it exempted that connection from every deadline // for the rest of the call. What matters is which credential the // connection is *on*, and `listAt` is that: it moves only on proof. // // Cycle 1 succeeds, so listAt moves to the first renewal's list. // Cycle 2's fetch then fails, and the credential that expires is that // same list — which this connection is still using. const cycleOne = 2000; const entry = { listAt: cycleOne, failed: false }; const pcs = new Map([['them', entry]]); const renewals = new Map([['them', { running: false, renewed: true, attempts: 1, trouble: '' }]]); const { expireUnrenewed, tileNote } = watching(pcs, renewals); expireUnrenewed(cycleOne); assert.equal(renewals.get('them').trouble, 'expired', 'the second credential expired under a connection that had renewed once, and nobody was told'); assert.equal(tileNote('them', entry), EXPIRED_SENTENCE, 'and the tile has to say so, which is the whole point of marking it'); }); test('a connection already proven onto a newer list is left alone by the deadline', () => { // The other direction, so the fix above cannot be "mark everything": // this one really is on a newer credential. const entry = { listAt: 3000, failed: false }; const pcs = new Map([['them', entry]]); const renewals = new Map([['them', { running: false, renewed: true, attempts: 1, trouble: '' }]]); const { expireUnrenewed, tileNote } = watching(pcs, renewals); expireUnrenewed(2000); assert.equal(renewals.get('them').trouble, ''); assert.equal(tileNote('them', entry), '', 'and its tile says nothing'); }); // ── the roster the fetch came back to (§4.5, §4.6, §5.2) ────────────── // // §5.2 puts one await in front of the first connection this browser // builds, and that await is long enough for the room to change under it. // Two rules keep the call alive across it, and the gate's three ICE // scenarios died for want of both (Task 5's report): // // 1. what is reconciled is the roster held when the fetch *returns*, // not the one captured before it. A superseded roster does not name // the peer that arrived during the fetch, and reconciling it closes // a connection the hub has since named. // 2. a close from `reconcile` does not forget §4.6's counters. `seq` // belongs to the sender's participation, not to a connection, and a // connection closed and rebuilt that starts again at 1 is answered // by a peer whose cursor is already at 3 — which discards it as a // replay, for ever. const rosterPath = new Function( 'state', 'icePrimed', 'iceReady', 'reconcile', 'pendingRoster', 'pendingRosterAt', 'tick', 'console', [lift(src, 'onRoster'), lift(src, 'applyRoster')].join('\n') + '\nreturn { onRoster };', ); test('the roster applied is the newest one, not the one the fetch started with', async () => { const applied = []; let release = null; const fetching = new Promise((resolve) => { release = resolve; }); const { onRoster } = rosterPath( { stopped: false }, false, // icePrimed: the first fetch is still out () => fetching, (roster) => applied.push(roster), null, 0, 0, console, ); onRoster({ roster: { peers: [{ peer: 'a' }] } }); // A `peer` joins ten milliseconds later, and the hub sends the roster // again. Both land while the fetch is out. onRoster({ roster: { peers: [{ peer: 'a' }, { peer: 'b' }] } }); release(); await new Promise((resolve) => setTimeout(resolve, 0)); assert.equal(applied.length, 1, 'a superseded roster must not be applied at all'); assert.deepEqual(applied[0].peers.map((p) => p.peer), ['a', 'b'], 'the roster that names the peer who arrived during the fetch is the one that wins'); }); // reconcile and closePC, lifted together and driven over real Maps: the // forget rule is a property of the pair of them, and stubbing either // would be testing the stub. const reconciler = new Function( 'state', 'stale', 'names', 'pcs', 'gen', 'seqOut', 'lastIn', 'queue', 'frames', 'renewals', 'ensurePC', 'renderTiles', 'renderQueue', 'renderStatus', 'rosters', 'lastRoster', 'waiting', 'mountChat', 'chatEnabled', 'SHARE_SUFFIX', // §15.19's roster half is its own function, gated in room-mute.test.mjs. 'rosterMedia', // §15.20: closePC drops what was heard on the connection it closes. 'hearing', // shareKey comes in as source rather than as a stub: closePC forgets a // peer's share tile through it (§5.4), and a stub would be testing the // stub. [lift(src, 'reconcile'), lift(src, 'closePC'), lift(src, 'shareKey')].join('\n') + '\nreturn { reconcile, closePC };', ); function reconcileOver(pcs, seqOut, lastIn, frames) { const built = []; const chat = []; const { reconcile, closePC } = reconciler( { stopped: false, peer: 'me', kind: 'member' }, new Set(), new Map(), pcs, new Map(), seqOut, lastIn, new Map(), frames || new Map(), new Map(), (id, g) => built.push([id, g]), () => {}, () => {}, () => {}, 0, null, [], () => chat.push('mount'), (on) => chat.push(on), '#share', () => {}, new Map(), ); return { reconcile, closePC, built, chat }; } test('a roster is admission, so the chat panel is mounted and enabled', () => { // §6: the panel appears when the hub says this tab is in the room, and // not before. Mounting is idempotent, so a second roster is one more // setEnabled(true) and nothing else. const { reconcile, chat } = reconcileOver(new Map(), new Map(), new Map()); reconcile({ peers: [{ peer: 'me' }] }, 1); reconcile({ peers: [{ peer: 'me' }] }, 2); assert.deepEqual(chat, ['mount', true, 'mount', true]); }); // A connection, as reconcile sees one: what tick it was built on, and a // close it can report. function connection(bornAt, closed, id) { return { bornAt, pc: { close() { closed.push(id); } } }; } test('a peer the roster stops naming loses its connection and keeps its counters', () => { const closed = []; const pcs = new Map([['peer1', connection(1, closed, 'peer1')]]); const seqOut = new Map([['peer1', 7]]); const lastIn = new Map([['peer1', 3]]); const { reconcile } = reconcileOver(pcs, seqOut, lastIn); reconcile({ peers: [{ peer: 'me' }] }, 2); assert.deepEqual(closed, ['peer1'], 'the connection is still closed — that part was right'); assert.equal(seqOut.get('peer1'), 7, 'seq belongs to the sender participation (§4.6), not to the connection'); assert.equal(lastIn.get('peer1'), 3, 'and so does the replay cursor'); }); test('forgetting a peer forgets its screen\u2019s frame count too', () => { // §5.4: a peer sharing a screen counts frames under two keys — its own // and shareKey(its own) — and the participation that ends takes both // with it. A share count left behind would be inherited by whatever // next drew a tile under that key. const closed = []; const pcs = new Map([['peer1', connection(1, closed, 'peer1')]]); const frames = new Map([['peer1', 30], ['peer1#share', 12], ['peer2', 5]]); const { closePC } = reconcileOver(pcs, new Map(), new Map(), frames); closePC('peer1', true); assert.deepEqual(Array.from(frames.keys()), ['peer2'], 'both of the peer\u2019s tiles are forgotten, and nobody else\u2019s'); }); test('a close that is not a forgetting keeps both frame counts', () => { // The other half of the same rule: a roster's silence closes a // connection without ending a participation (§4.6), and the counts // stand — including the screen's, which is how a gate can still see // that the screen was arriving. const closed = []; const pcs = new Map([['peer1', connection(1, closed, 'peer1')]]); const frames = new Map([['peer1', 30], ['peer1#share', 12]]); const { closePC } = reconcileOver(pcs, new Map(), new Map(), frames); closePC('peer1', false); assert.equal(frames.get('peer1'), 30); assert.equal(frames.get('peer1#share'), 12); }); test('a peer the roster still names keeps its connection and its counters', () => { const closed = []; const pcs = new Map([['peer1', connection(1, closed, 'peer1')]]); const seqOut = new Map([['peer1', 7]]); const lastIn = new Map([['peer1', 3]]); const { reconcile, built } = reconcileOver(pcs, seqOut, lastIn); reconcile({ peers: [{ peer: 'me' }, { peer: 'peer1', gen: 1 }] }, 2); assert.deepEqual(closed, [], 'a present peer must not be closed'); assert.ok(pcs.has('peer1'), 'the connection survives a roster that names it'); assert.deepEqual(built, [['peer1', 1]], 'and goes through the one door into the mesh'); assert.equal(seqOut.get('peer1'), 7); assert.equal(lastIn.get('peer1'), 3); }); // ── the renewal schedule (§5.3 step 4) ──────────────────────────────── // // Two readings of the same schedule, and the earlier of them wins. The // ratio reading is immune to clock skew and wrong at short lives, where // the server's truncation to whole seconds eats the margin; the clock // reading is exact when the clocks agree and skewed when they do not, // but only ever in a direction that renews early. Neither is safe alone. const schedule = new Function( 'ice', 'MIN_RENEW_DELAY', [lift(src, 'renewDelay'), lift(src, 'mustSucceedBy')].join('\n') + '\nreturn { renewDelay, mustSucceedBy };', ); // answered builds what /ice sends for a life in seconds — including the // server's own truncation, which is where the short lives go wrong — // read by a browser whose clock is `skew` ms ahead of the server's. const SERVER_NOW = 1700000000000; // an exact second, as Unix() gives function answered(life, skew) { const secs = SERVER_NOW / 1000; const expiresAt = (secs + life) * 1000; const renewAt = (secs + Math.floor(life * 5 / 6)) * 1000; return { servers: [1], expiresAt, renewAt, margin: Math.max(0, expiresAt - renewAt), fetchedAt: SERVER_NOW + (skew || 0), }; } function scheduleFor(ice) { const { renewDelay, mustSucceedBy } = schedule(ice, 1000); return { delay: renewDelay(), life: mustSucceedBy() - ice.fetchedAt }; } test('the three-hour life schedules where the contract says', () => { // 10800 s: renew at 9000, expire at 10800, and the truncation is a // no-op because both divide. assert.deepEqual(scheduleFor(answered(10800)), { delay: 9000000, life: 10800000 }); }); test('a twelve-second life survives truncation because it divides too', () => { // The gate's own icerenewal TTL: 10 and 12, margin 2, and 2 x 5 = 10. assert.deepEqual(scheduleFor(answered(12)), { delay: 10000, life: 12000 }); }); test('a ten-second life is where the ratio reading alone would renew too late', () => { // renewAt is 8 (8.333 truncated), so the margin reads 2 and the ratio // says 10 — the exact moment the credential dies. The clock reading // says 8, and the earlier of the two is what is used. assert.deepEqual(scheduleFor(answered(10)), { delay: 8000, life: 10000 }); }); test('a two-second life renews at its floor and not before', () => { // renewAt is 1 (1.667 truncated): the clock says 1000 ms, the ratio // says 5000, and the floor is 1000 — so 1000, inside a life of 2000. const got = scheduleFor(answered(2)); assert.deepEqual(got, { delay: 1000, life: 2000 }); assert.ok(got.delay < got.life, 'a renewal scheduled after the expiry is not a renewal'); }); test('a browser running behind the server is capped by the ratio, not stretched by the clock', () => { // 60 s behind: the clock reading says 9060 s, which would renew a // minute after the server expected. The ratio reading caps it. assert.equal(scheduleFor(answered(10800, -60000)).delay, 9000000); }); test('a browser running ahead of the server renews early, which costs nothing', () => { const got = scheduleFor(answered(10800, 60000)); assert.equal(got.delay, 8940000, 'the clock reading is the earlier one here, and early is safe'); assert.equal(got.life, 10740000); }); test('a server that leaves no margin at all cannot turn the schedule into a spin', () => { const ice = { servers: [1], expiresAt: SERVER_NOW + 2000, renewAt: SERVER_NOW, margin: 0, fetchedAt: SERVER_NOW }; assert.equal(scheduleFor(ice).delay, 1000, 'the floor is what stops fetch-schedule-fetch'); }); test('a roster cannot close a connection built after the hub sent it', () => { // The door the "newest roster" rule does not cover, and the one that // actually cost the gate three scenarios: the hub does not re-send a // roster when someone joins — it sends `peer` to the people already // there. So a peer arriving while §5.2's fetch is out builds a // connection, no newer roster ever comes, and the roster waiting on // the fetch does not name it. Closing it there leaves a call with a // participant it will never hear from again. const closed = []; const pcs = new Map([ ['before', connection(1, closed, 'before')], ['during', connection(3, closed, 'during')], ]); const { reconcile } = reconcileOver(pcs, new Map(), new Map()); reconcile({ peers: [{ peer: 'me' }] }, 2); assert.deepEqual(closed, ['before'], 'a connection older than the roster is the only one it can speak for'); assert.ok(pcs.has('during'), 'the connection built during the fetch survives'); }); // ── adopting the list a connection was built without (§5.2) ─────────── // // The first fetch is one await long, and a `peer` can land inside it. It // builds a connection on the floor's empty list, and when the list // arrives that connection is given it. What it is NOT given is an ICE // restart on top of a negotiation already in flight: doing that cost the // `ice` scenario its media in one gate run out of four — both sides // reached `connected`, the remote track arrived, and no frame ever came // down it. const adopter = new Function( 'ice', 'pcs', 'requestReset', [lift(src, 'adoptServers'), lift(src, 'adoptWhenQuiet'), lift(src, 'readyToAdopt')].join('\n') + '\nreturn { adoptServers };', ); function adoptable(state, desc) { const calls = []; return { floor: true, desc: desc || 0, makingOffer: false, listAt: 0, calls, pc: { signalingState: state, setConfiguration: (c) => calls.push(['setConfiguration', c.iceServers.length]), restartIce: () => calls.push(['restartIce']), }, }; } test('a connection that has not started negotiating takes the servers and restarts', () => { const entry = adoptable('stable', 0); const ice = { servers: [1, 2], fetchedAt: 99 }; adopter(ice, new Map([['p', entry]]), () => {}).adoptServers(); assert.deepEqual(entry.calls, [['setConfiguration', 2], ['restartIce']]); assert.equal(entry.floor, false, 'this one really is on the list now'); assert.equal(entry.listAt, 99); }); test('a connection mid-negotiation takes the servers and is left alone', () => { // Only mid-negotiation, now. A connection that is stable and *has* // negotiated is the deferred case landing, and restarting it there is // the whole point of the signalingstatechange listener — see "a quiet // connection on the floor takes the list" below. const entry = adoptable('have-local-offer', 0); const ice = { servers: [1, 2], fetchedAt: 99 }; adopter(ice, new Map([['p', entry]]), () => {}).adoptServers(); assert.deepEqual(entry.calls, [['setConfiguration', 2]], 'an ICE restart across an offer in flight is what broke the media'); assert.equal(entry.floor, true, 'it stays on the floor, and is asked again the moment it is quiet'); }); test('there is nothing to adopt on the floor', () => { const entry = adoptable('stable', 0); adopter({ servers: [], fetchedAt: 99 }, new Map([['p', entry]]), () => {}).adoptServers(); assert.deepEqual(entry.calls, []); }); // ── §4.6's inbound rules, in the order they have to be read ─────────── // // The cursor and the generation both refuse messages, and which is // consulted first decides whether a pair that has just been reset can // ever speak again. A message from a newer generation is measured // against a cursor the *previous* connection built, so calling it a // replay is calling it something it cannot be. const inbound = new Function( 'gen', 'lastIn', 'pcs', 'rebuildPC', lift(src, 'accept') + '\nreturn { accept };', ); function inboundOver(g, cursor) { const rebuilt = []; const pcs = new Map([['them', { id: 'pc' }]]); const gen = new Map([['them', g]]); const lastIn = new Map([['them', cursor]]); const { accept } = inbound(gen, lastIn, pcs, (from, to) => { rebuilt.push([from, to]); gen.set(from, to); }); return { accept, rebuilt, lastIn }; } test('a message at or below the cursor is a replay and is dropped', () => { const { accept } = inboundOver(1, 3); assert.equal(accept({ from: 'them', seq: 3, gen: 1 }), null); }); test('a message above the cursor is taken, and moves it', () => { const { accept, lastIn } = inboundOver(1, 3); assert.ok(accept({ from: 'them', seq: 4, gen: 1 })); assert.equal(lastIn.get('them'), 4); }); test('a message from an older generation is dropped whatever its seq', () => { const { accept } = inboundOver(2, 3); assert.equal(accept({ from: 'them', seq: 99, gen: 1 }), null); }); test('a message from a newer generation is not a replay, whatever the cursor says', () => { // The line under the stale-roster defect: seq 1 from a pair that has // just been reset, against a cursor of 3 from the connection it // replaced. Read cursor-first, this is discarded — and so is // everything after it, because the cursor never moves again. const { accept, rebuilt, lastIn } = inboundOver(1, 3); assert.ok(accept({ from: 'them', seq: 1, gen: 2 }), 'the first message of a rebuilt pair must get through'); assert.deepEqual(rebuilt, [['them', 2]], 'and it rebuilds the pair under the new generation'); assert.equal(lastIn.get('them'), 1, 'the cursor follows the generation that is speaking now'); }); // ── taking the list at the next quiet moment (§5.2) ─────────────────── // // A connection built during the first fetch is on the floor's empty // list. Restarting it there and then cuts across the negotiation it is // in the middle of, which cost a gate run its media; never restarting it // leaves it on host candidates until the 150-minute renewal, which is // most of a call. So the decision is deferred, and asked again at every // quiet moment: createPC's signalingstatechange listener and // adoptServers both come through here. const adopting = new Function( 'ice', 'requestReset', [lift(src, 'adoptWhenQuiet'), lift(src, 'readyToAdopt')].join('\n') + '\nreturn { adoptWhenQuiet, readyToAdopt };', ); function floorEntry(signalingState, opts) { const o = opts || {}; const calls = []; return { floor: o.floor !== false, makingOffer: !!o.makingOffer, listAt: 0, calls, pc: { signalingState, restartIce: () => calls.push('restartIce'), }, }; } function adopt(entry, servers) { const reset = []; const ice = { servers: servers === undefined ? [1, 2] : servers, fetchedAt: 77 }; adopting(ice, (id) => reset.push(id)).adoptWhenQuiet('them', entry); return reset; } test('a quiet connection on the floor takes the list, once', () => { const entry = floorEntry('stable'); adopt(entry); assert.deepEqual(entry.calls, ['restartIce']); assert.equal(entry.floor, false, 'and is no longer on the floor'); assert.equal(entry.listAt, 77, 'and is stamped with the list it took'); // Every later quiet moment asks again; nothing happens twice. adopt(entry); assert.deepEqual(entry.calls, ['restartIce']); }); test('a connection mid-negotiation waits, and is still on the floor', () => { for (const entry of [floorEntry('have-local-offer'), floorEntry('have-remote-offer'), floorEntry('stable', { makingOffer: true })]) { adopt(entry); assert.deepEqual(entry.calls, [], 'a restart across a negotiation in flight is the bug'); assert.equal(entry.floor, true, 'so it stays on the floor and is asked again later'); } }); test('a connection that is not on the floor is never restarted by this', () => { const entry = floorEntry('stable', { floor: false }); adopt(entry); assert.deepEqual(entry.calls, []); }); test('there is no list to take on the floor', () => { const entry = floorEntry('stable'); adopt(entry, []); assert.deepEqual(entry.calls, []); assert.equal(entry.floor, true); }); test('a browser with no restartIce asks the hub for a fresh generation instead', () => { const entry = floorEntry('stable'); entry.pc.restartIce = () => { throw new Error('unsupported'); }; assert.deepEqual(adopt(entry), ['them']); assert.equal(entry.floor, false, 'the pair is being rebuilt; it is not left to try again'); }); // ── asking to receive what we cannot send (§5.1) ────────────────────── // // The recvonly fallback used to be all-or-nothing: a local stream of any // shape at all and nothing asked to receive. `startLocal`'s ladder ends // at audio alone, though — a camera in use, or gone — and that browser // then offered no video m-line, so the person on the other side had a // tile that could never get a frame. Per kind, and an ended track counts // as no track. const media = new Function( 'local', lift(src, 'hasLiveTrack') + '\nreturn { hasLiveTrack };', ); function stream(tracks) { return { getVideoTracks: () => tracks.filter((t) => t.kind === 'video'), getAudioTracks: () => tracks.filter((t) => t.kind === 'audio'), }; } const live = (kind) => ({ kind, readyState: 'live' }); const ended = (kind) => ({ kind, readyState: 'ended' }); test('a stream with both kinds asks to receive neither', () => { const { hasLiveTrack } = media(stream([live('video'), live('audio')])); assert.equal(hasLiveTrack('video'), true); assert.equal(hasLiveTrack('audio'), true); }); test('the audio-only rung of the ladder still asks for video', () => { const { hasLiveTrack } = media(stream([live('audio')])); assert.equal(hasLiveTrack('video'), false, 'this is the m-line that was never offered'); assert.equal(hasLiveTrack('audio'), true); }); test('an ended track is not a track', () => { const { hasLiveTrack } = media(stream([ended('video'), live('audio')])); assert.equal(hasLiveTrack('video'), false); }); test('no local stream asks to receive both', () => { const { hasLiveTrack } = media(null); assert.equal(hasLiveTrack('video'), false); assert.equal(hasLiveTrack('audio'), false); }); // ── the renewal state machine, driven (§5.3 step 2, §15.14) ─────────── // // The pure decisions above are read by a loop, and the loop is where // three of Codex's round-1 findings lived: which connections a fetch is // for, how many attempts a fresh credential buys, and which of the two // sentences the failure earns. So the loop itself is lifted and driven // over fake connections, with the windows shortened to milliseconds. const machine = new Function( 'state', 'pcs', 'renewals', 'ice', 'fetchIce', 'requestReset', 'renderTiles', 'bareRestart', 'RENEW_ATTEMPTS', 'RENEW_WINDOW', 'RENEW_POLL', 'console', [lift(src, 'renewOne'), lift(src, 'renewOutdated'), lift(src, 'retryRenewal'), lift(src, 'followUp'), lift(src, 'renewalRecord'), lift(src, 'markTrouble'), lift(src, 'verdict'), lift(src, 'markRelayExpired'), lift(src, 'proveRenewed'), lift(src, 'renewedUfrag'), lift(src, 'selectedPairUfrag'), lift(src, 'selectedUfrag'), lift(src, 'statsList'), lift(src, 'sleep')].join('\n') + '\nreturn { renewOne, renewOutdated, retryRenewal, proveRenewed };', ); // A connection whose getStats walks a list of fragments, one reading at // a time, and whose restartIce completes an exchange the way a real one // does. Give it ['old0'] and it never moves: the restart does not land. function renewable(ufrags, listAt) { const entry = { desc: 0, listAt: listAt || 0, floor: false, makingOffer: false }; let i = 0; entry.calls = []; entry.pc = { signalingState: 'stable', setConfiguration: (c) => entry.calls.push('setConfiguration:' + ((c.iceServers || []).length)), restartIce: () => { entry.calls.push('restartIce'); entry.desc += 1; }, getStats: () => { const u = ufrags[Math.min(i, ufrags.length - 1)]; i += 1; return Promise.resolve(new Map(report(u).map((r) => [r.id, r]))); }, }; return entry; } // The machine, wired to a mesh and a fetch that behaves as told. function mesh(entries, fetchResult) { const pcs = new Map(Object.entries(entries)); const renewals = new Map(); const ice = { servers: [1, 2], fetchedAt: 1000, expiresAt: 0, renewAt: 0, margin: 0 }; const resets = []; const fetchIce = async () => { if (!fetchResult) return false; ice.fetchedAt += 1000; // a new list, with a stamp of its own return true; }; const api = machine( { stopped: false }, pcs, renewals, ice, fetchIce, (id) => resets.push(id), () => {}, (id) => resets.push('bare:' + id), 3, 60, 10, console, ); return { ...api, pcs, renewals, ice, resets }; } test('a fresh credential buys three restarts and the verdict is stuck, never expired', async () => { // Codex S3-4. A renewal that starts shortly before the old deadline — // after a tab suspension, say — used to spend one attempt of its // three, read the deadline, and report the credential expired. It has // a replacement credential in its hand while it says so. The // credential is fine; the path is not; that is a different sentence // and it is worth all three attempts. const them = renewable(['old0']); const m = mesh({ them }); // The second argument is deliberate and deliberately ignored: an old // deadline, passed the way the removed optional-deadline branch used // to take one. Restore that branch and this test fails on the first // attempt (Codex round 2's note on this test). const ok = await m.renewOne('them', Date.now() - 60000); assert.equal(ok, false); const record = m.renewals.get('them'); assert.equal(record.attempts, 3, 'the full budget, not one attempt cut short by a deadline'); assert.equal(record.trouble, 'stuck'); assert.equal(record.renewed, false); assert.equal(them.listAt, 0, 'and nothing was stamped: this connection is on no new credential'); }); test('a restart that lands is proven, stamped and quiet', async () => { const them = renewable(['old0', 'old0', 'new1']); const m = mesh({ them }); assert.equal(await m.renewOne('them'), true); const record = m.renewals.get('them'); assert.equal(record.renewed, true); assert.equal(record.ufrag, 'new1'); assert.equal(record.trouble, ''); assert.equal(record.attempts, 1, 'one restart was enough'); assert.equal(them.listAt, m.ice.fetchedAt, 'and the connection is stamped onto the list it took'); }); test('a Retry reaches a connection whose own restart was already running', async () => { // Codex S3-9's other half. B is mid-restart when the Retry fetches: // renewOutdated skips it, rightly — cutting across an attempt is what // broke the media in an earlier round — so the debt is paid when that // attempt settles, and B ends up on the new list rather than keeping // the old one until the next schedule. const a = renewable(['a0', 'a0', 'a1']); // B's fragments have to carry two restarts — its own, and the // follow-up onto the list the Retry fetches — and the first has to // take long enough that the Retry really does land inside it. const b = renewable(['b0', 'b0', 'b0', 'b0', 'b1', 'b1', 'b1', 'b2']); const m = mesh({ a, b }, true); const running = m.renewOne('b'); // B is restarting already // Far enough in that B has installed the list it is renewing onto, // and not so far that its proof has already landed: a fetch before // the install is one B takes for itself, and one after the proof // finds B idle — both are different cases, and neither is this. await new Promise((resolve) => setTimeout(resolve, 15)); await m.retryRenewal('a', 'expired'); // and A's tile is clicked await running; await new Promise((resolve) => setTimeout(resolve, 600)); assert.equal(m.renewals.get('a').renewed, true); assert.equal(m.renewals.get('b').renewed, true); assert.equal(b.listAt, m.ice.fetchedAt, 'B is on the list the Retry fetched, not the one it was mid-restart onto'); // And it got there by actually restarting onto it. Without the // follow-up, B's first proof simply stamped whatever the newest list // happened to be, which reads the same and is not the same thing. assert.ok(b.calls.filter((c) => c === 'restartIce').length >= 2, 'B restarted again for the newer list rather than claiming it (' + JSON.stringify(b.calls) + ')'); }); test('a Retry on one tile renews every connection the fetch re-credentialed', async () => { // Codex S3-2. The fetch a Retry makes replaces the credential the // whole call was using and resets the shared schedule with it, so // renewing only the tile that was clicked left every other connection // on a credential about to die with nothing coming to fix it. const a = renewable(['a0', 'a0', 'a1']); const b = renewable(['b0', 'b0', 'b1']); const m = mesh({ a, b }, true); await m.retryRenewal('a', 'expired'); // renewOutdated does not await its renewals — the mesh renews in // parallel, as it does in the page — so wait for both to settle. await new Promise((resolve) => setTimeout(resolve, 400)); assert.equal(m.renewals.get('a').renewed, true, 'the tile that was clicked'); assert.equal(m.renewals.get('b').renewed, true, 'and the one that was not'); assert.equal(b.listAt, m.ice.fetchedAt, 'both are stamped onto the new list'); }); test('a connection already on the new list is not renewed again', async () => { const a = renewable(['a0', 'a0', 'a1'], 1000); const joined = renewable(['j0'], 9999); // built after the fetch const m = mesh({ a, joined }); m.renewOutdated(1000); await new Promise((resolve) => setTimeout(resolve, 400)); assert.equal(m.renewals.get('a').renewed, true); assert.equal(m.renewals.has('joined'), false, 'nothing was asked of it, so it has no record'); assert.deepEqual(joined.calls, [], 'and its connection was not touched'); }); test('the proof takes a baseline after the restart when it had none before', async () => { // Codex S3-3, the other half: with no baseline the poller does not // give up, it takes the first fragment it can read afterwards and // then requires a different one. A connection that moves is still // proven. const them = renewable(['', 'base', 'base', 'moved']); const m = mesh({ them }); them.desc = 1; // the exchange has completed const proven = await m.proveRenewed('them', them, null, 0, Date.now() + 2000); assert.equal(proven, 'moved'); }); test('a connection that never moves is unproven, whatever it started with', async () => { const them = renewable(['', 'stuck0']); const m = mesh({ them }); them.desc = 1; const proven = await m.proveRenewed('them', them, null, 0, Date.now() + 200); assert.equal(proven, null, 'an unchanged pair with no baseline must not read as a new generation'); }); // ── a margin too small to schedule on (W3-12) ───────────────────────── // // The delay has a floor of a second, so a list whose renew time and // expiry are less than a second apart would be fetched about once a // second for as long as the tab stayed open. The server clamps what it // mints, so only the development TTL header can produce one — but a // client that answers a nonsense schedule by hammering its own server // is a client that can be made to do it, so it declines instead. const scheduling = new Function( 'ice', 'MIN_MARGIN', lift(src, 'schedulable') + '\nreturn { schedulable };', ); test('an ordinary list is schedulable', () => { assert.equal(scheduling({ expiresAt: 9, servers: [1], margin: 1800000 }, 1000).schedulable(), true); assert.equal(scheduling({ expiresAt: 9, servers: [1], margin: 1000 }, 1000).schedulable(), true); }); test('a margin under a second is not a schedule', () => { assert.equal(scheduling({ expiresAt: 9, servers: [1], margin: 999 }, 1000).schedulable(), false); assert.equal(scheduling({ expiresAt: 9, servers: [1], margin: 0 }, 1000).schedulable(), false); }); test('the floor has nothing to say about a call with no relay', () => { assert.equal(scheduling({ expiresAt: 0, servers: [], margin: 0 }, 1000).schedulable(), false); }); test('a proof stamps the list it installed, not one that arrived while it was proving', async () => { // Codex S3-9. A restart installs list B and starts proving it. Another // tile's Retry fetches C in the meantime — renewOutdated skips this // connection, because a restart is running — and when B's proof landed // the connection was stamped with C: a credential it had never been // given, whose schedule then governed it. const them = renewable(['old0', 'old0', 'new1', 'new1', 'new1', 'new2']); const m = mesh({ them }, true); const B = m.ice.fetchedAt; // C arrives after the list is installed and while the proof polls. const real = them.pc.getStats.bind(them.pc); let calls = 0; them.pc.getStats = () => { calls += 1; if (calls === 2) m.ice.fetchedAt += 1000; return real(); }; assert.equal(await m.renewOne('them'), true); assert.equal(them.listAt, B, 'the connection is on the list it installed, not on one that merely arrived'); // And the debt that leaves is paid: nothing else was coming for this // connection, because renewOutdated had already passed it by. await new Promise((resolve) => setTimeout(resolve, 600)); assert.equal(them.listAt, m.ice.fetchedAt, 'and it is renewed onto the newer list afterwards'); }); test('a configuration the browser refuses is not a credential the connection has', async () => { // Codex S3-13. setConfiguration throws — an older browser, or one // that will not take the list mid-call — and the connection stays on // the servers it had. Restarting from there re-gathers on *those* // servers, and a new fragment would then have been read as proof and // stamped the connection onto a list it never took. Nothing is // stamped, nothing is restarted, and a rebuild is asked for instead: // a fresh connection takes the current list at construction. const them = renewable(['old0', 'old0', 'new1']); them.pc.setConfiguration = () => { throw new Error('refused'); }; const m = mesh({ them }); assert.equal(await m.renewOne('them'), false); assert.deepEqual(them.calls, [], 'a refused configuration must not be followed by a restart'); assert.equal(them.listAt, 0, 'and nothing may be stamped: this connection took no new list'); const record = m.renewals.get('them'); assert.equal(record.renewed, false); assert.equal(record.trouble, 'stuck'); assert.deepEqual(m.resets, ['them'], 'a rebuild is the only way left to give it these servers'); }); test('a run records the budget it spent, alongside the lifetime count', async () => { // Codex S3-14. `attempts` counts a whole connection's life, so a // follow-up run raises it while the first verdict is still on the // screen: it cannot say how many restarts *this* verdict followed. const them = renewable(['old0']); const m = mesh({ them }); await m.renewOne('them'); const first = m.renewals.get('them'); assert.deepEqual(first.lastRun, { attempts: 3, verdict: 'stuck' }); assert.equal(first.attempts, 3); // A second run: the lifetime count moves on, and the run's own does // not pretend to be it. await m.renewOne('them'); const second = m.renewals.get('them'); assert.deepEqual(second.lastRun, { attempts: 3, verdict: 'stuck' }, 'the run spent three, whatever the lifetime count now reads'); assert.equal(second.attempts, 6, 'and the lifetime count is still there for whoever wants it'); }); test('a proven run says so, and says what it cost', async () => { const them = renewable(['old0', 'old0', 'new1']); const m = mesh({ them }); assert.equal(await m.renewOne('them'), true); assert.deepEqual(m.renewals.get('them').lastRun, { attempts: 1, verdict: 'renewed' }); });