From 8f0fd77dae0534b7b9a2f34546e98199e6099cb7 Mon Sep 17 00:00:00 2001 From: Paul Hitt Date: Fri, 11 Sep 2026 12:14:43 -0400 Subject: [PATCH] Guard the second-device call fix where Android CI actually runs it The iPhone test reads this repo's CallManager too, but only when the two checkouts sit side by side under the names it expects, which no CI build directory has, so its Android half returns early there. This is the copy that runs on every Android pipeline. Proven able to fail: against the code before the fix, both checks fail. Co-Authored-By: Claude Opus 5 --- .../messenger/app/SecondDeviceCallTest.kt | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 app/src/test/kotlin/io/flamenet/messenger/app/SecondDeviceCallTest.kt diff --git a/app/src/test/kotlin/io/flamenet/messenger/app/SecondDeviceCallTest.kt b/app/src/test/kotlin/io/flamenet/messenger/app/SecondDeviceCallTest.kt new file mode 100644 index 0000000..f0c8692 --- /dev/null +++ b/app/src/test/kotlin/io/flamenet/messenger/app/SecondDeviceCallTest.kt @@ -0,0 +1,62 @@ +package io.flamenet.messenger.app + +import java.io.File +import kotlin.test.Test +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +/** + * One account on two devices: a ring on the second must not end a call that + * was answered on the first. + * + * `CallManager` needs WebRTC, which the JVM test runtime does not have, so this + * reads the source the way the iPhone's `RingTimesOutTests` does. That iPhone + * test reads this file as well, but only when the two checkouts sit side by + * side under the directory names it expects, which a CI build directory never + * has. This copy is the one that actually runs on every Android pipeline. + */ +class SecondDeviceCallTest { + private val source: String by lazy { + listOf( + "src/main/kotlin/io/flamenet/messenger/app/CallManager.kt", + "app/src/main/kotlin/io/flamenet/messenger/app/CallManager.kt", + ).map(::File).first { it.exists() }.readText() + } + + /** The function starting at [marker], up to the next one. */ + private fun body(marker: String): String { + val start = source.indexOf(marker) + assertTrue(start >= 0, "no $marker in CallManager.kt") + val rest = source.substring(start + marker.length) + val end = rest.indexOf("private fun ").let { if (it < 0) rest.length else it } + return rest.substring(0, end) + } + + @Test + fun `the ring guard releases the ring without telling the caller`() { + val guard = body("private fun armRingGuard()") + assertTrue(guard.contains("notifyPeer = false"), "the guard must not message the caller") + // Bounded to the guard: the connect guard below it still, correctly, + // hangs up with notifyPeer = true. + assertFalse( + guard.contains("notifyPeer = true"), + "a timeout sent from a second device hangs up a call answered on the first", + ) + } + + @Test + fun `an end hangs up only its own call, and not once that call has connected`() { + val start = source.indexOf("Content.CallSignal.END ->") + assertTrue(start >= 0, "no end branch in onSignal") + val branch = source.substring(start, minOf(source.length, start + 2000)) + assertTrue( + branch.contains("incoming.callId != current?.callId"), + "an end for another call must not hang this one up", + ) + assertTrue(branch.contains("setOf(\"declined\", \"busy\", \"timeout\")")) + assertTrue( + branch.contains("state == State.CONNECTING || state == State.ACTIVE"), + "only a call that has connected may ignore them; a ringing one must still stop", + ) + } +}