diff --git a/nestsClient/build.gradle.kts b/nestsClient/build.gradle.kts index 52ee10adc..a83e115f7 100644 --- a/nestsClient/build.gradle.kts +++ b/nestsClient/build.gradle.kts @@ -95,4 +95,7 @@ kotlin { tasks.withType().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) } } diff --git a/nestsClient/src/jvmTest/kotlin/com/vitorpamplona/nestsclient/interop/InteropDebug.kt b/nestsClient/src/jvmTest/kotlin/com/vitorpamplona/nestsclient/interop/InteropDebug.kt index ae440546b..f9c7f55b3 100644 --- a/nestsClient/src/jvmTest/kotlin/com/vitorpamplona/nestsclient/interop/InteropDebug.kt +++ b/nestsClient/src/jvmTest/kotlin/com/vitorpamplona/nestsclient/interop/InteropDebug.kt @@ -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)}") diff --git a/nestsClient/src/jvmTest/kotlin/com/vitorpamplona/nestsclient/interop/NostrNestsHarness.kt b/nestsClient/src/jvmTest/kotlin/com/vitorpamplona/nestsclient/interop/NostrNestsHarness.kt index 3b4683f87..8957dd23a 100644 --- a/nestsClient/src/jvmTest/kotlin/com/vitorpamplona/nestsclient/interop/NostrNestsHarness.kt +++ b/nestsClient/src/jvmTest/kotlin/com/vitorpamplona/nestsclient/interop/NostrNestsHarness.kt @@ -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