test(audio-rooms): -DnestsInteropExternal bypasses Docker

Adds a "bring your own stack" path to NostrNestsHarness so the interop
tests can run without a Docker daemon. With `-DnestsInteropExternal=true`:

  - Skip the `docker compose up` that builds + boots
    moq-auth + moq-relay
  - Port-probe + /health-check the same 8090 / 4443 endpoints the
    Docker path uses
  - close() is a no-op — the caller owns the lifecycle

Useful in two situations:
  1. Sandboxes / restricted CI without a Docker daemon (just run
     `cargo install moq-relay` + `node moq-auth/dist/index.js`
     yourself, then run gradle with the flag)
  2. Fast iteration — the Docker path takes ~30 s to compile
     moq-relay on first run; with this, you keep both processes
     alive across many test invocations

Forward `nestsInteropExternal` (and `nestsInteropDebug`,
`nestsInteropMoqRev`) through the Test task so the property reaches
test workers; without that, the gate stays off in the worker JVM.

Also: `assertSpeakerReached` / `assertListenerReached` now log a "✘"
checkpoint with the rich state description before calling fail().
JUnit captures the assertion message in a separate section, but the
"standard output" tab is what most people read first when scanning
for a cause — so the chained-cause string now lands in both places.
This commit is contained in:
Claude
2026-04-26 20:23:45 +00:00
parent 1a26b23448
commit d00e406587
3 changed files with 64 additions and 2 deletions
+3
View File
@@ -95,4 +95,7 @@ kotlin {
tasks.withType<Test>().configureEach {
System.getProperty("nestsInterop")?.let { systemProperty("nestsInterop", it) }
System.getProperty("nestsInteropRev")?.let { systemProperty("nestsInteropRev", it) }
System.getProperty("nestsInteropMoqRev")?.let { systemProperty("nestsInteropMoqRev", it) }
System.getProperty("nestsInteropExternal")?.let { systemProperty("nestsInteropExternal", it) }
System.getProperty("nestsInteropDebug")?.let { systemProperty("nestsInteropDebug", it) }
}
@@ -167,6 +167,10 @@ object InteropDebug {
else -> error("unsupported expectedClass=$expectedClass")
}
if (!ok) {
// Land the rich state dump on stdout too — JUnit captures the
// fail message separately, but the standard-output section is
// what people glance at first when debugging.
checkpoint(scope, "✘ speaker NOT @ $expectedClass — actual=${describe(actual)}")
fail("[$scope] speaker did not reach $expectedClass — was ${describe(actual)}")
}
checkpoint(scope, "speaker @ $expectedClass${describe(actual)}")
@@ -183,6 +187,7 @@ object InteropDebug {
else -> error("unsupported expectedClass=$expectedClass")
}
if (!ok) {
checkpoint(scope, "✘ listener NOT @ $expectedClass — actual=${describe(actual)}")
fail("[$scope] listener did not reach $expectedClass — was ${describe(actual)}")
}
checkpoint(scope, "listener @ $expectedClass${describe(actual)}")
@@ -45,7 +45,7 @@ import java.nio.file.Path
* across as many cases as possible.
*/
class NostrNestsHarness private constructor(
private val workDir: File,
private val workDir: File?,
val authBaseUrl: String,
val moqEndpoint: String,
) : AutoCloseable {
@@ -54,16 +54,37 @@ class NostrNestsHarness private constructor(
override fun close() {
if (stopped) return
stopped = true
runDocker(workDir, "down", "-v", "--remove-orphans")
// External mode: caller owns the lifecycle of moq-auth +
// moq-relay. Nothing for us to tear down.
val dir = workDir ?: return
runDocker(dir, "down", "-v", "--remove-orphans")
}
companion object {
/** System property gate. Tests should call [assumeNestsInterop] first. */
const val ENABLE_PROPERTY = "nestsInterop"
/**
* Bypass-Docker mode. When `-DnestsInteropExternal=true`, the
* harness doesn't try to bring up containers — it assumes the
* caller has already started `moq-auth` on [AUTH_HOST_PORT] and
* `moq-relay` on [MOQ_HOST_PORT] (e.g. via
* `nests/dev-config/start-dev.sh`). Useful on machines without a
* Docker daemon (sandboxes, restricted CI), and for fast
* iteration since each test no longer pays the
* `docker compose up` / `docker compose down` cost.
*
* The harness still port-probes + health-checks both endpoints
* before declaring "ready", so a half-started external stack
* fails the same way a half-started Docker stack would.
*/
const val EXTERNAL_PROPERTY = "nestsInteropExternal"
/** Tests that depend on the harness call this in `@BeforeTest`. */
fun isEnabled(): Boolean = System.getProperty(ENABLE_PROPERTY) == "true"
private fun isExternal(): Boolean = System.getProperty(EXTERNAL_PROPERTY) == "true"
/**
* Pin the nostrnests revision to a known-good SHA. Bump deliberately
* — drift on `main` should not silently change interop expectations.
@@ -138,6 +159,8 @@ class NostrNestsHarness private constructor(
"Call NostrNestsHarness.assumeNestsInterop() first to skip cleanly."
}
if (isExternal()) return startExternal()
val workDir = ensureRepo()
// The compose file `build:`s `./moq` — the upstream moq-rs
// sources — but nostrnests does NOT ship that directory; it
@@ -186,6 +209,37 @@ class NostrNestsHarness private constructor(
)
}
/**
* "External" path: caller has already started moq-auth +
* moq-relay (e.g. via `nests/dev-config/start-dev.sh` or a
* `cargo install moq-relay` build kicked off by hand). We just
* port-probe + health-check, then return a harness whose
* [close] is a no-op.
*
* Skip Docker entirely — useful on machines without a Docker
* daemon, and dramatically faster on developer iteration.
*/
private fun startExternal(): NostrNestsHarness {
try {
waitForPort("127.0.0.1", AUTH_HOST_PORT, PORT_READY_TIMEOUT_MS)
waitForPort("127.0.0.1", MOQ_HOST_PORT, PORT_READY_TIMEOUT_MS)
waitForHealth("http://127.0.0.1:$AUTH_HOST_PORT/health", PORT_READY_TIMEOUT_MS)
} catch (t: Throwable) {
throw IllegalStateException(
"external moq-auth / moq-relay not reachable on 127.0.0.1:" +
"$AUTH_HOST_PORT and 127.0.0.1:$MOQ_HOST_PORT — " +
"did you start the stack? See nests/dev-config/start-dev.sh.\n" +
"Original error: ${t.message}",
t,
)
}
return NostrNestsHarness(
workDir = null,
authBaseUrl = "http://127.0.0.1:$AUTH_HOST_PORT",
moqEndpoint = "https://127.0.0.1:$MOQ_HOST_PORT/",
)
}
/**
* Convenience for `@BeforeTest`: throws JUnit's `AssumptionViolated`
* (via `org.junit.Assume`) when the property isn't set, which JUnit