589ced580c
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 mention00f6cba31+207057374were 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.
151 lines
7.5 KiB
Markdown
151 lines
7.5 KiB
Markdown
# 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 3–10 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.
|