1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164 | /**
* lib/media, tested for the first time.
*
* Until 3d this file had no `:testlib` export, no `_` seam and zero coverage:
* 445 lines of perfect-negotiation race logic — glare and politeness,
* transceiver reuse, ICE buffering, a per-peer promise-chain mutex — with
* nothing pinning any of it. It was the largest single hole in the suite, and
* every one of those properties is the kind that fails silently in a call
* rather than loudly in a test.
*
* vectors/media-effects.json holds one interleaved transcript per scenario;
* helpers/media-effects.mjs explains what each drives and through which seam.
*/
import test from "node:test";
import assert from "node:assert/strict";
import { installDom } from "./harness/dom.mjs";
import { mods, readVector } from "./helpers/load.mjs";
installDom();
const M = await mods();
const S = await import("./helpers/media-effects.mjs");
const V = readVector("media-effects");
const env = { Media: M.Media, RoomCrypto: M.RoomCrypto };
const count = (t, pred) => t.filter(pred).length;
const isSRD = (e) => e[0] === "pc.setRemoteDescription";
test("glare, polite side: the collision is accepted and answered", async () => {
const got = await S.glare(env, { polite: true });
assert.deepEqual(got.transcript, V.glarePolite.transcript);
assert.equal(got.polite, true);
assert.equal(got.ignoreOffer, false, "the polite side never sets ignoreOffer");
assert.equal(got.setRemoteAfterGlare, 1, "exactly one setRemoteDescription for the colliding offer");
assert.equal(got.signalingState, "stable", "and the negotiation completes");
assert.deepEqual(got.outgoing, [
{ kind: "offer", sdp: "offer-sdp", candidate: null },
{ kind: "answer", sdp: "answer-sdp", candidate: null },
]);
});
test("glare, impolite side: the offer is dropped — no setRemoteDescription at all", async () => {
const got = await S.glare(env, { polite: false });
assert.deepEqual(got.transcript, V.glareImpolite.transcript);
assert.equal(got.polite, false);
assert.equal(got.ignoreOffer, true);
assert.equal(got.setRemoteAfterGlare, 0,
"the offer is ignored, not applied-and-rolled-back: the pc is never told about it");
assert.equal(got.signalingState, "have-local-offer", "our own offer still stands");
// exactly one frame ever left: our offer. No answer, no ICE.
assert.deepEqual(got.outgoing, [{ kind: "offer", sdp: "offer-sdp", candidate: null }]);
});
test("the settingRemoteAnswer window: an impolite peer accepts inside it, drops outside it", async () => {
const got = await S.settingRemoteAnswerWindow(env);
assert.deepEqual(got.transcript, V.settingRemoteAnswerWindow.transcript);
assert.deepEqual(got.inWindow, { settingRemoteAnswer: true, signalingState: "have-local-offer" });
// the SAME peer, the SAME non-stable state, the SAME impoliteness — only the
// flag differs, and it flips the decision
assert.equal(got.acceptedInWindow, 1);
assert.equal(got.ignoreInWindow, false);
assert.equal(got.droppedOutsideWindow, 0);
assert.equal(got.ignoreOutsideWindow, true);
});
test("ICE candidates buffer before the remote description and flush in arrival order", async () => {
const got = await S.candidateBuffering(env);
assert.deepEqual(got.transcript, V.candidateBuffering.transcript);
assert.equal(got.addedEarly, 0, "not one addIceCandidate before a remote description exists");
assert.deepEqual(got.buffered, ["c1", "c2", "c3"]);
// arrival order, then the live ones, then the null end-of-candidates marker
// which lib/media passes as `undefined` (TS `candidate ?? undefined`)
assert.deepEqual(got.flushOrder, ["c1", "c2", "c3", "c4", "undefined"]);
assert.equal(got.pendingAfter, 0, "the buffer is spliced empty, not copied");
});
test("THE MUTEX: two un-awaited signals run strictly one after the other", async () => {
const got = await S.signalMutex(env);
assert.deepEqual(got.transcript, V.signalMutex.transcript);
// While A was suspended inside setRemoteDescription, B had been delivered,
// decrypted and chained — and had still not touched the pc.
assert.deepEqual(got.whileGated, ["A"], "B must not start while A is in flight");
// A's whole sequence, then B's. This is the one assertion that catches
// "someone replaced the peer.queue promise chain with p/let": p/let keeps the
// order WITHIN a handler and destroys it BETWEEN two.
const pcSteps = got.steps.filter(isSRD).map((e) => e[2]);
assert.deepEqual(pcSteps, ["A", "B"]);
const iA = got.steps.findIndex((e) => isSRD(e) && e[2] === "A");
const iB = got.steps.findIndex((e) => isSRD(e) && e[2] === "B");
const answersBetween = got.steps.slice(iA, iB).filter((e) => e[0] === "pc.setLocalDescription").length;
assert.equal(answersBetween, 1, "A finished setLocalDescription BEFORE B touched the pc");
assert.equal(count(got.steps, (e) => e[0] === "pc.setLocalDescription"), 2);
assert.deepEqual(got.outgoing, [
{ kind: "answer", sdp: "answer-sdp", candidate: null },
{ kind: "answer", sdp: "answer-sdp", candidate: null },
]);
});
test("the media-effects vectors are not vacuous", () => {
const clone = (x) => JSON.parse(JSON.stringify(x));
// THE probe the whole scenario exists for: interleave the two signal
// sequences the way a lost mutex would.
const t = clone(V.signalMutex.transcript);
const iA = t.findIndex((e) => isSRD(e) && e[2] === "A");
const iB = t.findIndex((e) => isSRD(e) && e[2] === "B");
assert.ok(iA !== -1 && iB !== -1 && iA < iB);
const [b] = t.splice(iB, 1);
t.splice(iA + 1, 0, b); // B's setRemoteDescription hoisted next to A's
assert.notDeepEqual(t, V.signalMutex.transcript);
// an impolite side that applied the colliding offer instead of ignoring it
const applied = clone(V.glareImpolite.transcript);
applied.push(["pc.setRemoteDescription", "offer", "their-offer"]);
assert.notDeepEqual(applied, V.glareImpolite.transcript);
// a politeness flip
const flipped = clone(V.glarePolite.transcript).filter((e) => !isSRD(e));
assert.notDeepEqual(flipped, V.glarePolite.transcript);
// candidates flushed out of arrival order
const reordered = clone(V.candidateBuffering.transcript);
const i1 = reordered.findIndex((e) => e[0] === "pc.addIceCandidate" && e[1] === "c1");
const i3 = reordered.findIndex((e) => e[0] === "pc.addIceCandidate" && e[1] === "c3");
[reordered[i1], reordered[i3]] = [reordered[i3], reordered[i1]];
assert.notDeepEqual(reordered, V.candidateBuffering.transcript);
// the settingRemoteAnswer window closed one step early
const early = clone(V.settingRemoteAnswerWindow.transcript)
.filter((e) => !(isSRD(e) && e[2] === "their-offer"));
assert.notDeepEqual(early, V.settingRemoteAnswerWindow.transcript);
// ...and the recorded transcripts are real
assert.equal(count(V.signalMutex.transcript, isSRD), 2);
assert.equal(count(V.glareImpolite.transcript, isSRD), 0);
assert.equal(count(V.candidateBuffering.transcript, (e) => e[0] === "pc.addIceCandidate"), 5);
});
test("a first attachMedia applies media ONCE — the duplicate addTrack is gone", async () => {
// This test used to assert the opposite, and the transcripts used to record
// it: attach-media calls setup-pc!, whose tail applied media once the ICE
// config resolved, while attach-media ALSO chained apply-media! onto
// peer.mediaQueue. The two were not serialized against each other, both read
// getTransceivers() before either addTrack landed, and both added the same
// track — the browser rejected the second with InvalidAccessError and
// apply-media!'s .catch swallowed it into a console.warn.
//
// setup-pc!'s tail now goes THROUGH peer.mediaQueue, so the later run sees
// the transceiver the earlier one added and takes the reuse path. Kept as an
// assertion rather than deleted: one addTrack per kind and a silent console
// is the property, and it would regress the moment either call site stopped
// sharing the mutex.
const got = await S.glare(env, { polite: true });
const adds = got.transcript.filter((e) => e[0] === "pc.addTrack");
assert.deepEqual(adds, [
["pc.addTrack", "audio"],
["pc.addTrack", "video"],
]);
assert.equal(count(got.transcript, (e) => e[2] === "InvalidAccessError"), 0,
"no addTrack is rejected any more");
assert.equal(count(got.transcript, (e) => e[0] === "console.warn"), 0,
"and nothing is swallowed into console.warn");
});
|