Removes the `hang-interop` + `browser-interop` jobs from
`build.yml` (added in commit `21947bc5` after a 10/10 stability
sweep × 22 tests = 220/220 hard-pass). Cold-cache cost is
~10 min hang + ~13 min browser; warm-cache critical-path add
is ~6 min via the parallel browser job. Most PRs don't touch
audio / MoQ / QUIC, so paying that cost on every PR is
net-negative for the change-pattern the repo sees.
Adds `nestsClient/tests/README.md` covering:
- when to run (changes to nip53 / moq-lite session / audio
pipeline / MoqLiteNests* / ReconnectingNests* / :quic /
the sidecars themselves);
- quick-start gradle commands for hang-only, browser-only,
combined;
- prerequisites + first-run cache costs;
- all the configuration knobs (incl. the
`-DnestsHangInteropTraceRelay=true` trace-capture switch
added during the routing investigation);
- known limitations (hot-swap browser soft-pass,
framesPerGroup pin-vs-prod gap, I7 cycle 2 truncation);
- a 4-step debug recipe for triaging a flaking scenario.
Plan updates:
- `2026-05-07-t16-closure-roadmap.md` — Priority 3 marked
⏸ DEFERRED instead of ✅ CLOSED, pointing at the new README.
- `2026-05-07-cross-stack-interop-ci-gating.md` — status
changed to ⏸ DEFERRED; YAML shape preserved verbatim in the
plan for the next revisit.
- `2026-05-06-cross-stack-interop-test-results.md`,
`2026-05-06-cross-stack-interop-test-gap-matrix.md` — CI
integration § rewritten to "manual-run only" + README link.
The trace-capture instrumentation in `NativeMoqRelayHarness`
stays in place; it's useful for future flake triage even
without CI.
9.2 KiB
Cross-stack interop tests
Manually invoked. Not part of
build.yml— see "Why not in CI" below.
Two suites that drive the production Amethyst Kotlin nests stack against external reference implementations of moq-lite-03:
| Suite | Path | Runs | What it asserts |
|---|---|---|---|
HangInteropTest |
nestsClient/tests/hang-interop/ |
Rust hang-listen / hang-publish (kixelated reference) ↔ Amethyst Kotlin via a real moq-relay 0.10.x subprocess |
Wire-byte capture, FFT peaks on decoded PCM, sample-count floors, mute / hot-swap / packet-loss / late-join / 60 s long broadcast / multi-listener fan-out. |
BrowserInteropTest |
nestsClient/tests/browser-interop/ |
Headless Chromium running @moq/lite + @moq/hang via Playwright ↔ Amethyst Kotlin (forward + reverse) |
Same scenario family as hang-interop, plus WebCodecs AudioDecoder / AudioEncoder correctness, ALPN negotiation, browser-side reconnect. |
Coverage matrix: nestsClient/plans/2026-05-06-cross-stack-interop-test-gap-matrix.md
When to run
Run both suites locally before merging any change that touches:
quartz/.../nip53(audio rooms event types, catalog wire format)nestsClient/src/.../moq/lite/(moq-lite session, publisher/subscriber, codec)nestsClient/src/.../audio/(capture, encoder/decoder, broadcaster, player)nestsClient/src/.../MoqLiteNestsSpeaker.kt/MoqLiteNestsListener.ktnestsClient/src/.../ReconnectingNests*.kt:quic(WebTransport, packet header protection, key updates, stream demux)- The hang/browser sidecars themselves
Skip if the change is documentation, UI-only, build-script-only, or otherwise can't affect wire bytes / decoded audio.
Quick start
# Hang-tier (Rust ↔ Kotlin via real moq-relay subprocess)
./gradlew :nestsClient:jvmTest \
--tests "com.vitorpamplona.nestsclient.interop.native.HangInteropTest" \
-DnestsHangInterop=true
# Browser-tier (Chromium ↔ Kotlin) — also boots the moq-relay
./gradlew :nestsClient:jvmTest \
--tests "com.vitorpamplona.nestsclient.interop.native.BrowserInteropTest" \
-DnestsHangInterop=true \
-DnestsBrowserInterop=true
# Both together
./gradlew :nestsClient:jvmTest \
--tests "com.vitorpamplona.nestsclient.interop.native.HangInteropTest" \
--tests "com.vitorpamplona.nestsclient.interop.native.BrowserInteropTest" \
-DnestsHangInterop=true -DnestsBrowserInterop=true
The opt-in flags (-DnestsHangInterop=true / -DnestsBrowserInterop=true)
also act as JUnit Assume gates — without them, the suites mark every
scenario skipped rather than failing on missing prerequisites.
Prerequisites
| Tool | Why | Install |
|---|---|---|
| Rust ≥ 1.95 | moq-relay 0.10.25's transitive dep constant_time_eq 0.4.3 requires it |
rustup install 1.95 && rustup default 1.95 |
cargo on PATH |
Builds the hang-interop sidecars + cargo installs moq-relay, moq-token-cli |
Comes with rustup |
bun ≥ 1.3.x (browser only) |
Bundles the Chromium harness + drives Playwright | curl -fsSL https://bun.sh/install | bash |
| Playwright Chromium (browser only) | Headless Chromium for @moq/lite + @moq/hang |
Auto-installed by interopInstallPlaywrightChromium Gradle task |
The Gradle build automates everything from there. First run installs and caches:
moq-relay0.10.25 +moq-token-cli0.5.23 →~/.cache/amethyst-nests-interop/hang-interop-cargo/bin/- Sidecar release binaries →
nestsClient/tests/hang-interop/target/release/ - bun's
node_modules+dist/→nestsClient/tests/browser-interop/ - Playwright Chromium →
~/.cache/ms-playwright/
Cold first run: ~10 min hang, ~13 min browser. Cached: ~3-4 min hang, ~5-7 min browser.
Configuration knobs
| Flag | Default | Purpose |
|---|---|---|
-DnestsHangInterop=true |
unset (skip) | Enable HangInteropTest. |
-DnestsBrowserInterop=true |
unset (skip) | Enable BrowserInteropTest. Implies nestsHangInterop (the browser harness boots the same moq-relay subprocess). |
-DnestsHangInteropTraceRelay=true |
unset | Per-test moq-relay trace capture. Writes the relay subprocess's combined stdout/stderr (with RUST_LOG=info,moq_relay=trace,moq_lite=trace,moq_native=debug) to nestsClient/build/relay-logs/<methodName>-<seq>-<ts>.log. Use for debugging suite flakes. |
-DnestsHangInteropDiagnostic=true |
unset | Runs the Kotlin↔Kotlin variant gated separately (KotlinSpeakerKotlinListenerThroughNativeRelayTest) — useful to bisect wire-format bugs without involving Rust or Chromium. |
BUN_BIN, NPX_BIN (env) |
autodetected | Override the bun / npx binary path if your install lives outside the agent default (/root/.bun/bin/bun). |
Known limitations
chromium_listener_speaker_hot_swap_does_not_crashsoft-passes its PCM assertion — Chromium's@moq/lite0.2.x doesn't re-attach acrossActive::Ended → Active, so the browser captures only ~100 ms post-swap. T12 (group sequence carry across hot-swaps) is hard-asserted by the hang-tier counterpart.framesPerGroupis pinned to5in the test scenarios. Production defaults to50. The two haven't been reconciled against a single multi-rig benchmark — seeframespergroup-reconciliation.- I7 cycle 2 truncation:
moq-relay0.10.x truncates the second cycle of a publisher reconnect at ~1.0 s out of ~2.5 s. Tests assert≥ 2.5 sfloor which the truncated cycle still clears; a future upstream fix may let us tighten further. - I12 GOAWAY: not applicable to moq-lite-03; only the IETF moq-transport target (currently disabled) would exercise it.
Full open-issues list:
2026-05-06-cross-stack-interop-test-results.md § Pending follow-ups.
Debugging a flaking scenario
- Re-run the failing scenario in isolation (no
--testsfilter races):./gradlew :nestsClient:jvmTest \ --tests "*HangInteropTest.late_join_listener_still_decodes_tail" \ -DnestsHangInterop=true \ -DnestsHangInteropTraceRelay=true \ --rerun-tasks - Inspect
nestsClient/build/relay-logs/<methodName>-*.logforsubscribed started/encoding self=Subscribe/decoded result=SubscribeOk/subscribed cancelledevents. - Cross-reference with the speaker-side
Log.d("NestTx")lines captured in JUnit XML's<system-err>(nestsClient/build/test-results/jvmTest/TEST-*.xml) by timestamp. - The fastest diagnostic loop bypasses Browser/Chromium entirely — use
the diagnostic test:
./gradlew :nestsClient:jvmTest \ --tests "*KotlinSpeakerKotlinListenerThroughNativeRelayTest" \ -DnestsHangInteropDiagnostic=true
Sample pre/post-merge trace pair for the previously-flaking late-join
scenario lives at
plans/artefacts/2026-05-07-routing-race-disproven/
Why not in CI
Both suites are slow on a cold cache (~10–13 min each), and even on a warm cache the browser tier dominates the critical path at ~5-7 min. Running them on every PR doubles CI time for changes that don't touch audio / MoQ / QUIC.
History:
21947bc5re-added both jobs after the T16 closure roadmap closed Priority 1 (:quicpost-handshake bidi-drop fixed viaorigin/mainmerge) and Priority 2 (assertion hardening). 10/10 sweep × 22 tests = 220/220 hard-pass on the merged branch.- A subsequent maintainer review judged the wallclock cost too high for the change-pattern (most PRs don't touch the relevant code) and removed the jobs.
The suites still run locally per the rules above. If a PR DOES touch audio / MoQ / QUIC code paths and the author hasn't run them, ask in review.
Files
hang-interop/REV— pinned upstream versions (MOQ_RELAY_VERSION,MOQ_TOKEN_CLI_VERSION,HANG_VERSION,MOQ_LITE_VERSION,MOQ_NATIVE_VERSION). Bump deliberately.hang-interop/Cargo.toml+hang-{listen,publish}/,udp-loss-shim/— Rust sidecar workspace.browser-interop/package.json+src/+playwright.config.ts— bun + Playwright harness.nestsClient/src/jvmTest/kotlin/.../interop/native/:NativeMoqRelayHarness.kt— boots the relay subprocess + captures per-test trace.HangInteropTest.kt,HangInteropReverseTest.kt,HangInteropMultiListenerTest.kt,KotlinSpeakerKotlinListenerThroughNativeRelayTest.kt— hang-tier scenarios.BrowserInteropTest.kt,PlaywrightDriver.kt— browser-tier.
See also
2026-05-06-cross-stack-interop-test.md— original spec / Definition of Done.2026-05-07-t16-closure-roadmap.md— closure roadmap (Priorities 1, 2 closed; Priority 3 deferred).2026-05-07-cross-stack-interop-ci-gating.md— the deferred CI-gating plan (kept around in case the wallclock cost calculus changes).