feat/decoupling (#4685)

Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
This commit is contained in:
James Rich 2026-03-03 07:15:28 -06:00 committed by GitHub
parent 40244f8337
commit 2c49db8041
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
254 changed files with 5132 additions and 2666 deletions

View file

@ -44,6 +44,7 @@ import org.meshtastic.core.ble.MeshtasticBleConstants.FROMRADIO_CHARACTERISTIC
import org.meshtastic.core.ble.MeshtasticBleConstants.LOGRADIO_CHARACTERISTIC
import org.meshtastic.core.ble.MeshtasticBleConstants.SERVICE_UUID
import org.meshtastic.core.ble.MeshtasticBleConstants.TORADIO_CHARACTERISTIC
import org.meshtastic.core.repository.RadioInterfaceService
import kotlin.time.Duration.Companion.milliseconds
@OptIn(ExperimentalCoroutinesApi::class)

View file

@ -47,6 +47,7 @@ import org.meshtastic.core.ble.MeshtasticBleConstants.FROMRADIO_CHARACTERISTIC
import org.meshtastic.core.ble.MeshtasticBleConstants.LOGRADIO_CHARACTERISTIC
import org.meshtastic.core.ble.MeshtasticBleConstants.SERVICE_UUID
import org.meshtastic.core.ble.MeshtasticBleConstants.TORADIO_CHARACTERISTIC
import org.meshtastic.core.repository.RadioInterfaceService
import kotlin.time.Duration.Companion.milliseconds
@OptIn(ExperimentalCoroutinesApi::class)
@ -662,7 +663,7 @@ class NordicBleInterfaceTest {
advanceUntilIdle()
// Verify handleFromRadio was called directly with the payload
verify(timeout = 2000) { service.handleFromRadio(p = payload) }
verify(timeout = 2000) { service.handleFromRadio(payload) }
nordicInterface.close()
}

View file

@ -20,6 +20,7 @@ import io.mockk.confirmVerified
import io.mockk.mockk
import io.mockk.verify
import org.junit.Test
import org.meshtastic.core.repository.RadioInterfaceService
class StreamInterfaceTest {

View file

@ -17,10 +17,10 @@
package com.geeksville.mesh.service
import android.app.Notification
import com.geeksville.mesh.repository.radio.RadioInterfaceService
import io.mockk.mockk
import org.meshtastic.core.database.entity.NodeEntity
import org.meshtastic.core.service.MeshServiceNotifications
import org.meshtastic.core.model.Node
import org.meshtastic.core.repository.MeshServiceNotifications
import org.meshtastic.core.repository.RadioInterfaceService
import org.meshtastic.proto.ClientNotification
import org.meshtastic.proto.Telemetry
@ -64,15 +64,15 @@ class FakeMeshServiceNotifications : MeshServiceNotifications {
override fun showAlertNotification(contactKey: String, name: String, alert: String) {}
override fun showNewNodeSeenNotification(node: NodeEntity) {}
override fun showNewNodeSeenNotification(node: Node) {}
override fun showOrUpdateLowBatteryNotification(node: NodeEntity, isRemote: Boolean) {}
override fun showOrUpdateLowBatteryNotification(node: Node, isRemote: Boolean) {}
override fun showClientNotification(clientNotification: ClientNotification) {}
override fun cancelMessageNotification(contactKey: String) {}
override fun cancelLowBatteryNotification(node: NodeEntity) {}
override fun cancelLowBatteryNotification(node: Node) {}
override fun clearClientNotification(notification: ClientNotification) {}
}

View file

@ -1,119 +0,0 @@
/*
* Copyright (c) 2025-2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.service
import io.mockk.mockk
import io.mockk.verify
import org.junit.Before
import org.junit.Test
import org.meshtastic.core.service.MeshServiceNotifications
import org.meshtastic.core.service.ServiceRepository
import org.meshtastic.proto.ClientNotification
import org.meshtastic.proto.Config
import org.meshtastic.proto.DeviceMetadata
import org.meshtastic.proto.FromRadio
import org.meshtastic.proto.MyNodeInfo
import org.meshtastic.proto.NodeInfo
import org.meshtastic.proto.QueueStatus
class FromRadioPacketHandlerTest {
private val serviceRepository: ServiceRepository = mockk(relaxed = true)
private val router: MeshRouter = mockk(relaxed = true)
private val mqttManager: MeshMqttManager = mockk(relaxed = true)
private val packetHandler: PacketHandler = mockk(relaxed = true)
private val serviceNotifications: MeshServiceNotifications = mockk(relaxed = true)
private lateinit var handler: FromRadioPacketHandler
@Before
fun setup() {
handler = FromRadioPacketHandler(serviceRepository, router, mqttManager, packetHandler, serviceNotifications)
}
@Test
fun `handleFromRadio routes MY_INFO to configFlowManager`() {
val myInfo = MyNodeInfo(my_node_num = 1234)
val proto = FromRadio(my_info = myInfo)
handler.handleFromRadio(proto)
verify { router.configFlowManager.handleMyInfo(myInfo) }
}
@Test
fun `handleFromRadio routes METADATA to configFlowManager`() {
val metadata = DeviceMetadata(firmware_version = "v1.0")
val proto = FromRadio(metadata = metadata)
handler.handleFromRadio(proto)
verify { router.configFlowManager.handleLocalMetadata(metadata) }
}
@Test
fun `handleFromRadio routes NODE_INFO to configFlowManager and updates status`() {
val nodeInfo = NodeInfo(num = 1234)
val proto = FromRadio(node_info = nodeInfo)
handler.handleFromRadio(proto)
verify { router.configFlowManager.handleNodeInfo(nodeInfo) }
verify { serviceRepository.setConnectionProgress(any()) }
}
@Test
fun `handleFromRadio routes CONFIG_COMPLETE_ID to configFlowManager`() {
val nonce = 69420
val proto = FromRadio(config_complete_id = nonce)
handler.handleFromRadio(proto)
verify { router.configFlowManager.handleConfigComplete(nonce) }
}
@Test
fun `handleFromRadio routes QUEUESTATUS to packetHandler`() {
val queueStatus = QueueStatus(free = 10)
val proto = FromRadio(queueStatus = queueStatus)
handler.handleFromRadio(proto)
verify { packetHandler.handleQueueStatus(queueStatus) }
}
@Test
fun `handleFromRadio routes CONFIG to configHandler`() {
val config = Config(lora = Config.LoRaConfig(use_preset = true))
val proto = FromRadio(config = config)
handler.handleFromRadio(proto)
verify { router.configHandler.handleDeviceConfig(config) }
}
@Test
fun `handleFromRadio routes CLIENTNOTIFICATION to serviceRepository and notifications`() {
val notification = ClientNotification(message = "test")
val proto = FromRadio(clientNotification = notification)
handler.handleFromRadio(proto)
verify { serviceRepository.setClientNotification(notification) }
verify { serviceNotifications.showClientNotification(notification) }
verify { packetHandler.removeResponse(0, complete = false) }
}
}

View file

@ -1,122 +0,0 @@
/*
* Copyright (c) 2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.service
import io.mockk.every
import io.mockk.mockk
import io.mockk.slot
import io.mockk.verify
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runTest
import okio.ByteString.Companion.toByteString
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.meshtastic.core.data.repository.RadioConfigRepository
import org.meshtastic.core.model.ConnectionState
import org.meshtastic.core.model.DataPacket
import org.meshtastic.proto.Config
import org.meshtastic.proto.LocalConfig
import org.meshtastic.proto.MeshPacket
class MeshCommandSenderHopLimitTest {
private val packetHandler: PacketHandler = mockk(relaxed = true)
private val nodeManager = MeshNodeManager()
private val connectionStateHolder: ConnectionStateHandler = mockk(relaxed = true)
private val radioConfigRepository: RadioConfigRepository = mockk(relaxed = true)
private val localConfigFlow = MutableStateFlow(LocalConfig())
private val testDispatcher = UnconfinedTestDispatcher()
private val testScope = CoroutineScope(testDispatcher)
private lateinit var commandSender: MeshCommandSender
@Before
fun setUp() {
val connectedFlow = MutableStateFlow(ConnectionState.Connected)
every { connectionStateHolder.connectionState } returns connectedFlow
every { radioConfigRepository.localConfigFlow } returns localConfigFlow
commandSender = MeshCommandSender(packetHandler, nodeManager, connectionStateHolder, radioConfigRepository)
commandSender.start(testScope)
nodeManager.myNodeNum = 123
}
@Test
fun `sendData uses default hop limit when config hop limit is zero`() = runTest(testDispatcher) {
val packet =
DataPacket(
to = DataPacket.ID_BROADCAST,
bytes = byteArrayOf(1, 2, 3).toByteString(),
dataType = 1, // PortNum.TEXT_MESSAGE_APP
)
val meshPacketSlot = slot<MeshPacket>()
every { packetHandler.sendToRadio(capture(meshPacketSlot)) } returns Unit
// Ensure localConfig has lora.hop_limit = 0
localConfigFlow.value = LocalConfig(lora = Config.LoRaConfig(hop_limit = 0))
commandSender.sendData(packet)
verify(exactly = 1) { packetHandler.sendToRadio(any<MeshPacket>()) }
val capturedHopLimit = meshPacketSlot.captured.hop_limit ?: 0
assertTrue("Hop limit should be greater than 0, but was $capturedHopLimit", capturedHopLimit > 0)
assertEquals(3, capturedHopLimit)
assertEquals(3, meshPacketSlot.captured.hop_start)
}
@Test
fun `sendData respects non-zero hop limit from config`() = runTest(testDispatcher) {
val packet =
DataPacket(to = DataPacket.ID_BROADCAST, bytes = byteArrayOf(1, 2, 3).toByteString(), dataType = 1)
val meshPacketSlot = slot<MeshPacket>()
every { packetHandler.sendToRadio(capture(meshPacketSlot)) } returns Unit
localConfigFlow.value = LocalConfig(lora = Config.LoRaConfig(hop_limit = 7))
commandSender.sendData(packet)
verify { packetHandler.sendToRadio(any<MeshPacket>()) }
assertEquals(7, meshPacketSlot.captured.hop_limit)
assertEquals(7, meshPacketSlot.captured.hop_start)
}
@Test
fun `requestUserInfo sets hopStart equal to hopLimit`() = runTest(testDispatcher) {
val destNum = 12345
val meshPacketSlot = slot<MeshPacket>()
every { packetHandler.sendToRadio(capture(meshPacketSlot)) } returns Unit
localConfigFlow.value = LocalConfig(lora = Config.LoRaConfig(hop_limit = 6))
// Mock node manager interactions
nodeManager.nodeDBbyNodeNum.remove(destNum)
commandSender.requestUserInfo(destNum)
verify { packetHandler.sendToRadio(any<MeshPacket>()) }
assertEquals("Hop Limit should be 6", 6, meshPacketSlot.captured.hop_limit)
assertEquals("Hop Start should be 6", 6, meshPacketSlot.captured.hop_start)
}
}

View file

@ -1,74 +0,0 @@
/*
* Copyright (c) 2025-2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.service
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotEquals
import org.junit.Before
import org.junit.Test
import org.meshtastic.core.database.entity.NodeEntity
import org.meshtastic.core.model.DataPacket
import org.meshtastic.proto.User
class MeshCommandSenderTest {
private lateinit var commandSender: MeshCommandSender
private lateinit var nodeManager: MeshNodeManager
@Before
fun setUp() {
nodeManager = MeshNodeManager()
commandSender = MeshCommandSender(null, nodeManager, null, null)
}
@Test
fun `generatePacketId produces unique non-zero IDs`() {
val ids = mutableSetOf<Int>()
repeat(1000) {
val id = commandSender.generatePacketId()
assertNotEquals(0, id)
ids.add(id)
}
assertEquals(1000, ids.size)
}
@Test
fun `resolveNodeNum handles broadcast ID`() {
assertEquals(DataPacket.NODENUM_BROADCAST, commandSender.resolveNodeNum(DataPacket.ID_BROADCAST))
}
@Test
fun `resolveNodeNum handles hex ID with exclamation mark`() {
assertEquals(123, commandSender.resolveNodeNum("!0000007b"))
}
@Test
fun `resolveNodeNum handles custom node ID from database`() {
val nodeNum = 456
val userId = "custom_id"
val entity = NodeEntity(num = nodeNum, user = User(id = userId))
nodeManager.nodeDBbyNodeNum[nodeNum] = entity
nodeManager.nodeDBbyID[userId] = entity
assertEquals(nodeNum, commandSender.resolveNodeNum(userId))
}
@Test(expected = IllegalArgumentException::class)
fun `resolveNodeNum throws for unknown ID`() {
commandSender.resolveNodeNum("unknown")
}
}

View file

@ -1,241 +0,0 @@
/*
* Copyright (c) 2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.service
import android.content.Context
import androidx.glance.appwidget.GlanceAppWidget
import androidx.glance.appwidget.updateAll
import androidx.work.ExistingWorkPolicy
import androidx.work.OneTimeWorkRequest
import androidx.work.WorkManager
import com.geeksville.mesh.repository.radio.RadioInterfaceService
import io.mockk.coEvery
import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkStatic
import io.mockk.unmockkStatic
import io.mockk.verify
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runTest
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
import org.meshtastic.core.analytics.platform.PlatformAnalytics
import org.meshtastic.core.data.repository.NodeRepository
import org.meshtastic.core.data.repository.PacketRepository
import org.meshtastic.core.data.repository.RadioConfigRepository
import org.meshtastic.core.database.entity.MyNodeEntity
import org.meshtastic.core.database.model.Node
import org.meshtastic.core.model.ConnectionState
import org.meshtastic.core.model.DataPacket
import org.meshtastic.core.prefs.ui.UiPrefs
import org.meshtastic.core.service.MeshServiceNotifications
import org.meshtastic.feature.messaging.domain.worker.SendMessageWorker
import org.meshtastic.proto.Config
import org.meshtastic.proto.LocalConfig
import org.meshtastic.proto.LocalModuleConfig
import org.meshtastic.proto.LocalStats
import org.meshtastic.proto.ModuleConfig
import org.meshtastic.proto.ToRadio
class MeshConnectionManagerTest {
private val context: Context = mockk(relaxed = true)
private val radioInterfaceService: RadioInterfaceService = mockk(relaxed = true)
private val connectionStateHolder = ConnectionStateHandler()
private val serviceBroadcasts: MeshServiceBroadcasts = mockk(relaxed = true)
private val serviceNotifications: MeshServiceNotifications = mockk(relaxed = true)
private val uiPrefs: UiPrefs = mockk(relaxed = true)
private val packetHandler: PacketHandler = mockk(relaxed = true)
private val nodeRepository: NodeRepository = mockk(relaxed = true)
private val locationManager: MeshLocationManager = mockk(relaxed = true)
private val mqttManager: MeshMqttManager = mockk(relaxed = true)
private val historyManager: MeshHistoryManager = mockk(relaxed = true)
private val radioConfigRepository: RadioConfigRepository = mockk(relaxed = true)
private val commandSender: MeshCommandSender = mockk(relaxed = true)
private val nodeManager: MeshNodeManager = mockk(relaxed = true)
private val analytics: PlatformAnalytics = mockk(relaxed = true)
private val packetRepository: PacketRepository = mockk(relaxed = true)
private val workManager: WorkManager = mockk(relaxed = true)
private val radioConnectionState = MutableStateFlow<ConnectionState>(ConnectionState.Disconnected)
private val localConfigFlow = MutableStateFlow(LocalConfig())
private val moduleConfigFlow = MutableStateFlow(LocalModuleConfig())
private val testDispatcher = UnconfinedTestDispatcher()
private lateinit var manager: MeshConnectionManager
@Before
fun setUp() {
mockkStatic("org.jetbrains.compose.resources.StringResourcesKt")
mockkStatic("androidx.glance.appwidget.GlanceAppWidgetKt")
coEvery { org.jetbrains.compose.resources.getString(any()) } returns "Mocked String"
coEvery { org.jetbrains.compose.resources.getString(any(), *anyVararg()) } returns "Mocked String"
coEvery { any<GlanceAppWidget>().updateAll(any()) } returns Unit
every { radioInterfaceService.connectionState } returns radioConnectionState
every { radioConfigRepository.localConfigFlow } returns localConfigFlow
every { radioConfigRepository.moduleConfigFlow } returns moduleConfigFlow
every { nodeRepository.myNodeInfo } returns MutableStateFlow<MyNodeEntity?>(null)
every { nodeRepository.ourNodeInfo } returns MutableStateFlow<Node?>(null)
every { nodeRepository.localStats } returns MutableStateFlow(LocalStats())
manager =
MeshConnectionManager(
context,
radioInterfaceService,
connectionStateHolder,
serviceBroadcasts,
serviceNotifications,
uiPrefs,
packetHandler,
nodeRepository,
locationManager,
mqttManager,
historyManager,
radioConfigRepository,
commandSender,
nodeManager,
analytics,
packetRepository,
workManager,
)
}
@After
fun tearDown() {
unmockkStatic("org.jetbrains.compose.resources.StringResourcesKt")
unmockkStatic("androidx.glance.appwidget.GlanceAppWidgetKt")
}
@Test
fun `Connected state triggers broadcast and config start`() = runTest(testDispatcher) {
manager.start(backgroundScope)
radioConnectionState.value = ConnectionState.Connected
advanceUntilIdle()
assertEquals(
"State should be Connecting after radio Connected",
ConnectionState.Connecting,
connectionStateHolder.connectionState.value,
)
verify { serviceBroadcasts.broadcastConnection() }
verify { packetHandler.sendToRadio(any<ToRadio>()) }
}
@Test
fun `Disconnected state stops services`() = runTest(testDispatcher) {
manager.start(backgroundScope)
// Transition to Connected first so that Disconnected actually does something
radioConnectionState.value = ConnectionState.Connected
advanceUntilIdle()
radioConnectionState.value = ConnectionState.Disconnected
advanceUntilIdle()
assertEquals(
"State should be Disconnected after radio Disconnected",
ConnectionState.Disconnected,
connectionStateHolder.connectionState.value,
)
verify { packetHandler.stopPacketQueue() }
verify { locationManager.stop() }
verify { mqttManager.stop() }
}
@Test
fun `DeviceSleep behavior when power saving is off maps to Disconnected`() = runTest(testDispatcher) {
// Power saving disabled + Role CLIENT
val config =
LocalConfig(
power = Config.PowerConfig(is_power_saving = false),
device = Config.DeviceConfig(role = Config.DeviceConfig.Role.CLIENT),
)
every { radioConfigRepository.localConfigFlow } returns flowOf(config)
manager.start(backgroundScope)
advanceUntilIdle()
radioConnectionState.value = ConnectionState.DeviceSleep
advanceUntilIdle()
assertEquals(
"State should be Disconnected when power saving is off",
ConnectionState.Disconnected,
connectionStateHolder.connectionState.value,
)
}
@Test
fun `DeviceSleep behavior when power saving is on stays in DeviceSleep`() = runTest(testDispatcher) {
// Power saving enabled
val config = LocalConfig(power = Config.PowerConfig(is_power_saving = true))
every { radioConfigRepository.localConfigFlow } returns flowOf(config)
manager.start(backgroundScope)
advanceUntilIdle()
radioConnectionState.value = ConnectionState.DeviceSleep
advanceUntilIdle()
assertEquals(
"State should stay in DeviceSleep when power saving is on",
ConnectionState.DeviceSleep,
connectionStateHolder.connectionState.value,
)
}
@Test
fun `onRadioConfigLoaded enqueues queued packets and sets time`() = runTest(testDispatcher) {
manager.start(backgroundScope)
val packetId = 456
val dataPacket = mockk<DataPacket>(relaxed = true)
every { dataPacket.id } returns packetId
coEvery { packetRepository.getQueuedPackets() } returns listOf(dataPacket)
manager.onRadioConfigLoaded()
advanceUntilIdle()
verify {
workManager.enqueueUniqueWork(
match<String> { it.startsWith(SendMessageWorker.WORK_NAME_PREFIX) },
any<ExistingWorkPolicy>(),
any<OneTimeWorkRequest>(),
)
}
verify { commandSender.sendAdmin(any(), initFn = any()) }
}
@Test
fun `onNodeDbReady starts MQTT and requests history`() = runTest(testDispatcher) {
val moduleConfig = mockk<LocalModuleConfig>(relaxed = true)
every { moduleConfig.mqtt } returns ModuleConfig.MQTTConfig(enabled = true)
every { moduleConfig.store_forward } returns ModuleConfig.StoreForwardConfig(enabled = true)
moduleConfigFlow.value = moduleConfig
manager.start(backgroundScope)
manager.onNodeDbReady()
advanceUntilIdle()
verify { mqttManager.start(any(), true, any()) }
verify { historyManager.requestHistoryReplay("onNodeDbReady", any(), any(), "Unknown") }
}
}

View file

@ -1,156 +0,0 @@
/*
* Copyright (c) 2025-2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.service
import dagger.Lazy
import io.mockk.coVerify
import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkStatic
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runTest
import okio.ByteString.Companion.toByteString
import org.junit.Before
import org.junit.Test
import org.meshtastic.core.analytics.platform.PlatformAnalytics
import org.meshtastic.core.data.repository.PacketRepository
import org.meshtastic.core.data.repository.RadioConfigRepository
import org.meshtastic.core.model.DataPacket
import org.meshtastic.core.model.MessageStatus
import org.meshtastic.core.prefs.mesh.MeshPrefs
import org.meshtastic.core.service.MeshServiceNotifications
import org.meshtastic.core.service.ServiceRepository
import org.meshtastic.core.service.filter.MessageFilterService
import org.meshtastic.proto.Data
import org.meshtastic.proto.MeshPacket
import org.meshtastic.proto.PortNum
import org.meshtastic.proto.StoreForwardPlusPlus
class MeshDataHandlerTest {
private val nodeManager: MeshNodeManager = mockk(relaxed = true)
private val packetHandler: PacketHandler = mockk(relaxed = true)
private val serviceRepository: ServiceRepository = mockk(relaxed = true)
private val packetRepository: PacketRepository = mockk(relaxed = true)
private val packetRepositoryLazy: Lazy<PacketRepository> = mockk { every { get() } returns packetRepository }
private val serviceBroadcasts: MeshServiceBroadcasts = mockk(relaxed = true)
private val serviceNotifications: MeshServiceNotifications = mockk(relaxed = true)
private val analytics: PlatformAnalytics = mockk(relaxed = true)
private val dataMapper: MeshDataMapper = mockk(relaxed = true)
private val configHandler: MeshConfigHandler = mockk(relaxed = true)
private val configFlowManager: MeshConfigFlowManager = mockk(relaxed = true)
private val commandSender: MeshCommandSender = mockk(relaxed = true)
private val historyManager: MeshHistoryManager = mockk(relaxed = true)
private val meshPrefs: MeshPrefs = mockk(relaxed = true)
private val connectionManager: MeshConnectionManager = mockk(relaxed = true)
private val tracerouteHandler: MeshTracerouteHandler = mockk(relaxed = true)
private val neighborInfoHandler: MeshNeighborInfoHandler = mockk(relaxed = true)
private val radioConfigRepository: RadioConfigRepository = mockk(relaxed = true)
private val messageFilterService: MessageFilterService = mockk(relaxed = true)
private lateinit var meshDataHandler: MeshDataHandler
@OptIn(ExperimentalCoroutinesApi::class)
@Before
fun setUp() {
mockkStatic(android.util.Log::class)
every { android.util.Log.d(any(), any()) } returns 0
every { android.util.Log.i(any(), any()) } returns 0
every { android.util.Log.w(any(), any<String>()) } returns 0
every { android.util.Log.e(any(), any()) } returns 0
meshDataHandler =
MeshDataHandler(
nodeManager,
packetHandler,
serviceRepository,
packetRepositoryLazy,
serviceBroadcasts,
serviceNotifications,
analytics,
dataMapper,
configHandler,
configFlowManager,
commandSender,
historyManager,
meshPrefs,
connectionManager,
tracerouteHandler,
neighborInfoHandler,
radioConfigRepository,
messageFilterService,
)
// Use UnconfinedTestDispatcher for running coroutines synchronously in tests
meshDataHandler.start(CoroutineScope(UnconfinedTestDispatcher()))
every { nodeManager.myNodeNum } returns 123
every { nodeManager.getMyId() } returns "!0000007b"
// Default behavior for dataMapper to return a valid DataPacket when requested
every { dataMapper.toDataPacket(any()) } answers
{
val packet = firstArg<MeshPacket>()
DataPacket(
to = "to",
channel = 0,
bytes = packet.decoded?.payload,
dataType = packet.decoded?.portnum?.value ?: 0,
id = packet.id,
)
}
}
@Test
fun `handleReceivedData with SFPP LINK_PROVIDE updates SFPP status`() = runTest {
val sfppMessage =
StoreForwardPlusPlus(
sfpp_message_type = StoreForwardPlusPlus.SFPP_message_type.LINK_PROVIDE,
encapsulated_id = 999,
encapsulated_from = 456,
encapsulated_to = 789,
encapsulated_rxtime = 1000,
message = "EncryptedPayload".toByteArray().toByteString(),
message_hash = "Hash".toByteArray().toByteString(),
)
val payload = StoreForwardPlusPlus.ADAPTER.encode(sfppMessage).toByteString()
val meshPacket =
MeshPacket(
from = 456,
to = 123,
decoded = Data(portnum = PortNum.STORE_FORWARD_PLUSPLUS_APP, payload = payload),
id = 1001,
)
meshDataHandler.handleReceivedData(meshPacket, 123)
// SFPP_ROUTING because commit_hash is empty
coVerify {
packetRepository.updateSFPPStatus(
packetId = 999,
from = 456,
to = 789,
hash = any(),
status = MessageStatus.SFPP_ROUTING,
rxTime = 1000L,
myNodeNum = 123,
)
}
}
}

View file

@ -1,119 +0,0 @@
/*
* Copyright (c) 2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.service
import io.mockk.every
import io.mockk.mockk
import okio.ByteString.Companion.toByteString
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Before
import org.junit.Test
import org.meshtastic.core.model.DataPacket
import org.meshtastic.proto.Data
import org.meshtastic.proto.MeshPacket
import org.meshtastic.proto.PortNum
class MeshDataMapperTest {
private val nodeManager: MeshNodeManager = mockk()
private lateinit var mapper: MeshDataMapper
@Before
fun setUp() {
mapper = MeshDataMapper(nodeManager)
}
@Test
fun `toNodeID resolves broadcast correctly`() {
every { nodeManager.toNodeID(DataPacket.NODENUM_BROADCAST) } returns DataPacket.ID_BROADCAST
assertEquals(DataPacket.ID_BROADCAST, mapper.toNodeID(DataPacket.NODENUM_BROADCAST))
}
@Test
fun `toNodeID resolves known node correctly`() {
val nodeNum = 1234
val nodeId = "!1234abcd"
every { nodeManager.toNodeID(nodeNum) } returns nodeId
assertEquals(nodeId, mapper.toNodeID(nodeNum))
}
@Test
fun `toNodeID resolves unknown node to default ID`() {
val nodeNum = 1234
val nodeId = DataPacket.nodeNumToDefaultId(nodeNum)
every { nodeManager.toNodeID(nodeNum) } returns nodeId
assertEquals(nodeId, mapper.toNodeID(nodeNum))
}
@Test
fun `toDataPacket returns null when no decoded data`() {
val packet = MeshPacket()
assertNull(mapper.toDataPacket(packet))
}
@Test
fun `toDataPacket maps basic fields correctly`() {
val nodeNum = 1234
val nodeId = "!1234abcd"
every { nodeManager.toNodeID(nodeNum) } returns nodeId
every { nodeManager.toNodeID(DataPacket.NODENUM_BROADCAST) } returns DataPacket.ID_BROADCAST
val proto =
MeshPacket(
id = 42,
from = nodeNum,
to = DataPacket.NODENUM_BROADCAST,
rx_time = 1600000000,
rx_snr = 5.5f,
rx_rssi = -100,
hop_limit = 3,
hop_start = 3,
decoded =
Data(
portnum = PortNum.TEXT_MESSAGE_APP,
payload = "hello".encodeToByteArray().toByteString(),
reply_id = 123,
),
)
val result = mapper.toDataPacket(proto)
assertNotNull(result)
assertEquals(42, result!!.id)
assertEquals(nodeId, result.from)
assertEquals(DataPacket.ID_BROADCAST, result.to)
assertEquals(1600000000000L, result.time)
assertEquals(5.5f, result.snr)
assertEquals(-100, result.rssi)
assertEquals(PortNum.TEXT_MESSAGE_APP.value, result.dataType)
assertEquals("hello", result.bytes?.utf8())
assertEquals(123, result.replyId)
}
@Test
fun `toDataPacket maps PKC channel correctly for encrypted packets`() {
val proto = MeshPacket(pki_encrypted = true, channel = 1, decoded = Data())
every { nodeManager.toNodeID(any()) } returns "any"
val result = mapper.toDataPacket(proto)
assertEquals(DataPacket.PKC_CHANNEL_INDEX, result!!.channel)
}
}

View file

@ -1,122 +0,0 @@
/*
* Copyright (c) 2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.service
import io.mockk.coVerify
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.meshtastic.core.data.repository.MeshLogRepository
import org.meshtastic.core.database.entity.MeshLog
import org.meshtastic.core.service.ServiceRepository
import org.meshtastic.proto.Data
import org.meshtastic.proto.MeshPacket
import org.meshtastic.proto.PortNum
class MeshMessageProcessorTest {
private val nodeManager: MeshNodeManager = mockk(relaxed = true)
private val serviceRepository: ServiceRepository = mockk(relaxed = true)
private val meshLogRepository: MeshLogRepository = mockk(relaxed = true)
private val router: MeshRouter = mockk(relaxed = true)
private val fromRadioDispatcher: FromRadioPacketHandler = mockk(relaxed = true)
private val meshLogRepositoryLazy = dagger.Lazy { meshLogRepository }
private val dataHandler: MeshDataHandler = mockk(relaxed = true)
private val isNodeDbReady = MutableStateFlow(false)
private val testDispatcher = StandardTestDispatcher()
private val testScope = TestScope(testDispatcher)
private lateinit var processor: MeshMessageProcessor
@Before
fun setUp() {
every { nodeManager.isNodeDbReady } returns isNodeDbReady
every { router.dataHandler } returns dataHandler
processor =
MeshMessageProcessor(nodeManager, serviceRepository, meshLogRepositoryLazy, router, fromRadioDispatcher)
processor.start(testScope)
}
@Test
fun `early packets are buffered and flushed when DB is ready`() = runTest(testDispatcher) {
val packet = MeshPacket(id = 123, decoded = Data(portnum = PortNum.TEXT_MESSAGE_APP))
// 1. Database is NOT ready
isNodeDbReady.value = false
testScheduler.runCurrent() // trigger start() onEach
processor.handleReceivedMeshPacket(packet, 999)
// Verify that handleReceivedData has NOT been called yet
verify(exactly = 0) { dataHandler.handleReceivedData(any(), any(), any(), any()) }
// 2. Database becomes ready
isNodeDbReady.value = true
testScheduler.runCurrent() // trigger onEach(true)
// Verify that handleReceivedData is now called with the buffered packet
verify(exactly = 1) { dataHandler.handleReceivedData(match { it.id == 123 }, any(), any(), any()) }
}
@Test
fun `packets are processed immediately if DB is already ready`() = runTest(testDispatcher) {
val packet = MeshPacket(id = 456, decoded = Data(portnum = PortNum.TEXT_MESSAGE_APP))
isNodeDbReady.value = true
testScheduler.runCurrent()
processor.handleReceivedMeshPacket(packet, 999)
verify(exactly = 1) { dataHandler.handleReceivedData(match { it.id == 456 }, any(), any(), any()) }
}
@Test
fun `packets from local node are logged with NODE_NUM_LOCAL`() = runTest(testDispatcher) {
val myNodeNum = 1234
val packet = MeshPacket(from = myNodeNum, id = 789, decoded = Data(portnum = PortNum.TEXT_MESSAGE_APP))
isNodeDbReady.value = true
testScheduler.runCurrent()
processor.handleReceivedMeshPacket(packet, myNodeNum)
testScheduler.runCurrent() // wait for log insert job
coVerify { meshLogRepository.insert(match { log -> log.fromNum == MeshLog.NODE_NUM_LOCAL }) }
}
@Test
fun `packets from remote nodes are logged with their node number`() = runTest(testDispatcher) {
val myNodeNum = 1234
val remoteNodeNum = 5678
val packet = MeshPacket(from = remoteNodeNum, id = 789, decoded = Data(portnum = PortNum.TEXT_MESSAGE_APP))
isNodeDbReady.value = true
testScheduler.runCurrent()
processor.handleReceivedMeshPacket(packet, myNodeNum)
testScheduler.runCurrent()
coVerify { meshLogRepository.insert(match { log -> log.fromNum == remoteNodeNum }) }
}
}

View file

@ -1,116 +0,0 @@
/*
* Copyright (c) 2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.service
import io.mockk.mockk
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.meshtastic.core.data.repository.NodeRepository
import org.meshtastic.core.model.DataPacket
import org.meshtastic.core.service.MeshServiceNotifications
import org.meshtastic.proto.HardwareModel
import org.meshtastic.proto.Position
import org.meshtastic.proto.User
class MeshNodeManagerTest {
private val nodeRepository: NodeRepository = mockk(relaxed = true)
private val serviceBroadcasts: MeshServiceBroadcasts = mockk(relaxed = true)
private val serviceNotifications: MeshServiceNotifications = mockk(relaxed = true)
private lateinit var nodeManager: MeshNodeManager
@Before
fun setUp() {
nodeManager = MeshNodeManager(nodeRepository, serviceBroadcasts, serviceNotifications)
}
@Test
fun `getOrCreateNodeInfo creates default user for unknown node`() {
val nodeNum = 1234
val result = nodeManager.getOrCreateNodeInfo(nodeNum)
assertNotNull(result)
assertEquals(nodeNum, result.num)
assertTrue(result.user.long_name?.startsWith("Meshtastic") == true)
assertEquals(DataPacket.nodeNumToDefaultId(nodeNum), result.user.id)
}
@Test
fun `handleReceivedUser preserves existing user if incoming is default`() {
val nodeNum = 1234
val existingUser =
User(id = "!12345678", long_name = "My Custom Name", short_name = "MCN", hw_model = HardwareModel.TLORA_V2)
// Setup existing node
nodeManager.updateNodeInfo(nodeNum) { it.user = existingUser }
val incomingDefaultUser =
User(id = "!12345678", long_name = "Meshtastic 5678", short_name = "5678", hw_model = HardwareModel.UNSET)
nodeManager.handleReceivedUser(nodeNum, incomingDefaultUser)
val result = nodeManager.nodeDBbyNodeNum[nodeNum]
assertEquals("My Custom Name", result!!.user.long_name)
assertEquals(HardwareModel.TLORA_V2, result.user.hw_model)
}
@Test
fun `handleReceivedUser updates user if incoming is higher detail`() {
val nodeNum = 1234
val existingUser =
User(id = "!12345678", long_name = "Meshtastic 5678", short_name = "5678", hw_model = HardwareModel.UNSET)
nodeManager.updateNodeInfo(nodeNum) { it.user = existingUser }
val incomingDetailedUser =
User(id = "!12345678", long_name = "Real User", short_name = "RU", hw_model = HardwareModel.TLORA_V1)
nodeManager.handleReceivedUser(nodeNum, incomingDetailedUser)
val result = nodeManager.nodeDBbyNodeNum[nodeNum]
assertEquals("Real User", result!!.user.long_name)
assertEquals(HardwareModel.TLORA_V1, result.user.hw_model)
}
@Test
fun `handleReceivedPosition updates node position`() {
val nodeNum = 1234
val position = Position(latitude_i = 450000000, longitude_i = 900000000)
nodeManager.handleReceivedPosition(nodeNum, 9999, position)
val result = nodeManager.nodeDBbyNodeNum[nodeNum]
assertNotNull(result!!.position)
assertEquals(45.0, result.latitude, 0.0001)
assertEquals(90.0, result.longitude, 0.0001)
}
@Test
fun `clear resets internal state`() {
nodeManager.updateNodeInfo(1234) { it.longName = "Test" }
nodeManager.clear()
assertTrue(nodeManager.nodeDBbyNodeNum.isEmpty())
assertTrue(nodeManager.nodeDBbyID.isEmpty())
assertNull(nodeManager.myNodeNum)
}
}

View file

@ -1,115 +0,0 @@
/*
* Copyright (c) 2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.service
import com.geeksville.mesh.repository.radio.RadioInterfaceService
import io.mockk.coVerify
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.meshtastic.core.data.repository.MeshLogRepository
import org.meshtastic.core.data.repository.PacketRepository
import org.meshtastic.core.database.entity.MeshLog
import org.meshtastic.core.model.ConnectionState
import org.meshtastic.proto.Data
import org.meshtastic.proto.MeshPacket
import org.meshtastic.proto.PortNum
import org.meshtastic.proto.QueueStatus
import org.meshtastic.proto.ToRadio
class PacketHandlerTest {
private val packetRepository: PacketRepository = mockk(relaxed = true)
private val serviceBroadcasts: MeshServiceBroadcasts = mockk(relaxed = true)
private val radioInterfaceService: RadioInterfaceService = mockk(relaxed = true)
private val meshLogRepository: MeshLogRepository = mockk(relaxed = true)
private val connectionStateHolder: ConnectionStateHandler = mockk(relaxed = true)
private val testDispatcher = StandardTestDispatcher()
private val testScope = TestScope(testDispatcher)
private lateinit var handler: PacketHandler
@Before
fun setUp() {
handler =
PacketHandler(
dagger.Lazy { packetRepository },
serviceBroadcasts,
radioInterfaceService,
dagger.Lazy { meshLogRepository },
connectionStateHolder,
)
handler.start(testScope)
}
@Test
fun `sendToRadio with ToRadio sends immediately`() {
val toRadio = ToRadio(packet = MeshPacket(id = 123))
handler.sendToRadio(toRadio)
verify { radioInterfaceService.sendToRadio(any()) }
}
@Test
fun `sendToRadio with MeshPacket queues and sends when connected`() = runTest(testDispatcher) {
val packet = MeshPacket(id = 456)
every { connectionStateHolder.connectionState } returns MutableStateFlow(ConnectionState.Connected)
handler.sendToRadio(packet)
testScheduler.runCurrent()
verify { radioInterfaceService.sendToRadio(any()) }
}
@Test
fun `handleQueueStatus completes deferred`() = runTest(testDispatcher) {
val packet = MeshPacket(id = 789)
every { connectionStateHolder.connectionState } returns MutableStateFlow(ConnectionState.Connected)
handler.sendToRadio(packet)
testScheduler.runCurrent()
val status =
QueueStatus(
mesh_packet_id = 789,
res = 0, // Success
free = 1,
)
handler.handleQueueStatus(status)
testScheduler.runCurrent()
}
@Test
fun `outgoing packets are logged with NODE_NUM_LOCAL`() = runTest(testDispatcher) {
val packet = MeshPacket(id = 123, decoded = Data(portnum = PortNum.TEXT_MESSAGE_APP))
val toRadio = ToRadio(packet = packet)
handler.sendToRadio(toRadio)
testScheduler.runCurrent()
coVerify { meshLogRepository.insert(match { log -> log.fromNum == MeshLog.NODE_NUM_LOCAL }) }
}
}

View file

@ -19,35 +19,36 @@ package com.geeksville.mesh.service
import android.app.Application
import android.content.Context
import androidx.test.core.app.ApplicationProvider
import io.mockk.every
import io.mockk.mockk
import kotlinx.coroutines.flow.MutableStateFlow
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.meshtastic.core.model.ConnectionState
import org.meshtastic.core.service.ServiceRepository
import org.meshtastic.core.repository.ServiceRepository
import org.robolectric.RobolectricTestRunner
import org.robolectric.Shadows.shadowOf
import org.robolectric.annotation.Config
@RunWith(RobolectricTestRunner::class)
@Config(sdk = [34])
class MeshServiceBroadcastsTest {
class ServiceBroadcastsTest {
private lateinit var context: Context
private val connectionStateHolder = ConnectionStateHandler()
private val serviceRepository: ServiceRepository = mockk(relaxed = true)
private lateinit var broadcasts: MeshServiceBroadcasts
private lateinit var broadcasts: ServiceBroadcasts
@Before
fun setUp() {
context = ApplicationProvider.getApplicationContext()
broadcasts = MeshServiceBroadcasts(context, connectionStateHolder, serviceRepository)
broadcasts = ServiceBroadcasts(context, serviceRepository)
}
@Test
fun `broadcastConnection sends uppercase state string for ATAK`() {
connectionStateHolder.setState(ConnectionState.Connected)
every { serviceRepository.connectionState } returns MutableStateFlow(ConnectionState.Connected)
broadcasts.broadcastConnection()
@ -58,7 +59,7 @@ class MeshServiceBroadcastsTest {
@Test
fun `broadcastConnection sends legacy connection intent`() {
connectionStateHolder.setState(ConnectionState.Connected)
every { serviceRepository.connectionState } returns MutableStateFlow(ConnectionState.Connected)
broadcasts.broadcastConnection()

View file

@ -1,70 +0,0 @@
/*
* Copyright (c) 2025-2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.service
import org.junit.Assert.assertEquals
import org.junit.Test
import org.meshtastic.proto.StoreAndForward
class StoreForwardHistoryRequestTest {
@Test
fun `buildStoreForwardHistoryRequest copies positive parameters`() {
val request =
MeshHistoryManager.buildStoreForwardHistoryRequest(
lastRequest = 42,
historyReturnWindow = 15,
historyReturnMax = 25,
)
assertEquals(StoreAndForward.RequestResponse.CLIENT_HISTORY, request.rr)
assertEquals(42, request.history?.last_request)
assertEquals(15, request.history?.window)
assertEquals(25, request.history?.history_messages)
}
@Test
fun `buildStoreForwardHistoryRequest clamps non-positive parameters`() {
val request =
MeshHistoryManager.buildStoreForwardHistoryRequest(
lastRequest = 0,
historyReturnWindow = -1,
historyReturnMax = 0,
)
assertEquals(StoreAndForward.RequestResponse.CLIENT_HISTORY, request.rr)
assertEquals(0, request.history?.last_request)
assertEquals(0, request.history?.window)
assertEquals(0, request.history?.history_messages)
}
@Test
fun `resolveHistoryRequestParameters uses config values when positive`() {
val (window, max) = MeshHistoryManager.resolveHistoryRequestParameters(window = 30, max = 10)
assertEquals(30, window)
assertEquals(10, max)
}
@Test
fun `resolveHistoryRequestParameters falls back to defaults when non-positive`() {
val (window, max) = MeshHistoryManager.resolveHistoryRequestParameters(window = 0, max = -5)
assertEquals(1440, window)
assertEquals(100, max)
}
}