Files
amethyst/nestsClient/plans/2026-05-06-cross-stack-interop-test-gap-matrix.md
Claude 15b8560547 ci(nests): defer cross-stack interop CI; document manual run
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.
2026-05-07 23:18:31 +00:00

116 lines
9.2 KiB
Markdown
Raw Permalink 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.
# T16 gap matrix — wire fixes ↔ asserting interop scenarios
**Status:** documentation. Closes Definition of Done #5 from
`2026-05-06-cross-stack-interop-test.md`.
This document maps each audit-branch T# wire fix that landed in
`main` to ≥ 1 hang-tier and/or browser-tier interop scenario that
asserts its wire output. A regression on any T# fix would trip the
listed scenario(s) deterministically.
**Source of truth for the T# series:** `git log --grep "fix(nests):
T"` against `origin/main` — I list only fixes that exist as concrete
commits, not the spec's aspirational T1T14 enumeration. Scenarios
are addressed by their interop-suite ID (I1I15) per
`2026-05-06-cross-stack-interop-test.md` and the
`HangInteropTest` / `BrowserInteropTest` Kotlin classes that
implement them.
## Wire fixes ↔ scenarios
| T# | Fix | Commit | Asserting scenario(s) | Tier | Status |
|---|---|---|---|---|---|
| **T8** | Skip `BUFFER_FLAG_CODEC_CONFIG` outputs in `MediaCodecOpusEncoder` (don't emit `OpusHead` as an audio frame). | `96cfa1235` | **I11** (`first_audio_frame_is_not_opus_codec_config`) — strips Container::Legacy and asserts the first audio frame's payload doesn't begin with the `OpusHead` magic. **I14** (`chromium_decoder_no_errors_through_warmup_window`) is the browser-side mate — asserts Chromium `AudioDecoder.error` count is 0 across the warmup window. | hang ✅ + browser ✅ | **green** |
| **T10** | `endGroup()` on unmuted → muted transition (don't park the open uni stream when the speaker mutes). | `c23da5279` | **I3** (`mid_broadcast_mute_shortens_decoded_pcm`) hang-tier + `chromium_listener_mid_broadcast_mute_shortens_pcm` browser-tier. Asserts the listener-side decoded PCM has a sample-count deficit consistent with stream FIN, NOT embedded zeros. A regression to "push zeros instead of FIN" would trip the upper bound. | hang ✅ + browser ✅ | **green** |
| **T11** | Drop `bestEffort = true` on moq-lite group uni streams (unreliable streams behave irregularly under loss). | `7e76ab113` | **I9** (`packet_loss_1pct_does_not_kill_audio`) hang-tier + `chromium_listener_packet_loss_1pct_does_not_kill_audio` browser-tier. Drives the QUIC client through `udp-loss-shim` at 1 % loss; asserts the FFT peak intact. With `bestEffort = true` re-introduced, frames lost on dropped packets would NOT be retransmitted. | hang ✅ + browser ✅ | **green** |
| **T12** | Carry audio group sequence across hot-swaps (don't reset to 0 on speaker re-issuance — listener decoder caches by group ordering). | `be4e0b9f9` | **I5** (`speaker_hot_swap_does_not_crash`) hang-tier + `chromium_listener_speaker_hot_swap_does_not_crash` browser-tier. Speaker calls `connectReconnectingSpeaker` mid-broadcast; asserts the listener sees no broadcast end and the post-swap window decodes cleanly with the 440 Hz peak intact. | hang ✅ + browser ✅ | **green** |
| **T13** | Reset Opus decoder on publisher boundary in `NestPlayer` (so the new publisher's pre-roll doesn't start mid-frame on stale decoder state). | `4714e3c72` | **I7** hang-tier (`rust_hang_publish_reconnect_kotlin_listener_recovers` in `HangInteropReverseTest`) — Rust hang-publish cycles its session at T+2.5 s. **I7 reverse browser** (`chromium_publisher_reconnect_kotlin_listener_recovers`) — Chromium publishes via `publish.ts` reconnect mode. Both assert ≥ 2.5 s of decoded mono PCM with the 440 Hz peak intact across the cycle. | hang ✅ + browser ✅ | **green** |
| **T14** | Recognise `GOAWAY` control type instead of silent FIN. | `73722d2ad` | **N/A in moq-lite-03.** moq-lite has no `GOAWAY` frame on the wire; the fix protects the IETF moq-transport-17 control-decoder path (`MoqSession.kt:417`). I12 was originally specced for this but doesn't apply — see `2026-05-06-cross-stack-interop-test-results.md`'s I12 section. The IETF code path is exercised by the existing `MoqCodecTest` unit test (`unknown_control_type_skips_message_without_corruption`) only — no cross-stack scenario covers it because no cross-stack peer speaks IETF moq-transport. | unit-test only | **green** |
## Aspirational T1T7, T9, T15
The spec's "T1T14" enumeration is not all wire fixes. Searching
`main` for `fix(nests): T1``T7`, `T9`, `T15` returns no commits;
those numbers existed in the audit's findings list but didn't
crystallise into named patches before audit closure. If a future
fix re-uses one of those numbers, this matrix should be updated to
record the asserting scenario(s) alongside the listed T# above.
The spec's claim "Catches every audio-path wire regression (T1T14)
with at least one cross-stack scenario" should be read as: catches
every audio-path wire regression that has a concrete fix in `main`,
which is the T8 / T10T14 set.
## Spec scenario ↔ T# reverse index
For maintainers reading the test code who want to know *which* fix
each scenario protects:
| Scenario | Protects |
|---|---|
| **I1** (forward 440 Hz mono) | Baseline path: T8 + T11 (frames carry pristine Opus over reliable streams). A break of either trips the FFT or sample-count assertion. |
| **I2** (late-join) | T11 implicitly (streams must arrive in order without RST under no-loss). |
| **I3** (mute window) | **T10** explicitly. |
| **I4 fwd** (stereo 440/660) | Stereo plumbing through `AudioBroadcastConfig` (PR #2755) — not a T-series fix; protects the per-channel catalog → encoder pipeline. |
| **I4 rev** (stereo Rust → Kotlin) | Listener stereo decode path. |
| **I5** (speaker hot-swap) | **T12** explicitly (hang + browser tiers). |
| **I6** (multi-listener) | T11 fan-out behaviour. |
| **I7** (publisher reconnect) | **T13** explicitly. Hang-tier exercises Rust hang-publish reconnect; browser-tier exercises Chromium publish.ts reconnect. |
| **I8** (SubscribeDrop on unknown track) | Subscribe rejection handling — moq-lite-03 protocol-level guard, not a T-series fix. |
| **I9** (1 % packet loss) | **T11** explicitly (hang + browser tiers). |
| **I10** (60 s long broadcast) | `framesPerGroup` cadence interaction at scale (see `2026-05-07-framespergroup-reconciliation.md`). |
| **I11** (wire-byte capture) | **T8** explicitly. |
| **I12** (Goaway) | N/A in moq-lite-03; **T14** is exercised only by the IETF moq-transport unit test path. |
| **I13** (browser `framesPerGroup=50` long broadcast) | `framesPerGroup` cadence interaction at scale on the browser path. Note: spec asked for `framesPerGroup = 50`; local relay's per-stream byte cliff blocks that, so the test pins `5` — see `2026-05-07-framespergroup-reconciliation.md`. |
| **I14** (WebCodecs warmup × CSD-skip) | **T8** browser-side mate of I11. Asserts `AudioDecoder.error` count is 0; a `OpusHead` leak would fail. |
| **I15** (Chromium ALPN round-trip) | moq-lite ALPN drift detection. |
## Coverage state
All T-series wire fixes (T8, T10T14) have ≥ 1 cross-stack
asserting scenario landed. Both hang-tier AND browser-tier
mates exist for T8/T10/T11/T12/T13. T14 (GOAWAY) only applies
to the IETF moq-transport target which the production stack
doesn't use.
DoD #5 (gap matrix coverage) closed.
**History notes (2026-05-07):**
- Five browser-tier scenarios used to soft-pass listener-side
0-frame outcomes during a flake we attributed to a moq-relay
routing race; trace capture later disproved that hypothesis
and a `:quic` fix on `origin/main` closed it (see
`2026-05-07-moq-relay-routing-investigation.md` § Closure).
Hard floors landed in
`2026-05-07-tighten-cross-stack-assertions.md` after the merge:
late-join (`≥ 1.5 s after warmup`), mute-window (`≥ 2.5 s`
lower + `< 5.5 s` upper kept), I14 (`decoderOutputs ≥ 4`),
stereo (`≥ 1 s × 2 ch`), packet-loss (`≥ 0.5 s`), and the
browser-publisher helper now hard-asserts on the listener side.
Hot-swap kept a (now-documented) soft-pass on the browser
tier — Chromium's `@moq/lite` 0.2.x re-attach across
`Active::Ended → Active` is unreliable; T12 protection is
asserted by the hang-tier counterpart.
- CI gating reached green-on-the-rig (10/10 × 22 = 220/220)
in commit `21947bc5`, then was deferred on cost grounds —
both suites run manually now per
[`nestsClient/tests/README.md`](../tests/README.md). YAML
shape preserved in
`2026-05-07-cross-stack-interop-ci-gating.md` for the next
revisit.
## Files referenced
- `nestsClient/plans/2026-05-06-cross-stack-interop-test.md` (spec — Definition of Done #5)
- `nestsClient/plans/2026-05-06-cross-stack-interop-test-results.md` (results — what landed)
- `nestsClient/plans/2026-05-07-framespergroup-reconciliation.md` (cadence reconciliation)
- `nestsClient/plans/2026-05-07-i7-post-reconnect-cliff-investigation.md` (I7 cycle-2 cliff)
- `nestsClient/plans/2026-05-07-late-join-catalog-flake-investigation.md` (relay routing flake)
- `nestsClient/plans/2026-05-07-t16-closure-roadmap.md` (next steps)
- `nestsClient/src/jvmTest/kotlin/com/vitorpamplona/nestsclient/interop/native/HangInteropTest.kt`
- `nestsClient/src/jvmTest/kotlin/com/vitorpamplona/nestsclient/interop/native/HangInteropReverseTest.kt`
- `nestsClient/src/jvmTest/kotlin/com/vitorpamplona/nestsclient/interop/native/HangInteropMultiListenerTest.kt`
- `nestsClient/src/jvmTest/kotlin/com/vitorpamplona/nestsclient/interop/native/BrowserInteropTest.kt`
- T# fix commits: `96cfa1235` (T8), `c23da5279` (T10), `7e76ab113` (T11), `be4e0b9f9` (T12), `4714e3c72` (T13), `73722d2ad` (T14)