From 7e67c4655fa9d1f8440814319695994c8ab0d44d Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 26 Apr 2026 19:30:25 +0000 Subject: [PATCH] fix(audio-rooms): share Docker harness across all interop test classes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Each interop test class was running its own NostrNestsHarness.start() in @BeforeClass and harnessOrNull?.close() in @AfterClass — meaning the Docker stack tore down + spun back up between every class. That sequence was both slow (~30 s Cargo build for moq-relay each time) and unreliable: leftover network state from the prior `down -v` was racing the next `up -d`, leaving moq-auth either unreachable (SocketException) or producing truncated 401 responses (EOFException on body.string()) for tests that ran after the first. Symptoms before this fix (first class wins; everything else fails): - NostrNestsAuthEndpointsInteropTest ✅ - NostrNestsAuthInteropTest → SocketException ❌ - NostrNestsAuthFailureInteropTest → EOFException ❌ - NostrNestsRoundTripInteropTest → "Failed to reach" ❌ - NostrNestsMultiPeerInteropTest → "Failed to reach" ❌ Fix: NostrNestsHarness.shared() returns a process-singleton. First caller does the docker compose up + port-probe; every subsequent caller reuses the same containers. Teardown is registered once via Runtime.addShutdownHook so the stack lives for the JVM's lifetime and dies cleanly at test process exit. All five test classes now call shared() in @BeforeClass; the @AfterClass blocks no longer call close() on the singleton (clearing the local reference is enough — the shutdown hook handles the real teardown). The original start() entry point is preserved for callers that want a per-call harness. --- .../NostrNestsAuthEndpointsInteropTest.kt | 3 +- .../NostrNestsAuthFailureInteropTest.kt | 3 +- .../interop/NostrNestsAuthInteropTest.kt | 3 +- .../nestsclient/interop/NostrNestsHarness.kt | 41 ++++++++++++++++++- .../interop/NostrNestsMultiPeerInteropTest.kt | 3 +- .../interop/NostrNestsRoundTripInteropTest.kt | 3 +- 6 files changed, 45 insertions(+), 11 deletions(-) diff --git a/nestsClient/src/jvmTest/kotlin/com/vitorpamplona/nestsclient/interop/NostrNestsAuthEndpointsInteropTest.kt b/nestsClient/src/jvmTest/kotlin/com/vitorpamplona/nestsclient/interop/NostrNestsAuthEndpointsInteropTest.kt index b6ef7f2ae..6e7e9046e 100644 --- a/nestsClient/src/jvmTest/kotlin/com/vitorpamplona/nestsclient/interop/NostrNestsAuthEndpointsInteropTest.kt +++ b/nestsClient/src/jvmTest/kotlin/com/vitorpamplona/nestsclient/interop/NostrNestsAuthEndpointsInteropTest.kt @@ -103,14 +103,13 @@ class NostrNestsAuthEndpointsInteropTest { @JvmStatic fun setUpHarness() { if (NostrNestsHarness.isEnabled()) { - harnessOrNull = NostrNestsHarness.start() + harnessOrNull = NostrNestsHarness.shared() } } @AfterClass @JvmStatic fun tearDownHarness() { - harnessOrNull?.close() harnessOrNull = null } } diff --git a/nestsClient/src/jvmTest/kotlin/com/vitorpamplona/nestsclient/interop/NostrNestsAuthFailureInteropTest.kt b/nestsClient/src/jvmTest/kotlin/com/vitorpamplona/nestsclient/interop/NostrNestsAuthFailureInteropTest.kt index 64942a509..9a82180b7 100644 --- a/nestsClient/src/jvmTest/kotlin/com/vitorpamplona/nestsclient/interop/NostrNestsAuthFailureInteropTest.kt +++ b/nestsClient/src/jvmTest/kotlin/com/vitorpamplona/nestsclient/interop/NostrNestsAuthFailureInteropTest.kt @@ -217,14 +217,13 @@ class NostrNestsAuthFailureInteropTest { @JvmStatic fun setUpHarness() { if (NostrNestsHarness.isEnabled()) { - harnessOrNull = NostrNestsHarness.start() + harnessOrNull = NostrNestsHarness.shared() } } @AfterClass @JvmStatic fun tearDownHarness() { - harnessOrNull?.close() harnessOrNull = null } } diff --git a/nestsClient/src/jvmTest/kotlin/com/vitorpamplona/nestsclient/interop/NostrNestsAuthInteropTest.kt b/nestsClient/src/jvmTest/kotlin/com/vitorpamplona/nestsclient/interop/NostrNestsAuthInteropTest.kt index b72f6a065..9eb5390e1 100644 --- a/nestsClient/src/jvmTest/kotlin/com/vitorpamplona/nestsclient/interop/NostrNestsAuthInteropTest.kt +++ b/nestsClient/src/jvmTest/kotlin/com/vitorpamplona/nestsclient/interop/NostrNestsAuthInteropTest.kt @@ -72,14 +72,13 @@ class NostrNestsAuthInteropTest { @JvmStatic fun setUpHarness() { if (NostrNestsHarness.isEnabled()) { - harnessOrNull = NostrNestsHarness.start() + harnessOrNull = NostrNestsHarness.shared() } } @AfterClass @JvmStatic fun tearDownHarness() { - harnessOrNull?.close() harnessOrNull = null } } 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 7b9de6943..0d6f8efe9 100644 --- a/nestsClient/src/jvmTest/kotlin/com/vitorpamplona/nestsclient/interop/NostrNestsHarness.kt +++ b/nestsClient/src/jvmTest/kotlin/com/vitorpamplona/nestsclient/interop/NostrNestsHarness.kt @@ -89,11 +89,50 @@ class NostrNestsHarness private constructor( private const val PORT_READY_TIMEOUT_MS = 90_000L private const val PORT_PROBE_INTERVAL_MS = 500L + /** + * Process-level singleton instance. Once a test class brings the + * stack up, every other test class in the same JVM run shares + * the same containers — bringing the moq-relay Cargo build down + * and back up between every `@BeforeClass` is both slow (~30 s + * compile) and flaky (port-bind / network-create races leave + * the second start in a half-broken state). Container teardown + * is registered once with the JVM as a shutdown hook. + */ + @Volatile private var sharedInstance: NostrNestsHarness? = null + private val sharedLock = Any() + + /** + * Bring the stack up if not already running, returning the + * shared instance for this JVM run. Subsequent callers reuse + * the same containers. Use this in @BeforeClass. + */ + fun shared(): NostrNestsHarness { + sharedInstance?.let { return it } + synchronized(sharedLock) { + sharedInstance?.let { return it } + val instance = doStart() + Runtime + .getRuntime() + .addShutdownHook( + Thread({ + runCatching { instance.close() } + }, "NostrNestsHarness-shutdown"), + ) + sharedInstance = instance + return instance + } + } + /** * Bring the stack up. Caller must `close()` to tear it down — use * `try-with-resources` / Kotlin's `use { … }`. + * + * Most callers should use [shared] instead so the stack is + * reused across test classes. */ - fun start(): NostrNestsHarness { + fun start(): NostrNestsHarness = doStart() + + private fun doStart(): NostrNestsHarness { check(isEnabled()) { "NostrNestsHarness.start called without -D$ENABLE_PROPERTY=true. " + "Call NostrNestsHarness.assumeNestsInterop() first to skip cleanly." diff --git a/nestsClient/src/jvmTest/kotlin/com/vitorpamplona/nestsclient/interop/NostrNestsMultiPeerInteropTest.kt b/nestsClient/src/jvmTest/kotlin/com/vitorpamplona/nestsclient/interop/NostrNestsMultiPeerInteropTest.kt index 72627e5a3..ab33a4779 100644 --- a/nestsClient/src/jvmTest/kotlin/com/vitorpamplona/nestsclient/interop/NostrNestsMultiPeerInteropTest.kt +++ b/nestsClient/src/jvmTest/kotlin/com/vitorpamplona/nestsclient/interop/NostrNestsMultiPeerInteropTest.kt @@ -392,14 +392,13 @@ class NostrNestsMultiPeerInteropTest { @JvmStatic fun setUpHarness() { if (NostrNestsHarness.isEnabled()) { - harnessOrNull = NostrNestsHarness.start() + harnessOrNull = NostrNestsHarness.shared() } } @AfterClass @JvmStatic fun tearDownHarness() { - harnessOrNull?.close() harnessOrNull = null } } diff --git a/nestsClient/src/jvmTest/kotlin/com/vitorpamplona/nestsclient/interop/NostrNestsRoundTripInteropTest.kt b/nestsClient/src/jvmTest/kotlin/com/vitorpamplona/nestsclient/interop/NostrNestsRoundTripInteropTest.kt index 3bf21fd3d..5d5e31723 100644 --- a/nestsClient/src/jvmTest/kotlin/com/vitorpamplona/nestsclient/interop/NostrNestsRoundTripInteropTest.kt +++ b/nestsClient/src/jvmTest/kotlin/com/vitorpamplona/nestsclient/interop/NostrNestsRoundTripInteropTest.kt @@ -268,14 +268,13 @@ class NostrNestsRoundTripInteropTest { @JvmStatic fun setUpHarness() { if (NostrNestsHarness.isEnabled()) { - harnessOrNull = NostrNestsHarness.start() + harnessOrNull = NostrNestsHarness.shared() } } @AfterClass @JvmStatic fun tearDownHarness() { - harnessOrNull?.close() harnessOrNull = null } }