Commit Graph

3 Commits

Author SHA1 Message Date
Claude fa766e0992 test(nests): T16 Phase 4 — browser-tier I2/I3/I4/I5/I9 scenarios
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'.
2026-05-07 03:45:30 +00:00
Claude a7ea77a35a test(nests): T16 Phase 4 — I13 long broadcast + I14 WebCodecs warmup
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.
2026-05-07 02:55:25 +00:00
Claude e0a9332498 feat(nests): T16 Phase 4.A+B — browser-side cross-stack interop scaffold + I1 forward
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
2026-05-07 00:44:42 +00:00