Adds the two browser-tier P0 scenarios deferred from Phase 4.C:
I13 (chromium_listener_long_broadcast_60s_tone_440):
- 60 s end-to-end Amethyst speaker -> Chromium @moq/lite + @moq/hang
Container.Legacy.Consumer; assert >= 50 s of decoded PCM, FFT
peak intact at 440 Hz, zero decoder errors.
- Spec asked for framesPerGroup = 50 against actual Container.Consumer.
Two constraints: (1) @moq/hang 0.2.4 doesn't export the high-level
Container.Consumer/Format API (Phase 4 uses Container.Legacy.Consumer
directly), (2) framesPerGroup = 50 against the local moq-relay
0.10.25 --auth-public minimal setup hits the per-subscriber forward
cliff per 2026-05-07-framespergroup-reconciliation.md. Pin 5
locally; production keeps 50.
- Catches what I1 forward (10 s) doesn't: Chromium WebTransport
MAX_STREAMS_UNI credit drift over 600+ uni streams, group-queue
eviction past MAX_GROUP_AGE = 30 s twice over, WebCodecs decoder
pacing/memory pressure at broadcast scale.
I14 (chromium_decoder_no_errors_through_warmup_window):
- Asserts Chromium AudioDecoder.error fires zero times during
a 10 s broadcast. Browser-tier mate of HangInteropTest's I11
(first_audio_frame_is_not_opus_codec_config); together they
cover T8 (BUFFER_FLAG_CODEC_CONFIG skip) on both reference paths.
- Deliberately no decoderOutputs floor: the Phase 4 harness has
a known cold-launch race that occasionally produces 0 frames
(per 2026-05-06-phase4-browser-harness-results.md). Since I14
is an absence assertion, vacuous-zero is safe — a T8 regression
would still trigger on whichever frames arrive in any green run.
- Note: JVM speaker uses libopus directly (no CSD prefix), so
this test path effectively asserts no spurious decode failures.
T8 itself is an Android-MediaCodecOpusEncoder fix; that path
isn't reachable from a JVM-host test.
Wire changes:
- listen.ts gains decoderOutputs + decoderErrors counters,
exposed via window.__decoderOutputs / __decoderErrors.
- tests/harness.spec.ts surfaces both in the meta JSON line.
- BrowserInteropTest.kt adds parseIntMetaFromStdout helper +
the two new scenarios.
Verification: all 4 BrowserInteropTest scenarios green in one
JVM run, no flake under -DnestsHangInterop=true -DnestsBrowserInterop=true.
Lands the bun + Playwright + headless Chromium harness for the
T16 cross-stack interop suite, parallel to the existing Rust
hang-listen tier. New top-level `nestsClient-browser-interop/`
directory with `@moq/lite` + `@moq/hang` 0.2.x pinned, a bun
static + WebSocket back-channel server, and a Playwright runner
that opens `listen.html` against the same `NativeMoqRelayHarness`
moq-relay subprocess the Rust scenarios use.
Kotlin side: `PlaywrightDriver` shells out to `bun x playwright
test`, forwards the relay URL + leaf-cert SHA-256 (captured via
a custom `CertCapturingValidator` during the speaker's QUIC
handshake), and reads back Float32 LE PCM frames from a tempfile
the bun WS server appends to. `BrowserInteropTest` ships I1
forward — Amethyst Kotlin speaker → Chromium `@moq/lite`
listener, asserting FFT 440 Hz on the captured tail.
Why pin via `serverCertificateHashes` instead of
`--ignore-certificate-errors`: Chromium's flag does NOT bypass
QUIC cert validation (crbug.com/1190655). `serverCertificateHashes`
is the supported path; moq-relay's `--tls-generate` produces a
14-day ECDSA P-256 cert that satisfies the spec.
Two Gradle tasks added: `interopBuildBrowserHarness` (bun
install + bun build → dist/) and `interopInstallPlaywrightChromium`
(skipped when `PLAYWRIGHT_BROWSERS_PATH` already has a chromium
build, as on the agent runner).
Verification:
- `./gradlew :nestsClient:jvmTest --tests
com.vitorpamplona.nestsclient.interop.native.BrowserInteropTest
-DnestsHangInterop=true -DnestsBrowserInterop=true` green.
- I1 forward asserts ≥ 1 s of decoded PCM with 440 Hz FFT peak.
Looser sample-count bound than the hang-tier I1 because
Chromium cold-launch + WebTransport handshake (3–5 s) + the
publisher's `framesPerGroup = 5` per-subscriber cache cliff
means the page captures only the broadcast tail.
Phase 4.C (I2/I3/I4/I13/I14/I15) and 4.D (CI) are separate
follow-up commits per the plan's per-scenario commit guidance.
See: nestsClient/plans/2026-05-06-phase4-browser-harness.md
https://claude.ai/code/session_01ERJPUYfdLPwZ99pr5EcEcV