diff --git a/apps/tui/src/main/kotlin/com/correx/apps/tui/TuiApp.kt b/apps/tui/src/main/kotlin/com/correx/apps/tui/TuiApp.kt index 642ef80b..ccb730fe 100644 --- a/apps/tui/src/main/kotlin/com/correx/apps/tui/TuiApp.kt +++ b/apps/tui/src/main/kotlin/com/correx/apps/tui/TuiApp.kt @@ -61,6 +61,10 @@ fun main(args: Array) { } } + // IMPORTANT: ws.messages and ws.connection are single-consumer cold flows + // (Channel.UNLIMITED.receiveAsFlow). Do not add a second collector for either — + // events would be split across collectors and dropped from the dispatcher's view. + // If a second consumer is needed (e.g. metrics), share via a downstream broadcast. effectScope.launch { ws.messages.collect { msg -> runner.runOnRenderThread { dispatch(Action.ServerEventReceived(msg)) } diff --git a/apps/tui/src/test/kotlin/com/correx/apps/tui/SingleConsumerContractTest.kt b/apps/tui/src/test/kotlin/com/correx/apps/tui/SingleConsumerContractTest.kt new file mode 100644 index 00000000..5a34a597 --- /dev/null +++ b/apps/tui/src/test/kotlin/com/correx/apps/tui/SingleConsumerContractTest.kt @@ -0,0 +1,29 @@ +package com.correx.apps.tui + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import java.io.File + +class SingleConsumerContractTest { + + private val sourceRoot = File("src/main/kotlin") + + private fun countOccurrences(literal: String): Int = + sourceRoot.walkTopDown() + .filter { it.isFile && it.extension == "kt" } + .sumOf { file -> + file.readText().split(literal).size - 1 + } + + @Test + fun `ws messages has exactly one collector`() { + assertEquals(1, countOccurrences("ws.messages.collect"), + "Expected exactly one ws.messages.collect in src/main/kotlin") + } + + @Test + fun `ws connection has exactly one collector`() { + assertEquals(1, countOccurrences("ws.connection.collect"), + "Expected exactly one ws.connection.collect in src/main/kotlin") + } +}