Fox 2026-06-04 directive: every system should be a state machine
with unit + integration + functional test coverage. Implicit-state
defects keep biting (kicked-listener-UI-still-green, two-kick race,
cohost-toggle-kills-phone, audio-wedge-no-recovery). Starting the
formalization with the most-broken-today system: self-listener mode.
Spec (selfListenerSpec):
off ──ENABLE / TOGGLE──▶ on
on ──DISABLE / TOGGLE / UNMUTE / DEMOTED / CLEAR──▶ off
Sits next to publishSpec, subscribeSpec, callSpec, remoteTileSpec
in zebra-spaces.html. Composed by wireZebraMachines() into
roomMachines.selfListener.
UNMUTE edge encodes fox's invariant: "unmuting should seamlessly
switch them back to the now of the conversation webrtc mesh" — if
the user clicks unmute while on, they implicitly drop back to off.
Side effects (mic mute, streamMode enrolment, remoteAudio muting)
move out of enableSelfListenerMode/disableSelfListenerMode (deleted)
into runSelfListenerEnable / runSelfListenerDisable, called by an
observer attached to the FSM. Pure spec stays Node-testable; the
runtime drives the actual audio plumbing from observed transitions.
Boolean selfListenerMode flag deleted. window.selfListenerMode is
now a getter against the FSM state — single source of truth, no
drift possible. All callers (toggle-button click, mute-unmute,
peer-joined, role-demote, leave) now dispatch FSM events instead
of calling helpers directly.
Tests in test/self-listener-fsm.test.js:
- starts in off
- TOGGLE / ENABLE / DISABLE transitions
- UNMUTE drops to off (the fox-invariant)
- UNMUTE / CLEAR while off is no-op
- DEMOTED drops to off
- CLEAR drops to off
- unknown event refuses
- observer fires on real transitions with prev/state
- runtime observer skips prev===state edges
Existing test/zebra-fsm.test.js updated to extract+expose
selfListenerSpec alongside the other specs (the wireZebraMachines
extract is the integration test).
Makefile gets test-self-listener target + slot in test-all.
All test suites green:
- self-listener: 12 / 12
- zebra-fsm: 83 / 83
- mod-actions: 6 / 6
- web-protocol: 3348 / 3348
- multi-peer-mesh: 8 / 8
- video-track-removal: 18 / 18