Files
amethyst/nestsClient/plans/2026-05-06-phase4-browser-harness-results.md
Claude 589ced580c docs(nests): refresh all T16 + cliff plans to current state
8 plan files updated to reflect what's actually shipped vs. what
each doc said before:

- 2026-05-06-cross-stack-interop-test.md: 'Spec — ready to
  implement' → 'Implemented and merged' with pointers to results,
  gap matrix, and closure roadmap.

- 2026-05-06-cross-stack-interop-test-results.md: scenario
  inventory updated to show all branches merged into
  claude/cross-stack-interop-test-XAbYB; suite-flake caveats
  flagged with cross-refs to the routing investigation; file
  inventory now lists BrowserInteropTest + PlaywrightDriver +
  the moved nestsClient/tests/browser-interop/ tree (no more 'in
  sister branches not yet merged' section).

- 2026-05-06-cross-stack-interop-test-gap-matrix.md: T8/T10/T11/
  T12/T13 all show hang  + browser ; I13/I14 'NOT YET LANDED'
  callouts removed; the 'in sister branch' qualifier on T13 / I7
  / I6 dropped; coverage-holes section replaced with caveat
  pointers to the routing investigation.

- 2026-05-06-i4-stereo-cross-stack-scenario.md: 'Spec — ready for
  implementation' → 'Landed' with PR #2755 + the three test
  scenarios that ship the assertion.

- 2026-05-06-phase4-browser-harness.md: 'Spec — ready for
  implementation' → 'Landed', with notes on how the implementation
  diverged (used Container.Legacy.Consumer not @moq/watch; cert
  pinning via serverCertificateHashes; I2/I3/I4/I13/I14 originally
  deferred but later landed).

- 2026-05-06-phase4-browser-harness-results.md: status updated to
  reflect 4.D CI removed per maintainer ask; layout note for
  the nestsClient/tests/browser-interop/ relocation; full
  scenario list reconciled with results doc.

- 2026-05-07-late-join-catalog-flake-investigation.md: status
  updated to mention 00f6cba31 + 207057374 were reverted, and
  link to 2026-05-07-moq-relay-routing-investigation.md as the
  action plan. Adds note that the flake also affects four
  browser-tier scenarios after Browser I7 landed.

- 2026-05-01-quic-stream-cliff-investigation.md: adds an HCgOY-
  override banner — recommendation 2 (framesPerGroup = 5) was
  superseded 4 days later by HCgOY field tests landing the prod
  default at 50. Cross-refs the reconciliation + production-
  rerun plans. Open follow-up #3 ('reset to 1') updated with
  the same context.

No code or test changes.
2026-05-07 14:52:21 +00:00

7.5 KiB
Raw Permalink Blame History

Plan: Phase 4 (browser harness) — landed results

Status: Phase 4.A (scaffold) + 4.B (Playwright driver) + 4.C (full scenario coverage) all landed. Phase 4.D (CI workflow job) was originally added in commit c79a3ffa8 but later removed per maintainer ask in commit b94737de7 ("don't add these tests to the build.yml for now"); see 2026-05-07-cross-stack-interop-ci-gating.md for the path to re-wiring.

Browser harness location: nestsClient/tests/browser-interop/ (was originally nestsClient-browser-interop/ at repo root; moved to mirror nestsClient/tests/hang-interop/ layout).

Final scenario coverage: I1, I2, I3, I4 (stereo), I5 (hot-swap), I7 (Chromium publisher reconnect), I9 (packet loss), I13 (long broadcast), I14 (WebCodecs warmup × CSD), I15 (ALPN). See 2026-05-06-cross-stack-interop-test-results.md for the full inventory.

Tracks the spec at nestsClient/plans/2026-05-06-phase4-browser-harness.md.

Where it landed

  • New top-level nestsClient/tests/browser-interop/ workspace:
    • package.json pins @moq/lite@0.2.2, @moq/hang@0.2.4, @moq/watch@0.2.10, @moq/publish@0.2.6, @playwright/test@1.56.1.
    • REV documents the pinned versions next to the nestsClient/tests/hang-interop/REV.
    • src/listen.html + src/listen.ts — Watch path, uses Container.Legacy.Consumer and WebCodecs AudioDecoder directly (the published @moq/hang 0.2.4 doesn't expose the higher-level Container.Consumer from upstream HEAD; we wire its data path manually).
    • src/publish.html + src/publish.ts — symmetric publisher scaffold for the I4-reverse / I14-decoder-warmup scenarios Phase 4.C extension can pick up.
    • src/server.ts — bun static + WebSocket back-channel; the listener page posts Float32 LE PCM frames as binary messages, a textual done message flips the server's done flag.
    • tests/harness.spec.ts — single Playwright spec the Kotlin driver invokes per scenario; reads NESTS_HARNESS_URL
      • NESTS_TIMEOUT_MS from env.
    • playwright.config.ts — Chromium with --enable-quic, --ignore-certificate-errors, AutoplayPolicy override.
  • nestsClient/src/jvmTest/.../interop/native/PlaywrightDriver.kt — Kotlin shim that spawns the bun server + bun x playwright test per test, returns a HarnessRun(pcmFile, stdout, exit). Includes a CertCapturingValidator that pulls the relay's leaf cert during the speaker's QUIC handshake so we can pass its SHA-256 to Chromium via serverCertificateHashes.
  • nestsClient/src/jvmTest/.../interop/native/BrowserInteropTest.kt — two scenarios:
    • I1 forward (browser): Amethyst Kotlin speaker → Chromium @moq/lite listener; asserts FFT 440 Hz on the captured tail.
    • I15 (WT-Protocol round-trip): asserts Chromium's Connection.version starts with moq-lite-.
  • nestsClient/build.gradle.kts — two new tasks:
    • interopBuildBrowserHarness (bun install + bun build → dist/),
    • interopInstallPlaywrightChromium (skipped if PLAYWRIGHT_BROWSERS_PATH already points at a chromium build). Both gated on -DnestsBrowserInterop=true like the hang tier is gated on -DnestsHangInterop=true.
  • .github/workflows/build.yml — new browser-interop job parallel to hang-interop, with bun + node_modules + Playwright caches.

Deviations from the spec

  1. Source layout: @moq/lite + @moq/hang direct, NOT @moq/watch Watch.Broadcast. The spec called for mirroring NostrNests's transport/moq-transport.ts Watch.Broadcast verbatim; in practice @moq/watch 0.2.x bakes in a heavy reactive Effect/Signal layer that's unwieldy for a one-shot capture page. The lower-level connection.consume(path) → broadcast.subscribe(track) → track.readFrame() pipeline is what the watch decoder uses internally, so this is a functionally equivalent path. NostrNests-side regressions in Watch.Broadcast plumbing aren't in scope of T16.
  2. Cert pinning via serverCertificateHashes, not --ignore-certificate-errors. Chromium's --ignore-certificate-errors flag does NOT bypass QUIC cert validation — reproduced as net::ERR_QUIC_PROTOCOL_ERROR. QUIC_TLS_CERTIFICATE_UNKNOWN. The spec mentioned --ignore-certificate-errors-spki-list as a "preferred long- term form"; we use serverCertificateHashes (Web-API equivalent), which works because moq-relay's --tls-generate produces a 14-day ECDSA P-256 cert — exactly what the WebTransport spec requires for a serverCertificateHashes pin. The CertCapturingValidator snags the cert during the speaker's QUIC handshake so we don't need a separate fingerprint endpoint.
  3. I1 sample-count assertion loosened. Hang-tier I1 asserts assertSampleCount(expected = 5 s, tolerance = 0.20) — the browser path can't hit that because Chromium cold-launch + Playwright runner setup eats 310 s before the page starts capturing, by which time the framesPerGroup = 5 per-subscriber forward cliff means only the latest cached group is replayable. The browser I1 instead asserts ≥ 1 s of decoded audio + FFT peak at 440 Hz. The FFT peak is the load-bearing assertion (catches downmix / channel-swap / OpusHead-leak regressions); the sample-count threshold is just a sanity floor.
  4. Phase 4.C scenarios I2/I3/I4/I13/I14 deferred. I2 late-join collapses into "tail capture" anyway given the Chromium boot lag, so it's not adding signal beyond I1. I3 mute-window has the same visibility issue. I4 needs the reverse publisher path wired up end-to-end (a stub publish.ts landed but isn't exercised by a Kotlin test yet). I13 long broadcast and I14 CSD-skip are runtime-of-test concerns the I1 path already exercises implicitly. Tracked as a follow-up on a separate plan if/when the gap matters.

Verification

./gradlew :nestsClient:jvmTest \
    --tests "com.vitorpamplona.nestsclient.interop.native.BrowserInteropTest" \
    -DnestsHangInterop=true \
    -DnestsBrowserInterop=true

Both amethyst_speaker_to_chromium_listener_static_tone_440 and chromium_round_trips_a_moq_lite_session pass in isolation.

Follow-ups

  • I4-reverse: wire BrowserInteropTest to drive PlaywrightDriver.openPublishPage (the Kotlin side already exposes the entry point) → Amethyst Kotlin listener decodes; assert per-channel FFT peaks. Needs the publish.ts harness graduated from scaffold to a fully-working pump (the MediaStreamTrackProcessorAudioEncoderContainer.Legacy. Producer chain compiles but isn't yet validated end-to-end).
  • I3 mute-window: works on the Kotlin speaker side, but the short browser tail capture window means the mute-gap deficit isn't observable. Would need either a longer broadcast (60 s+) or a tighter capture window that brackets the mute schedule reliably. Low priority — the hang-tier I3 already validates the speaker-side mute behaviour against a parser-correct watcher.
  • I15 strict pin: when moq-relay 0.10.x ships with both moq-lite-03 and moq-lite-04 ALPN advertisement, tighten the assertion from startsWith("moq-lite-") to exact-match moq-lite-03 (or whichever the production stack runs). Right now the relay we boot lands moq-lite-02 over the legacy moql ALPN.
  • CI cold-cache time: cold npx playwright install chromium takes ~60 s on a fresh GitHub runner. The actions/cache@v4 hits keyed on package.json should make warm runs near-zero, but the first run on a new branch will be slow.