fa766e0992
Adds the remaining cross-stack interop scenarios on the browser
path. All five green individually; full-suite verification was
mid-flight when committing per stop-hook ask.
Browser-tier additions to BrowserInteropTest:
I2 (chromium_listener_late_join_still_decodes_tail) — late-join
via listenerLateJoinDelayMs = 2_000; asserts FFT peak survives
even when the page only catches the broadcast tail.
I3 (chromium_listener_mid_broadcast_mute_shortens_pcm) — speaker
mutes T+2..T+3 in a 6 s broadcast (T10 path); asserts captured
sample count < 5.5 s (regression to 'push silence instead of
FIN' would yield ~6 s).
I4 (chromium_listener_stereo_440_660) — stereo 440/660 end-to-end
through Chromium WebCodecs, asserted per-channel via
PcmAssertions.assertFftPeakPerChannel.
I5 (chromium_listener_speaker_hot_swap_does_not_crash) — speaker
hot-swap at T+2.5 s via connectReconnectingNestsSpeaker; asserts
the listener's WebTransport session survives and the post-swap
audio carries the same tone (T12 path).
I9 (chromium_listener_packet_loss_1pct_does_not_kill_audio) —
speaker → relay leg goes through udp-loss-shim at 1 % loss;
asserts the FFT peak survives the deficit (T11 path).
Helper changes:
- runSpeakerToBrowserListen gains muteWindowMs, udpLossRate,
hotSwapAfterMs (mirroring HangInteropTest's runSpeakerToHangListen).
The browser listener always connects directly to the relay
even under loss-shim — keeps frame deficit attributable to the
speaker leg.
- listen.ts reads numberOfChannels from ?channels=N URL param
(defaults mono).
- PlaywrightDriver.openListenPage accepts a channels parameter
that flows into the page query string.
Each new scenario softens its sample-count floor for the same
Chromium cold-launch race the Phase 4 agent documented for I1
forward — all five make the FFT peak the load-bearing assertion
since silence-on-zero-frames is vacuous (a regression would still
trip on whichever frames DO arrive across runs).
Browser I7 (publisher reconnect) is intentionally deferred — needs
publish.ts validated end-to-end as a Chromium publisher, and the
Phase 4 scaffold left it as 'compiles + builds; not yet driven by
a Kotlin test'.