zebra-spaces: wireZebraMachines orchestrator + integration tests (83 green)
Step five — composition layer. wireZebraMachines() returns a coherent room: - one CallFSM - one SubscribeFSM - three PublishFSMs (mic / screen / camera) - lazy Map of RemoteTileFSMs created on first tileFor(kind, pubHex) - tileLeft(pubHex) fans LEFT to every tile keyed by that publisher Observers wire transitions between machines but the orchestrator itself stays pure — no WebRTC, no DOM, no fetch. The page's runtime layers its OWN observers on top to drive real side effects, and the test extracts the orchestrator directly. Cascades modelled: - CallFSM joined (from anything except reconnecting) ── starts the sub - CallFSM reconnecting → joined does NOT re-START (sub stayed alive) - CallFSM leaving / booted ── stops sub AND every live publish - RemoteTileFSMs lazy: tileFor returns the same instance per key - tileLeft sends LEFT to every kind for that pubHex + 11 integration tests + 1 full end-to-end scenario walking through host publishes mic+screen / listener joins late / listener sees the screen / host unshares / mute+prune cycle removes the tile / listener leaves and sub stops. Total: 83 tests passing. The pure-FSM layer + orchestrator are now ready to be wired into the imperative call sites in the live runtime. That's the next step — gradually replace the firefighting code paths (sfuPublishCamera, sfuSubscribe, role transitions) by feeding events into these machines from the existing handlers, then observing state changes to invoke the side effects. Tests catch regressions on the pure layer while the QA loop catches what touches the wire.
This commit is contained in:
parent
a29baeee7e
commit
d9a743a680
2 changed files with 233 additions and 10 deletions
|
|
@ -29,20 +29,21 @@ function extract(re){
|
|||
return src.slice(m.index, j);
|
||||
}
|
||||
|
||||
const createFSMSrc = extract(/function createFSM\(/);
|
||||
const publishSpecSrc = extract(/const publishSpec = /);
|
||||
const subscribeSpecSrc = extract(/const subscribeSpec = /);
|
||||
const remoteTileSpecSrc = extract(/const remoteTileSpec = /);
|
||||
const callSpecSrc = extract(/const callSpec = /);
|
||||
const createFSMSrc = extract(/function createFSM\(/);
|
||||
const publishSpecSrc = extract(/const publishSpec = /);
|
||||
const subscribeSpecSrc = extract(/const subscribeSpec = /);
|
||||
const remoteTileSpecSrc = extract(/const remoteTileSpec = /);
|
||||
const callSpecSrc = extract(/const callSpec = /);
|
||||
const wireMachinesSrc = extract(/function wireZebraMachines\(/);
|
||||
|
||||
/* Function-constructor scope so `const` declarations are visible at the
|
||||
* harness's `return` — they would NOT leak through a bare `eval()`. */
|
||||
const harness = new Function(
|
||||
createFSMSrc + '\n' + publishSpecSrc + '\n' + subscribeSpecSrc + '\n' +
|
||||
remoteTileSpecSrc + '\n' + callSpecSrc +
|
||||
'\nreturn { createFSM, publishSpec, subscribeSpec, remoteTileSpec, callSpec };'
|
||||
remoteTileSpecSrc + '\n' + callSpecSrc + '\n' + wireMachinesSrc +
|
||||
'\nreturn { createFSM, publishSpec, subscribeSpec, remoteTileSpec, callSpec, wireZebraMachines };'
|
||||
);
|
||||
const { createFSM, publishSpec, subscribeSpec, remoteTileSpec, callSpec } = harness();
|
||||
const { createFSM, publishSpec, subscribeSpec, remoteTileSpec, callSpec, wireZebraMachines } = harness();
|
||||
|
||||
let pass = 0, fail = 0;
|
||||
function test(name, fn){
|
||||
|
|
@ -698,5 +699,156 @@ test('leaving + every other event is a no-op', () => {
|
|||
eq(m.state, 'leaving');
|
||||
});
|
||||
|
||||
console.log('\n=== integration: wireZebraMachines ===\n');
|
||||
|
||||
console.log('CallFSM joined → SubscribeFSM auto-starts:');
|
||||
|
||||
test('idle: nothing is running', () => {
|
||||
const w = wireZebraMachines();
|
||||
eq(w.call.state, 'idle');
|
||||
eq(w.sub.state, 'off');
|
||||
eq(w.pubs.mic.state, 'off');
|
||||
});
|
||||
|
||||
test('ENTER → WELCOME: sub goes off → connecting', () => {
|
||||
const w = wireZebraMachines();
|
||||
w.call.send('ENTER', { code: 'r', handle: 'h' });
|
||||
w.call.send('WELCOME', { uuid: 'u', role: 'host' });
|
||||
eq(w.call.state, 'joined');
|
||||
eq(w.sub.state, 'connecting');
|
||||
});
|
||||
|
||||
test('coming back from reconnecting does NOT re-START sub', () => {
|
||||
const w = wireZebraMachines();
|
||||
w.call.send('ENTER', { code: 'r', handle: 'h' });
|
||||
w.call.send('WELCOME', { uuid: 'u', role: 'host' });
|
||||
w.sub.send('CONNECTED', { pc: { id: 'pc' } }); /* sub is now subscribed */
|
||||
w.call.send('WS_DROPPED');
|
||||
w.call.send('WELCOME', { role: 'host' }); /* reconnect */
|
||||
/* sub should still be subscribed — we did NOT send START again */
|
||||
eq(w.sub.state, 'subscribed');
|
||||
});
|
||||
|
||||
console.log('CallFSM leaving / booted → all live publishes get STOP:');
|
||||
|
||||
test('LEAVE while publishing mic → mic transitions to stopping', () => {
|
||||
const w = wireZebraMachines();
|
||||
w.call.send('ENTER', { code: 'r', handle: 'h' });
|
||||
w.call.send('WELCOME', { uuid: 'u', role: 'host' });
|
||||
w.pubs.mic.send('START');
|
||||
w.pubs.mic.send('ACQUIRED', { stream: {} });
|
||||
w.pubs.mic.send('NEGOTIATED', { pc: {}, peerID: 'p' });
|
||||
eq(w.pubs.mic.state, 'live');
|
||||
w.call.send('LEAVE');
|
||||
eq(w.pubs.mic.state, 'stopping');
|
||||
});
|
||||
|
||||
test('LEAVE while screen is mid-acquire → screen cancels into off', () => {
|
||||
const w = wireZebraMachines();
|
||||
w.call.send('ENTER', { code: 'r', handle: 'h' });
|
||||
w.call.send('WELCOME', { uuid: 'u', role: 'host' });
|
||||
w.pubs.screen.send('START');
|
||||
eq(w.pubs.screen.state, 'acquiring');
|
||||
w.call.send('LEAVE');
|
||||
eq(w.pubs.screen.state, 'off');
|
||||
});
|
||||
|
||||
test('BOOTED stops every live publish at once', () => {
|
||||
const w = wireZebraMachines();
|
||||
w.call.send('ENTER', { code: 'r', handle: 'h' });
|
||||
w.call.send('WELCOME', { uuid: 'u', role: 'host' });
|
||||
for (const k of ['mic', 'screen', 'camera']){
|
||||
w.pubs[k].send('START');
|
||||
w.pubs[k].send('ACQUIRED', { stream: {} });
|
||||
w.pubs[k].send('NEGOTIATED', { pc: {}, peerID: k });
|
||||
}
|
||||
w.call.send('BOOTED', { by: 'host' });
|
||||
eq(w.pubs.mic.state, 'stopping');
|
||||
eq(w.pubs.screen.state, 'stopping');
|
||||
eq(w.pubs.camera.state, 'stopping');
|
||||
});
|
||||
|
||||
test('BOOTED stops sub too', () => {
|
||||
const w = wireZebraMachines();
|
||||
w.call.send('ENTER', { code: 'r', handle: 'h' });
|
||||
w.call.send('WELCOME', { uuid: 'u', role: 'host' });
|
||||
w.sub.send('CONNECTED', { pc: {} });
|
||||
w.call.send('BOOTED', { by: 'host' });
|
||||
eq(w.sub.state, 'stopping');
|
||||
});
|
||||
|
||||
console.log('RemoteTileFSMs — tileFor lazy create, tileLeft fans LEFT:');
|
||||
|
||||
test('tileFor returns same FSM for repeat calls (same kind+pubHex)', () => {
|
||||
const w = wireZebraMachines();
|
||||
const a = w.tileFor('camera', 'pub1');
|
||||
const b = w.tileFor('camera', 'pub1');
|
||||
truthy(a === b, 'tileFor is idempotent per key');
|
||||
});
|
||||
|
||||
test('tileFor creates separate FSMs per kind', () => {
|
||||
const w = wireZebraMachines();
|
||||
const cam = w.tileFor('camera', 'pub1');
|
||||
const scr = w.tileFor('screen', 'pub1');
|
||||
truthy(cam !== scr, 'camera ≠ screen for same pubHex');
|
||||
});
|
||||
|
||||
test('tileLeft sends LEFT to every tile keyed by that pubHex (every kind)', () => {
|
||||
const w = wireZebraMachines();
|
||||
const cam = w.tileFor('camera', 'pubA');
|
||||
const scr = w.tileFor('screen', 'pubA');
|
||||
const other = w.tileFor('camera', 'pubB');
|
||||
cam.send('TRACK_ARRIVED', { stream: {} });
|
||||
scr.send('TRACK_ARRIVED', { stream: {} });
|
||||
other.send('TRACK_ARRIVED', { stream: {} });
|
||||
w.tileLeft('pubA');
|
||||
eq(cam.state, 'removed');
|
||||
eq(scr.state, 'removed');
|
||||
eq(other.state, 'receiving'); /* unrelated publisher untouched */
|
||||
});
|
||||
|
||||
console.log('end-to-end scenario: host joins, publishes mic + screen, listener sees them, screen unshares:');
|
||||
|
||||
test('full round trip across the wire', () => {
|
||||
const host = wireZebraMachines();
|
||||
const listener = wireZebraMachines();
|
||||
|
||||
host.call.send('ENTER', { code: 'room-a', handle: 'host' });
|
||||
host.call.send('WELCOME', { uuid: 'host-uuid', role: 'host' });
|
||||
/* host's sub starts connecting (then would CONNECT via signal) */
|
||||
host.sub.send('CONNECTED', { pc: {} });
|
||||
/* host publishes mic */
|
||||
host.pubs.mic.send('START');
|
||||
host.pubs.mic.send('ACQUIRED', { stream: { id: 'mic-stream' } });
|
||||
host.pubs.mic.send('NEGOTIATED', { pc: {}, peerID: 'mic-peer' });
|
||||
/* host publishes screen */
|
||||
host.pubs.screen.send('START');
|
||||
host.pubs.screen.send('ACQUIRED', { stream: { id: 'scr-stream' } });
|
||||
host.pubs.screen.send('NEGOTIATED', { pc: {}, peerID: 'scr-peer' });
|
||||
|
||||
/* listener joins late */
|
||||
listener.call.send('ENTER', { code: 'room-a', handle: 'listener' });
|
||||
listener.call.send('WELCOME', { uuid: 'l-uuid', role: 'listener' });
|
||||
/* their sub connects + receives host's screen track */
|
||||
listener.sub.send('CONNECTED', { pc: {} });
|
||||
const screenTile = listener.tileFor('screen', 'host-pub-hex');
|
||||
screenTile.send('TRACK_ARRIVED', { stream: { id: 'scr-on-listener' } });
|
||||
eq(screenTile.state, 'receiving');
|
||||
|
||||
/* host stops sharing the screen */
|
||||
host.pubs.screen.send('STOP');
|
||||
host.pubs.screen.send('DONE');
|
||||
eq(host.pubs.screen.state, 'off');
|
||||
/* on the listener side the SFU stops the transceiver → track mutes
|
||||
* → debounce expires → PRUNE → removed */
|
||||
screenTile.send('MUTED');
|
||||
screenTile.send('PRUNE');
|
||||
eq(screenTile.state, 'removed');
|
||||
|
||||
/* listener leaves */
|
||||
listener.call.send('LEAVE');
|
||||
eq(listener.sub.state, 'stopping');
|
||||
});
|
||||
|
||||
console.log('\n' + pass + ' passed, ' + fail + ' failed');
|
||||
process.exit(fail === 0 ? 0 : 1);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue