feat(nests): T16 Phase 1 — cross-stack interop harness scaffolding
Lands the load-bearing infra for the hang/Rust cross-stack interop
test plan (nestsClient/plans/2026-05-06-cross-stack-interop-test.md):
the cargo workspace, Gradle wiring to install moq-relay /
moq-token-cli + build sidecars, the Kotlin harness that boots a
real moq-relay subprocess on a random ephemeral UDP port with
self-signed TLS and unrestricted public auth, plus the signal-domain
PCM assertion library and deterministic sine-wave AudioCapture that
Phase 2 scenarios will assert against.
Phase-1 deviations from the spec are summarised in
nestsClient/plans/2026-05-06-cross-stack-interop-test-results.md
— notably: --auth-public "" instead of the JWT minter (no
security-relevant difference for wire-format scenarios), --tls-generate
instead of in-Kotlin cert generation, cargo-install of upstream
binaries instead of an embedded kixelated/moq checkout, and
hang-listen / hang-publish / udp-loss-shim shipped as Phase-1 stubs
that compile + accept --flags but do nothing. Phase 2 fills those
in (and adds JVM Opus encoder/decoder).
Verified green via:
./gradlew :nestsClient:jvmTest \
--tests "com.vitorpamplona.nestsclient.interop.native.*" \
--tests "com.vitorpamplona.nestsclient.audio.PcmAssertionsTest" \
-DnestsHangInterop=true
Default mode (no flag) cleanly skips the harness-dependent test.
https://claude.ai/code/session_01ERJPUYfdLPwZ99pr5EcEcV
This commit is contained in:
@@ -104,4 +104,109 @@ tasks.withType<Test>().configureEach {
|
||||
System.getProperty("nestsProd")?.let { systemProperty("nestsProd", it) }
|
||||
System.getProperty("nestsProdEndpoint")?.let { systemProperty("nestsProdEndpoint", it) }
|
||||
System.getProperty("nestsProdAuth")?.let { systemProperty("nestsProdAuth", it) }
|
||||
// Cross-stack interop (Hang/Rust) opt-in. Forwarded the same way as
|
||||
// -DnestsInterop. See nestsClient/plans/2026-05-06-cross-stack-interop-test.md.
|
||||
System.getProperty("nestsHangInterop")?.let { systemProperty("nestsHangInterop", it) }
|
||||
}
|
||||
|
||||
// ---- Cross-stack interop: Rust sidecar build + binary path forwarding -------
|
||||
//
|
||||
// Phase 1 of the interop plan ships the workspace at `cli/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
|
||||
// `cargo install`, caching everything under
|
||||
// `~/.cache/amethyst-nests-interop/hang-interop-cargo/` so reruns are
|
||||
// fast. Binary paths are forwarded to test workers as system properties.
|
||||
//
|
||||
// Opt-in only: Phase 1 just verifies the harness can boot a relay; the
|
||||
// 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`.
|
||||
|
||||
val hangInteropDir = rootProject.layout.projectDirectory.dir("cli/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
|
||||
// at configuration time; bumping requires touching both files.
|
||||
val moqRelayVersion = "0.10.25"
|
||||
val moqTokenCliVersion = "0.5.23"
|
||||
|
||||
val interopInstallMoqRelay by tasks.registering(Exec::class) {
|
||||
description = "cargo install moq-relay $moqRelayVersion (interop)"
|
||||
group = "interop"
|
||||
commandLine(
|
||||
"cargo", "install",
|
||||
"moq-relay",
|
||||
"--version", moqRelayVersion,
|
||||
"--root", hangInteropCacheDir.asFile.absolutePath,
|
||||
"--locked",
|
||||
)
|
||||
val installed =
|
||||
hangInteropCacheDir.dir("bin").file(
|
||||
if (org.gradle.internal.os.OperatingSystem.current().isWindows) "moq-relay.exe" else "moq-relay",
|
||||
)
|
||||
outputs.file(installed)
|
||||
outputs.cacheIf { true }
|
||||
onlyIf { !installed.asFile.exists() }
|
||||
doFirst { hangInteropCacheDir.asFile.mkdirs() }
|
||||
}
|
||||
|
||||
val interopInstallMoqTokenCli by tasks.registering(Exec::class) {
|
||||
description = "cargo install moq-token-cli $moqTokenCliVersion (interop)"
|
||||
group = "interop"
|
||||
commandLine(
|
||||
"cargo", "install",
|
||||
"moq-token-cli",
|
||||
"--version", moqTokenCliVersion,
|
||||
"--root", hangInteropCacheDir.asFile.absolutePath,
|
||||
"--locked",
|
||||
)
|
||||
val installed =
|
||||
hangInteropCacheDir.dir("bin").file(
|
||||
if (org.gradle.internal.os.OperatingSystem.current().isWindows) "moq-token-cli.exe" else "moq-token-cli",
|
||||
)
|
||||
outputs.file(installed)
|
||||
outputs.cacheIf { true }
|
||||
onlyIf { !installed.asFile.exists() }
|
||||
doFirst { hangInteropCacheDir.asFile.mkdirs() }
|
||||
}
|
||||
|
||||
val interopBuildSidecars by tasks.registering(Exec::class) {
|
||||
description = "cargo build --release for cli/hang-interop sidecars"
|
||||
group = "interop"
|
||||
workingDir = hangInteropDir.asFile
|
||||
commandLine("cargo", "build", "--release")
|
||||
// Track only manifests + sources; the `target/` subtree is the
|
||||
// output, including it as an input would mark the task always
|
||||
// out-of-date.
|
||||
val sidecarSources =
|
||||
fileTree(hangInteropDir.asFile) {
|
||||
include("Cargo.toml", "Cargo.lock")
|
||||
include("hang-listen/**", "hang-publish/**", "udp-loss-shim/**")
|
||||
exclude("**/target/**")
|
||||
}
|
||||
inputs.files(sidecarSources)
|
||||
outputs.dir(hangInteropDir.dir("target/release"))
|
||||
}
|
||||
|
||||
val interopBuildHangSidecars by tasks.registering {
|
||||
description = "Build all hang-interop binaries (sidecars + moq-relay + moq-token)."
|
||||
group = "interop"
|
||||
dependsOn(interopBuildSidecars, interopInstallMoqRelay, interopInstallMoqTokenCli)
|
||||
}
|
||||
|
||||
tasks.withType<Test>().configureEach {
|
||||
val isHangInterop = System.getProperty("nestsHangInterop") == "true"
|
||||
if (isHangInterop) {
|
||||
dependsOn(interopBuildHangSidecars)
|
||||
}
|
||||
val sidecarRelease = hangInteropDir.dir("target/release").asFile
|
||||
val cargoBin = hangInteropCacheDir.dir("bin").asFile
|
||||
systemProperty("nestsHangInteropSidecarsDir", sidecarRelease.absolutePath)
|
||||
systemProperty("nestsHangInteropCargoBinDir", cargoBin.absolutePath)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user