diff --git a/nestsClient/src/commonTest/kotlin/com/vitorpamplona/nestsclient/ReconnectingNestsSpeakerTest.kt b/nestsClient/src/commonTest/kotlin/com/vitorpamplona/nestsclient/ReconnectingNestsSpeakerTest.kt index e105f4a02..debd337ee 100644 --- a/nestsClient/src/commonTest/kotlin/com/vitorpamplona/nestsclient/ReconnectingNestsSpeakerTest.kt +++ b/nestsClient/src/commonTest/kotlin/com/vitorpamplona/nestsclient/ReconnectingNestsSpeakerTest.kt @@ -124,12 +124,27 @@ class ReconnectingNestsSpeakerTest { private val _startCount = AtomicInteger(0) val startCount: Int get() = _startCount.get() - val handles = mutableListOf() + + // CopyOnWriteArrayList — the broadcast pump runs on + // Dispatchers.Default while tests read `handles[0]` from the + // runBlocking thread. A plain `mutableListOf` is neither + // thread-safe nor publishes its writes; observed flake on + // Ubuntu CI where the test thread sees startCount==1 (volatile + // read on AtomicInteger) before the subsequent list mutation + // becomes visible. Using a thread-safe list AND ordering the + // list mutation BEFORE the AtomicInteger increment ensures + // that any reader who sees startCount>=N also sees N entries + // already published into `handles`. + val handles: MutableList = + java.util.concurrent.CopyOnWriteArrayList() override suspend fun startBroadcasting(): BroadcastHandle { - _startCount.incrementAndGet() val handle = ScriptedBroadcastHandle() handles += handle + // Increment AFTER appending to `handles` so the volatile + // write on AtomicInteger publishes the list mutation — + // tests poll on startCount and then read handles[index]. + _startCount.incrementAndGet() // Mirror the production speaker's contract: transition // Connected → Broadcasting on startBroadcasting. val current = mutableState.value