From 328ff03ed0467231f289c5771f8f3d78909f002a Mon Sep 17 00:00:00 2001 From: Taksh Date: Mon, 5 Oct 2026 16:16:07 +0530 Subject: [PATCH] Cover reconnect delivery and the trim MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The reconnect case fails on main: a peer served once, then sent mail while away, receives nothing when it returns. The other three hold the properties the fix must not break — a reconnect with no new mail still sends nothing, the trim leaves the cap in place rather than emptying it, and a recently delivered message is not the one forgotten. --- .../android/mesh/StoreForwardManagerTest.kt | 124 ++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100644 app/src/test/kotlin/com/bitchat/android/mesh/StoreForwardManagerTest.kt diff --git a/app/src/test/kotlin/com/bitchat/android/mesh/StoreForwardManagerTest.kt b/app/src/test/kotlin/com/bitchat/android/mesh/StoreForwardManagerTest.kt new file mode 100644 index 00000000..9772923f --- /dev/null +++ b/app/src/test/kotlin/com/bitchat/android/mesh/StoreForwardManagerTest.kt @@ -0,0 +1,124 @@ +package com.bitchat.android.mesh + +import com.bitchat.android.protocol.BitchatPacket +import com.bitchat.android.protocol.MessageType +import kotlinx.coroutines.runBlocking +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Test + +/** + * Store-and-forward exists so mail for an offline peer is handed over when it + * comes back. Two things stopped that working. + */ +class StoreForwardManagerTest { + private val manager = StoreForwardManager() + private val delegate = RecordingDelegate() + + init { + manager.delegate = delegate + } + + @After + fun tearDown() = manager.shutdown() + + @Test + fun `mail cached after a peer was served is delivered on the next reconnect`() = runBlocking { + // First contact: nothing held, peer is marked as served. + manager.sendCachedMessages(PEER) + waitForDelivery() + assertEquals(0, delegate.sent.size) + + // Peer goes away; a message is queued for it. + manager.cacheMessage(privateMessage("m-1"), "m-1") + + // It comes back. Before this fix the send was refused outright, + // because nothing ever removed the peer from the already-sent latch, + // and the message sat in the cache until it aged out. + manager.sendCachedMessages(PEER) + waitForDelivery() + + assertEquals(1, delegate.sent.size) + } + + @Test + fun `a peer with nothing new is still not re-sent the same batch`() { + // The latch has to keep working, or every reconnect replays whatever + // is still cached. + manager.cacheMessage(privateMessage("m-1"), "m-1") + manager.sendCachedMessages(PEER) + waitForDelivery() + val afterFirst = delegate.sent.size + + manager.sendCachedMessages(PEER) + waitForDelivery() + + assertEquals("A second connect with no new mail must send nothing more", afterFirst, delegate.sent.size) + } + + @Test + fun `trimming delivery state drops the oldest rather than all of it`() { + repeat(StoreForwardManager.MAX_DELIVERED_MESSAGE_IDS + 50) { index -> + manager.markMessageAsDelivered("delivered-$index") + } + + manager.forceCleanup() + + // Wiping the set bounded memory by forgetting which messages had + // already been handed over. + val debug = manager.getDebugInfo() + assertTrue( + "Delivery state must be trimmed to the cap, not emptied: $debug", + debug.contains("Delivered Messages: ${StoreForwardManager.MAX_DELIVERED_MESSAGE_IDS}") + ) + } + + @Test + fun `the newest delivery records are the ones kept`() { + repeat(StoreForwardManager.MAX_DELIVERED_MESSAGE_IDS + 10) { index -> + manager.markMessageAsDelivered("delivered-$index") + } + manager.forceCleanup() + + // A message delivered a moment ago must not be the one forgotten. + manager.cacheMessage(privateMessage("delivered-${StoreForwardManager.MAX_DELIVERED_MESSAGE_IDS + 9}"), + "delivered-${StoreForwardManager.MAX_DELIVERED_MESSAGE_IDS + 9}") + manager.sendCachedMessages(PEER) + waitForDelivery() + + assertEquals("An already-delivered message must not be queued again", 0, delegate.sent.size) + } + + private fun waitForDelivery() { + // sendCachedMessages dispatches on its own scope with a 10ms/message + // stagger; this is well clear of the single-message case under test. + Thread.sleep(300) + } + + private fun privateMessage(id: String): BitchatPacket = BitchatPacket( + version = 1u, + type = MessageType.MESSAGE.value, + senderID = "1111222233334444".hexToBytes(), + recipientID = PEER.toByteArray(), + timestamp = 1u, + payload = id.toByteArray(), + ttl = 7u + ) + + private class RecordingDelegate : StoreForwardManagerDelegate { + val sent = mutableListOf() + override fun isFavorite(peerID: String) = false + override fun isPeerOnline(peerID: String) = false + override fun sendPacket(packet: BitchatPacket) { + synchronized(sent) { sent.add(packet) } + } + } + + private fun String.hexToBytes(): ByteArray = + chunked(2).map { it.toInt(16).toByte() }.toByteArray() + + private companion object { + const val PEER = "aaaabbbbccccdddd" + } +}