test/zebra-fsm: MuteFSM transition tests — 14 new cases (102 total)

Covers: initial state, TOGGLE, FORCE_MUTE (incl. override-while-off),
AUTO_MUTE, AUTO_UNMUTE, RESTORE_MUTED, RESTORE_UNMUTED, ROLE_PROMOTED,
ctx.source pinning per event, observer notification on every transition,
mod-mute idempotency, and the documented invariant that the FSM itself
does NOT enforce "mod-mute is sticky" — policy lives at the call site.

102/0 passing.
This commit is contained in:
Russell Ballestrini 2026-06-04 14:14:52 -04:00
parent 7f9d8273c9
commit 6972424052
No known key found for this signature in database

View file

@ -902,5 +902,125 @@ test('full round trip across the wire', () => {
eq(listener.sub.state, 'stopping'); eq(listener.sub.state, 'stopping');
}); });
console.log('MuteFSM: transitions, ctx.source tracking, observer correctness:');
test('mute: initial state is on (unmuted UI = mic open)', () => {
const mute = createFSM(muteSpec);
eq(mute.state, 'on');
eq(mute.context.source, null);
});
test('TOGGLE on → off: source = self', () => {
const mute = createFSM(muteSpec);
mute.send('TOGGLE');
eq(mute.state, 'off');
eq(mute.context.source, 'self');
});
test('TOGGLE off → on: clears ctx.source via on.entry', () => {
const mute = createFSM(muteSpec);
mute.send('TOGGLE'); /* off, source=self */
mute.send('TOGGLE'); /* back to on */
eq(mute.state, 'on');
eq(mute.context.source, null);
});
test('FORCE_MUTE on → off: source = mod', () => {
const mute = createFSM(muteSpec);
mute.send('FORCE_MUTE');
eq(mute.state, 'off');
eq(mute.context.source, 'mod');
});
test('FORCE_MUTE while already off overwrites source to mod', () => {
const mute = createFSM(muteSpec);
mute.send('TOGGLE'); /* off, source=self */
mute.send('FORCE_MUTE'); /* still off, mod overrides */
eq(mute.state, 'off');
eq(mute.context.source, 'mod');
});
test('AUTO_MUTE on → off: source = self-listener', () => {
const mute = createFSM(muteSpec);
mute.send('AUTO_MUTE');
eq(mute.state, 'off');
eq(mute.context.source, 'self-listener');
});
test('AUTO_UNMUTE off → on regardless of source', () => {
const mute = createFSM(muteSpec);
mute.send('AUTO_MUTE'); /* off, source=self-listener */
mute.send('AUTO_UNMUTE'); /* back to on */
eq(mute.state, 'on');
eq(mute.context.source, null);
});
test('RESTORE_MUTED off → off: source pinned to self (sessionStorage restore)', () => {
const mute = createFSM(muteSpec);
mute.send('RESTORE_MUTED');
eq(mute.state, 'off');
eq(mute.context.source, 'self');
});
test('RESTORE_UNMUTED on → on: idempotent fresh-session', () => {
const mute = createFSM(muteSpec);
mute.send('RESTORE_UNMUTED');
eq(mute.state, 'on');
eq(mute.context.source, null);
});
test('RESTORE_UNMUTED off → on: page reload picks up unmuted', () => {
const mute = createFSM(muteSpec);
mute.send('FORCE_MUTE');
mute.send('RESTORE_UNMUTED');
eq(mute.state, 'on');
eq(mute.context.source, null);
});
test('ROLE_PROMOTED on → off: source = self (listener becomes speaker muted)', () => {
const mute = createFSM(muteSpec);
mute.send('ROLE_PROMOTED');
eq(mute.state, 'off');
eq(mute.context.source, 'self');
});
test('observer fires on every distinct transition', () => {
const mute = createFSM(muteSpec);
const seen = [];
mute.observe(({ state, prev, ctx, ev }) => {
if (prev === null || state === prev) return;
seen.push(prev + '→' + state + ':' + (ctx.source || '∅') + ':' + (ev && ev.type));
});
mute.send('TOGGLE'); /* on → off:self */
mute.send('TOGGLE'); /* off → on:∅ */
mute.send('AUTO_MUTE'); /* on → off:self-listener */
mute.send('AUTO_UNMUTE'); /* off → on:∅ */
eq(seen.length, 4);
eq(seen[0], 'on→off:self:TOGGLE');
eq(seen[1], 'off→on:∅:TOGGLE');
eq(seen[2], 'on→off:self-listener:AUTO_MUTE');
eq(seen[3], 'off→on:∅:AUTO_UNMUTE');
});
test('mod-forced mute survives an unrelated event', () => {
const mute = createFSM(muteSpec);
mute.send('FORCE_MUTE');
mute.send('FORCE_MUTE'); /* idempotent — still off, still mod */
eq(mute.state, 'off');
eq(mute.context.source, 'mod');
});
test('user TOGGLE off a mod-forced mute returns to on (no enforcement at FSM level)', () => {
/* The mute FSM intentionally does not enforce "mod-mute is sticky" that
* policy lives at the call site (only the mod's signed signed-mute message
* applies the FSM event). If a TOGGLE event ever does reach the FSM, it
* unmutes. Documented invariant. */
const mute = createFSM(muteSpec);
mute.send('FORCE_MUTE'); /* off:mod */
mute.send('TOGGLE'); /* on:∅ */
eq(mute.state, 'on');
eq(mute.context.source, null);
});
console.log('\n' + pass + ' passed, ' + fail + ' failed'); console.log('\n' + pass + ' passed, ' + fail + ' failed');
process.exit(fail === 0 ? 0 : 1); process.exit(fail === 0 ? 0 : 1);