chore(nests): mv cli/hang-interop → nestsClient/tests/hang-interop

Per maintainer request: the Rust sidecar workspace lives
under the module that owns it, parallel to the upcoming
nestsClient-browser-interop/ harness. The cargo workspace,
Gradle wiring, gitignore, CI YAML, plan docs, and harness
kdoc are all updated. cargo build --release still compiles;
HangInteropTest.amethyst_speaker_to_hang_listener_stereo_440_660
green at the new path.

Note: the live Phase 4 browser-harness agent (worktree
agent-a97a6be483ecee618) was branched from before this move
and references `cli/hang-interop` in its plan-doc imports.
It will need to rebase onto this commit before it pushes;
no code-level conflicts since the agent works exclusively
in nestsClient-browser-interop/ + a new
BrowserInteropTest.kt.

https://claude.ai/code/session_01ERJPUYfdLPwZ99pr5EcEcV
This commit is contained in:
Claude
2026-05-06 23:26:06 +00:00
parent 79a4019438
commit 96fa68e0cb
17 changed files with 37 additions and 37 deletions
+5 -5
View File
@@ -172,15 +172,15 @@ jobs:
path: ${{ matrix.desktop-artifact-path }}
# Cross-stack interop tests (T16). Builds the Rust hang-listen +
# hang-publish sidecars (cli/hang-interop/), `cargo install`s
# hang-publish sidecars (nestsClient/tests/hang-interop/), `cargo install`s
# moq-relay + moq-token-cli at the version pinned in
# `cli/hang-interop/REV`, then runs `:nestsClient:jvmTest
# `nestsClient/tests/hang-interop/REV`, then runs `:nestsClient:jvmTest
# -DnestsHangInterop=true`. See
# `nestsClient/plans/2026-05-06-cross-stack-interop-test.md`.
#
# Linux-only: the cargo install of `moq-relay` 0.10.x has nontrivial
# native deps (aws-lc-sys, ring) that take 5+ min cold; we cache
# both ~/.cargo and the cli/hang-interop/target tree so the warm
# both ~/.cargo and the nestsClient/tests/hang-interop/target tree so the warm
# path is a few seconds. macOS / Windows runs would double the
# matrix cost without catching anything Linux doesn't catch — the
# protocol logic is platform-agnostic and the JNA libopus natives
@@ -220,9 +220,9 @@ jobs:
path: |
~/.cargo/registry
~/.cargo/git
cli/hang-interop/target
nestsClient/tests/hang-interop/target
~/.cache/amethyst-nests-interop/hang-interop-cargo
key: ${{ runner.os }}-cargo-${{ hashFiles('cli/hang-interop/Cargo.lock', 'cli/hang-interop/REV') }}
key: ${{ runner.os }}-cargo-${{ hashFiles('nestsClient/tests/hang-interop/Cargo.lock', 'nestsClient/tests/hang-interop/REV') }}
restore-keys: |
${{ runner.os }}-cargo-
+2 -2
View File
@@ -178,5 +178,5 @@ benchmark/src/main/jniLibs/
/tools/marmot-interop/state
# Cargo build artifacts for the cross-stack interop sidecars at
# cli/hang-interop/. Cargo.lock is committed (binary workspace).
/cli/hang-interop/target/
# nestsClient/tests/hang-interop/. Cargo.lock is committed (binary workspace).
/nestsClient/tests/hang-interop/target/
+5 -5
View File
@@ -128,7 +128,7 @@ tasks.withType<Test>().configureEach {
// ---- Cross-stack interop: Rust sidecar build + binary path forwarding -------
//
// Phase 1 of the interop plan ships the workspace at `cli/hang-interop/`
// Phase 1 of the interop plan ships the workspace at `nestsClient/tests/hang-interop/`
// with three stub binaries (hang-listen, hang-publish, udp-loss-shim).
// `interopBuildHangSidecars` runs `cargo build --release` against it and
// resolves the upstream `moq-relay` + `moq-token` binaries via
@@ -140,15 +140,15 @@ tasks.withType<Test>().configureEach {
// actual interop scenarios land in Phase 2 once `hang-listen` /
// `hang-publish` have real subscribe/publish loops. See
// `nestsClient/plans/2026-05-06-cross-stack-interop-test.md` for the
// full plan and the pinned upstream versions in `cli/hang-interop/REV`.
// full plan and the pinned upstream versions in `nestsClient/tests/hang-interop/REV`.
val hangInteropDir = rootProject.layout.projectDirectory.dir("cli/hang-interop")
val hangInteropDir = rootProject.layout.projectDirectory.dir("nestsClient/tests/hang-interop")
val hangInteropCacheDir =
layout.projectDirectory
.dir(System.getProperty("user.home") ?: "/tmp")
.dir(".cache/amethyst-nests-interop/hang-interop-cargo")
// Versions are duplicated from cli/hang-interop/REV so Gradle has them
// Versions are duplicated from nestsClient/tests/hang-interop/REV so Gradle has them
// at configuration time; bumping requires touching both files.
val moqRelayVersion = "0.10.25"
val moqTokenCliVersion = "0.5.23"
@@ -194,7 +194,7 @@ val interopInstallMoqTokenCli by tasks.registering(Exec::class) {
}
val interopBuildSidecars by tasks.registering(Exec::class) {
description = "cargo build --release for cli/hang-interop sidecars"
description = "cargo build --release for nestsClient/tests/hang-interop sidecars"
group = "interop"
workingDir = hangInteropDir.asFile
commandLine("cargo", "build", "--release")
@@ -107,13 +107,13 @@ the spec, and the concrete pickup points for Phase 2.
## What landed
### Cargo workspace (`cli/hang-interop/`)
### Cargo workspace (`nestsClient/tests/hang-interop/`)
- Workspace with three binary crates: `hang-listen`, `hang-publish`,
`udp-loss-shim`. **All three are Phase-1 stubs** — they parse their
CLI flags via `clap`, print a banner, and exit 0. Phase 2 fills the
bodies.
- `cli/hang-interop/REV` documents the pinned upstream
- `nestsClient/tests/hang-interop/REV` documents the pinned upstream
`kixelated/moq` rev (`9e2461ee...`) plus the published crate
versions on crates.io that track that rev (`moq-relay 0.10.25`,
`moq-token-cli 0.5.23`, `hang 0.15.8`, `moq-lite 0.15.15`,
@@ -128,7 +128,7 @@ the spec, and the concrete pickup points for Phase 2.
the binary already exists in the cache.
- `interopInstallMoqTokenCli` — same shape for `moq-token-cli`.
- `interopBuildSidecars` — `cargo build --release` over the local
`cli/hang-interop/` workspace.
`nestsClient/tests/hang-interop/` workspace.
- `interopBuildHangSidecars` — umbrella task that depends on the
three above. Runs as a test dependency only when
`-DnestsHangInterop=true` is set.
@@ -210,7 +210,7 @@ rendition with `container.kind == "legacy"` and
`Bytes`-encoded VarInt timestamp + Opus packet, run the Opus
packets through `audiopus::Decoder`, write Float32 PCM to
`--output-pcm`. Dependencies to add to
`cli/hang-interop/hang-listen/Cargo.toml`:
`nestsClient/tests/hang-interop/hang-listen/Cargo.toml`:
```toml
hang = "0.15"
@@ -297,7 +297,7 @@ the smoke test.
## Files
```
cli/hang-interop/
nestsClient/tests/hang-interop/
├── REV
├── Cargo.toml + Cargo.lock
├── hang-listen/{Cargo.toml,src/main.rs} # Phase 2: real subscribe + decode
@@ -90,7 +90,7 @@ git rev. Cached.
rev. Use the same rev as the production `nostrnests/nests` Docker
image's `moq-relay` build, if discoverable; otherwise pin to the
latest commit on `main` at implementation time and document it in a
`cli/hang-interop/REV` file.
`nestsClient/tests/hang-interop/REV` file.
- Build: `cargo build --release -p moq-relay --manifest-path <pinned/checkout>/Cargo.toml`.
- Cache key: pinned rev sha. First run: ~30 s cold build. Warm: < 1 s.
- Config (rendered to a temp file at test setup):
@@ -140,10 +140,10 @@ self-signed cert without populating the system trust store.
### Rust hang sidecar binaries
New cargo workspace at `cli/hang-interop/` in this repo.
New cargo workspace at `nestsClient/tests/hang-interop/` in this repo.
```toml
# cli/hang-interop/Cargo.toml
# nestsClient/tests/hang-interop/Cargo.toml
[workspace]
members = ["hang-listen", "hang-publish", "udp-loss-shim"]
@@ -157,7 +157,7 @@ anyhow = "1"
Three binaries:
#### `hang-listen` — `cli/hang-interop/hang-listen/src/main.rs`
#### `hang-listen` — `nestsClient/tests/hang-interop/hang-listen/src/main.rs`
Args: `--relay-url <https://...>` `--jwt <token>` `--broadcast <path>` `--duration <secs>` `--output-pcm <path-or-->`.
Behaviour:
1. Connect to relay via `web-transport-quinn` with WT-Available-Protocols
@@ -176,7 +176,7 @@ Output format: little-endian Float32 PCM, no header. One channel for
mono, interleaved L/R for stereo (controlled by catalog's
`numberOfChannels`). The Gradle test reads this file or pipe.
#### `hang-publish` — `cli/hang-interop/hang-publish/src/main.rs`
#### `hang-publish` — `nestsClient/tests/hang-interop/hang-publish/src/main.rs`
Args: `--relay-url <...>` `--jwt <token>` `--broadcast <path>` `--freq-hz <int>` `--duration <secs>` `--channels <1|2>`.
Behaviour:
1. Connect, open session, claim broadcast under given path.
@@ -189,7 +189,7 @@ Behaviour:
opus_packet`, push into groups of 5 frames, FIN per group.
4. Run for `duration` seconds, then send `Announce::Ended` and exit.
#### `udp-loss-shim` — `cli/hang-interop/udp-loss-shim/src/main.rs`
#### `udp-loss-shim` — `nestsClient/tests/hang-interop/udp-loss-shim/src/main.rs`
Args: `--listen 127.0.0.1:<port>` `--upstream 127.0.0.1:<port>` `--loss-rate <0..1>`.
Behaviour: standard tokio UDP relay. For each datagram received,
`if rng.gen::<f32>() < loss_rate { drop }` else forward. Used for I9.
@@ -503,15 +503,15 @@ Total: ~5 days. P0 deliverable (1+2+4) is **3 days**.
### Phase 1 — Native moq-relay + Rust sidecars (1 day)
1. Pick `kixelated/moq` rev. Clone to `cli/hang-interop/.cache/moq` or
1. Pick `kixelated/moq` rev. Clone to `nestsClient/tests/hang-interop/.cache/moq` or
reference via `git` Cargo source. Document rev in
`cli/hang-interop/REV`.
2. Write `cli/hang-interop/Cargo.toml` workspace + the three binary
`nestsClient/tests/hang-interop/REV`.
2. Write `nestsClient/tests/hang-interop/Cargo.toml` workspace + the three binary
crates (`hang-listen`, `hang-publish`, `udp-loss-shim`). Verify
`cargo build --release` succeeds end-to-end.
3. Add Gradle task `interopBuildHangSidecars` (in
`nestsClient/build.gradle.kts`) that:
- Runs `cargo build --release` in `cli/hang-interop/`.
- Runs `cargo build --release` in `nestsClient/tests/hang-interop/`.
- Exposes binary paths to JVM tests via `tasks.test { systemProperty(...) }`.
- Caches based on `Cargo.lock` hash.
4. Write `NativeMoqRelayHarness.kt` (subprocess management, JWT mint,
@@ -603,8 +603,8 @@ jobs:
path: |
~/.cargo/registry
~/.cargo/git
cli/hang-interop/target
key: ${{ runner.os }}-cargo-${{ hashFiles('cli/hang-interop/Cargo.lock') }}
nestsClient/tests/hang-interop/target
key: ${{ runner.os }}-cargo-${{ hashFiles('nestsClient/tests/hang-interop/Cargo.lock') }}
- run: ./gradlew :nestsClient:jvmTest -DnestsHangInterop=true
browser-interop:
@@ -619,7 +619,7 @@ jobs:
~/.cargo/registry
~/.cache/ms-playwright
nestsClient-browser-interop/node_modules
key: ${{ runner.os }}-browser-${{ hashFiles('nestsClient-browser-interop/bun.lockb', 'cli/hang-interop/Cargo.lock') }}
key: ${{ runner.os }}-browser-${{ hashFiles('nestsClient-browser-interop/bun.lockb', 'nestsClient/tests/hang-interop/Cargo.lock') }}
- run: ./gradlew :nestsClient:jvmTest -DnestsBrowserInterop=true
```
@@ -640,7 +640,7 @@ Acceptable for PR-level CI.
| Risk | Mitigation |
|---|---|
| `kixelated/moq` HEAD breaks pin | Pin git rev in `cli/hang-interop/Cargo.toml` + REV file. Bump deliberately. |
| `kixelated/moq` HEAD breaks pin | Pin git rev in `nestsClient/tests/hang-interop/Cargo.toml` + REV file. Bump deliberately. |
| moq-relay config schema changes between revs | Pin rev. Document `relay.toml` fields used. Smoke test: boot relay, assert known broadcast lookup works, in `@BeforeAll`. |
| Self-signed cert + Chromium WebTransport rejection | Use `--ignore-certificate-errors-spki-list` (preferred) or `--ignore-certificate-errors` for test-only Chromium instance. |
| JVM Opus encoder/decoder availability | If `:nestsClient` JVM target lacks Opus, vendor `audiopus` JNI or write a small wrapper. Reuse `MediaCodecOpusEncoder`/`Decoder` if a JVM `MediaCodec` polyfill exists; otherwise pure-Java `concentus` library is a fallback. Decide at Phase 1. |
@@ -664,7 +664,7 @@ Acceptable for PR-level CI.
committed at `nestsClient/plans/2026-05-06-cross-stack-interop-test-gap-matrix.md`.
6. Both `-DnestsHangInterop=true` and `-DnestsBrowserInterop=true`
in the default PR-level GitHub Actions config.
7. `cli/hang-interop/REV` and `nestsClient-browser-interop/REV`
7. `nestsClient/tests/hang-interop/REV` and `nestsClient-browser-interop/REV`
document the pinned upstream revs.
8. New plan filed at
`nestsClient/plans/2026-05-06-cross-stack-interop-test-results.md`
@@ -192,14 +192,14 @@ FFT assertion on each. ZCR can be done the same way.
### `hang-listen` already supports stereo
Verify by reading the existing
`cli/hang-interop/hang-listen/src/main.rs`: it already passes
`nestsClient/tests/hang-interop/hang-listen/src/main.rs`: it already passes
`audio_cfg.channel_count` into `opus::Decoder::new(...)` and
allocates a stereo PCM buffer if the catalog says so. No Rust
change needed.
### `hang-publish` already supports stereo
Verify by reading `cli/hang-interop/hang-publish/src/main.rs`:
Verify by reading `nestsClient/tests/hang-interop/hang-publish/src/main.rs`:
the `--channels <1|2>` flag plumbs through to the catalog, the
opus encoder, and the sine generator. No Rust change needed.
*(Note: the current sine generator uses the same frequency on
@@ -104,7 +104,7 @@ nestsClient-browser-interop/
Pin all `@moq/*` deps to the same versions `nostrnests/nests`
ships in `NestsUI-v2/package.json`. Document the rev in
`nestsClient-browser-interop/REV` (parallel to
`cli/hang-interop/REV`).
`nestsClient/tests/hang-interop/REV`).
### 2. `listen.ts` — browser listener
@@ -36,7 +36,7 @@ import kotlin.concurrent.withLock
* binary paths for the cross-stack interop harness.
*
* - `moq-relay` is `cargo install`ed at the version pinned in
* `cli/hang-interop/REV` and cached under
* `nestsClient/tests/hang-interop/REV` and cached under
* `~/.cache/amethyst-nests-interop/hang-interop-cargo/bin/`.
* - TLS: `--tls-generate localhost` so the relay self-signs at
* startup. Kotlin clients use the existing
@@ -95,7 +95,7 @@ class NativeMoqRelayHarness private constructor(
/**
* `interopBuildHangSidecars` writes this. Points to
* `cli/hang-interop/target/release` where the (Phase-1
* `nestsClient/tests/hang-interop/target/release` where the (Phase-1
* stub) sidecar binaries live.
*/
const val SIDECARS_DIR_PROPERTY = "nestsHangInteropSidecarsDir"