Files
amethyst/nestsClient/plans/2026-05-06-phase4-browser-harness-results.md
T
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

151 lines
7.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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
```bash
./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
`MediaStreamTrackProcessor``AudioEncoder``Container.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.