From 36edbb146e4ee1f6cc2f3618d2877ed69bb5bb8a Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 1 Sep 2026 01:06:17 +0000 Subject: [PATCH] test: run the Blossom flow test on real dispatchers, not runTest virtual time BlossomServerListState.flow hops through real Dispatchers.IO (flowOn) into a stateIn collector. Under runTest both the awaiting coroutine and the stateIn scope sit on the virtual-time scheduler, and the IO handoff can park while that scheduler is idle - runTest then aborts with UncompletedCoroutinesError, which is exactly how the previous hardening (await-the-flow-first) failed on the Linux DEB CI job. runBlocking with a private cancellable scope keeps every dispatcher real, and withTimeout bounds a genuine hang with a clear error instead. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_011S1vbFWVVAMFDT8PTgdibV --- .../cache/DesktopBlossomServerListTest.kt | 41 +++++++++++++------ 1 file changed, 29 insertions(+), 12 deletions(-) diff --git a/desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/cache/DesktopBlossomServerListTest.kt b/desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/cache/DesktopBlossomServerListTest.kt index ce7cfe48c7..625ec2f2ef 100644 --- a/desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/cache/DesktopBlossomServerListTest.kt +++ b/desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/cache/DesktopBlossomServerListTest.kt @@ -25,8 +25,13 @@ import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal import com.vitorpamplona.quartz.nipB7Blossom.BlossomServersEvent +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.first +import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.withTimeout import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertNotNull @@ -79,24 +84,36 @@ class DesktopBlossomServerListTest { @Test fun `BlossomServerListState surfaces the servers from the cached event`() = - runTest { + // Deliberately runBlocking, not runTest: BlossomServerListState.flow hops through + // real Dispatchers.IO (flowOn) into a stateIn collector. Under runTest the awaiting + // coroutine and the stateIn scope sit on the virtual-time scheduler, and the IO + // handoff can park while that scheduler is idle — runTest then aborts with + // UncompletedCoroutinesError (seen on CI). Real dispatchers end-to-end make the + // await deterministic; withTimeout keeps a hang from stalling the suite. + runBlocking { val cache = DesktopLocalCache() val signer = NostrSignerInternal(KeyPair()) val servers = listOf("https://blossom.example.com") val event = signedServerList(servers, signer) cache.consume(event, relayUrl) - val state = - BlossomServerListState( - signer = signer, - cache = cache, - scope = backgroundScope, - ) + val scope = CoroutineScope(SupervisorJob()) + try { + val state = + BlossomServerListState( + signer = signer, + cache = cache, + scope = scope, + ) - // Await the IO-backed stateIn subscription first: the flow settling proves the - // state finished wiring, after which the synchronous getter must agree. Asserting - // the getter before the flow raced the Dispatchers.IO hop on fast CI runners. - assertEquals(servers, state.flow.first { it.isNotEmpty() }) - assertEquals(servers, state.getBlossomServersList()?.servers()) + // Await the IO-backed stateIn subscription first: the flow settling proves the + // state finished wiring, after which the synchronous getter must agree. + withTimeout(30_000) { + assertEquals(servers, state.flow.first { it.isNotEmpty() }) + } + assertEquals(servers, state.getBlossomServersList()?.servers()) + } finally { + scope.cancel() + } } }