From 57eaa3c22d9da8e4916f36bc0eeb83278ab4f450 Mon Sep 17 00:00:00 2001 From: James Rich Date: Tue, 19 May 2026 09:05:09 -0500 Subject: [PATCH] feat(discovery): add Apple parity fixes - infrastructure tracking, session recovery, default key guard - Track infrastructure nodes (ROUTER, ROUTER_LATE, CLIENT_BASE roles) in DiscoveredNodeEntity and DiscoveryPresetResultEntity - Add markInterruptedSessions() DAO query for cold-start recovery - Add usesDefaultKey StateFlow to DiscoveryViewModel that checks primary channel PSK and disables scan when using default/cleartext key - Wire default key guard into ScanButton with accessibility description - Add discovery_start_scan_reason_default_key string resource - Update all test DAO fakes with KMP-compatible implementations Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .skills/compose-ui/strings-index.txt | 1 + .../39.json | 22 +++++++++++++++---- .../core/database/dao/DiscoveryDao.kt | 3 +++ .../database/entity/DiscoveredNodeEntity.kt | 1 + .../entity/DiscoveryPresetResultEntity.kt | 1 + .../composeResources/values/strings.xml | 1 + .../feature/discovery/DiscoveryScanEngine.kt | 15 ++++++++++++- .../feature/discovery/DiscoveryViewModel.kt | 13 +++++++++++ .../discovery/ui/DiscoveryScanScreen.kt | 7 +++++- .../discovery/DiscoveryHistoryBehaviorTest.kt | 9 ++++++++ .../discovery/DiscoveryMapFilterTest.kt | 9 ++++++++ .../DiscoveryPacketCollectionTest.kt | 11 ++++++++-- .../discovery/DiscoveryScanEngineTest.kt | 9 ++++++++ 13 files changed, 94 insertions(+), 8 deletions(-) diff --git a/.skills/compose-ui/strings-index.txt b/.skills/compose-ui/strings-index.txt index e3467e71b..9cf5ec54e 100644 --- a/.skills/compose-ui/strings-index.txt +++ b/.skills/compose-ui/strings-index.txt @@ -325,6 +325,7 @@ discovery_session_detail discovery_shifting_to discovery_start_scan discovery_start_scan_disabled +discovery_start_scan_reason_default_key discovery_start_scan_reason_no_presets discovery_start_scan_reason_not_connected discovery_stat_analysis diff --git a/core/database/schemas/org.meshtastic.core.database.MeshtasticDatabase/39.json b/core/database/schemas/org.meshtastic.core.database.MeshtasticDatabase/39.json index 0d6f55c07..f3ddece52 100644 --- a/core/database/schemas/org.meshtastic.core.database.MeshtasticDatabase/39.json +++ b/core/database/schemas/org.meshtastic.core.database.MeshtasticDatabase/39.json @@ -2,7 +2,7 @@ "formatVersion": 1, "database": { "version": 39, - "identityHash": "e39ee4f34ed8da08f3cb21bfd4a5165c", + "identityHash": "90335dadf5ace3b9f23b3818bd257f35", "entities": [ { "tableName": "my_node", @@ -1149,7 +1149,7 @@ }, { "tableName": "discovery_preset_result", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `session_id` INTEGER NOT NULL, `preset_name` TEXT NOT NULL, `dwell_duration_seconds` INTEGER NOT NULL DEFAULT 0, `unique_nodes` INTEGER NOT NULL DEFAULT 0, `direct_neighbor_count` INTEGER NOT NULL DEFAULT 0, `mesh_neighbor_count` INTEGER NOT NULL DEFAULT 0, `message_count` INTEGER NOT NULL DEFAULT 0, `sensor_packet_count` INTEGER NOT NULL DEFAULT 0, `avg_channel_utilization` REAL NOT NULL DEFAULT 0.0, `avg_airtime_rate` REAL NOT NULL DEFAULT 0.0, `packet_success_rate` REAL NOT NULL DEFAULT 0.0, `packet_failure_rate` REAL NOT NULL DEFAULT 0.0, `ai_summary` TEXT, `num_packets_tx` INTEGER NOT NULL DEFAULT 0, `num_packets_rx` INTEGER NOT NULL DEFAULT 0, `num_packets_rx_bad` INTEGER NOT NULL DEFAULT 0, `num_rx_dupe` INTEGER NOT NULL DEFAULT 0, `num_tx_relay` INTEGER NOT NULL DEFAULT 0, `num_tx_relay_canceled` INTEGER NOT NULL DEFAULT 0, `num_online_nodes` INTEGER NOT NULL DEFAULT 0, `num_total_nodes` INTEGER NOT NULL DEFAULT 0, `uptime_seconds` INTEGER NOT NULL DEFAULT 0, FOREIGN KEY(`session_id`) REFERENCES `discovery_session`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `session_id` INTEGER NOT NULL, `preset_name` TEXT NOT NULL, `dwell_duration_seconds` INTEGER NOT NULL DEFAULT 0, `unique_nodes` INTEGER NOT NULL DEFAULT 0, `direct_neighbor_count` INTEGER NOT NULL DEFAULT 0, `mesh_neighbor_count` INTEGER NOT NULL DEFAULT 0, `infrastructure_node_count` INTEGER NOT NULL DEFAULT 0, `message_count` INTEGER NOT NULL DEFAULT 0, `sensor_packet_count` INTEGER NOT NULL DEFAULT 0, `avg_channel_utilization` REAL NOT NULL DEFAULT 0.0, `avg_airtime_rate` REAL NOT NULL DEFAULT 0.0, `packet_success_rate` REAL NOT NULL DEFAULT 0.0, `packet_failure_rate` REAL NOT NULL DEFAULT 0.0, `ai_summary` TEXT, `num_packets_tx` INTEGER NOT NULL DEFAULT 0, `num_packets_rx` INTEGER NOT NULL DEFAULT 0, `num_packets_rx_bad` INTEGER NOT NULL DEFAULT 0, `num_rx_dupe` INTEGER NOT NULL DEFAULT 0, `num_tx_relay` INTEGER NOT NULL DEFAULT 0, `num_tx_relay_canceled` INTEGER NOT NULL DEFAULT 0, `num_online_nodes` INTEGER NOT NULL DEFAULT 0, `num_total_nodes` INTEGER NOT NULL DEFAULT 0, `uptime_seconds` INTEGER NOT NULL DEFAULT 0, FOREIGN KEY(`session_id`) REFERENCES `discovery_session`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", "fields": [ { "fieldPath": "id", @@ -1197,6 +1197,13 @@ "notNull": true, "defaultValue": "0" }, + { + "fieldPath": "infrastructureNodeCount", + "columnName": "infrastructure_node_count", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, { "fieldPath": "messageCount", "columnName": "message_count", @@ -1341,7 +1348,7 @@ }, { "tableName": "discovered_node", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `preset_result_id` INTEGER NOT NULL, `node_num` INTEGER NOT NULL, `short_name` TEXT, `long_name` TEXT, `neighbor_type` TEXT NOT NULL DEFAULT 'direct', `latitude` REAL, `longitude` REAL, `distance_from_user` REAL, `hop_count` INTEGER NOT NULL DEFAULT 0, `snr` REAL NOT NULL DEFAULT 0, `rssi` INTEGER NOT NULL DEFAULT 0, `message_count` INTEGER NOT NULL DEFAULT 0, `sensor_packet_count` INTEGER NOT NULL DEFAULT 0, FOREIGN KEY(`preset_result_id`) REFERENCES `discovery_preset_result`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `preset_result_id` INTEGER NOT NULL, `node_num` INTEGER NOT NULL, `short_name` TEXT, `long_name` TEXT, `neighbor_type` TEXT NOT NULL DEFAULT 'direct', `latitude` REAL, `longitude` REAL, `distance_from_user` REAL, `hop_count` INTEGER NOT NULL DEFAULT 0, `snr` REAL NOT NULL DEFAULT 0, `rssi` INTEGER NOT NULL DEFAULT 0, `message_count` INTEGER NOT NULL DEFAULT 0, `sensor_packet_count` INTEGER NOT NULL DEFAULT 0, `is_infrastructure` INTEGER NOT NULL DEFAULT 0, FOREIGN KEY(`preset_result_id`) REFERENCES `discovery_preset_result`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", "fields": [ { "fieldPath": "id", @@ -1427,6 +1434,13 @@ "affinity": "INTEGER", "notNull": true, "defaultValue": "0" + }, + { + "fieldPath": "isInfrastructure", + "columnName": "is_infrastructure", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" } ], "primaryKey": { @@ -1472,7 +1486,7 @@ ], "setupQueries": [ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'e39ee4f34ed8da08f3cb21bfd4a5165c')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '90335dadf5ace3b9f23b3818bd257f35')" ] } } \ No newline at end of file diff --git a/core/database/src/commonMain/kotlin/org/meshtastic/core/database/dao/DiscoveryDao.kt b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/dao/DiscoveryDao.kt index 5497957a7..7e4ebdf5e 100644 --- a/core/database/src/commonMain/kotlin/org/meshtastic/core/database/dao/DiscoveryDao.kt +++ b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/dao/DiscoveryDao.kt @@ -48,6 +48,9 @@ interface DiscoveryDao { @Query("DELETE FROM discovery_session WHERE id = :sessionId") suspend fun deleteSession(sessionId: Long) + @Query("UPDATE discovery_session SET completion_status = 'interrupted' WHERE completion_status = 'in_progress'") + suspend fun markInterruptedSessions() + // endregion // region Preset result operations diff --git a/core/database/src/commonMain/kotlin/org/meshtastic/core/database/entity/DiscoveredNodeEntity.kt b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/entity/DiscoveredNodeEntity.kt index 710b285bd..eeb8c7eb3 100644 --- a/core/database/src/commonMain/kotlin/org/meshtastic/core/database/entity/DiscoveredNodeEntity.kt +++ b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/entity/DiscoveredNodeEntity.kt @@ -50,4 +50,5 @@ data class DiscoveredNodeEntity( @ColumnInfo(name = "rssi", defaultValue = "0") val rssi: Int = 0, @ColumnInfo(name = "message_count", defaultValue = "0") val messageCount: Int = 0, @ColumnInfo(name = "sensor_packet_count", defaultValue = "0") val sensorPacketCount: Int = 0, + @ColumnInfo(name = "is_infrastructure", defaultValue = "0") val isInfrastructure: Boolean = false, ) diff --git a/core/database/src/commonMain/kotlin/org/meshtastic/core/database/entity/DiscoveryPresetResultEntity.kt b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/entity/DiscoveryPresetResultEntity.kt index 6a229a37a..c957bc5c2 100644 --- a/core/database/src/commonMain/kotlin/org/meshtastic/core/database/entity/DiscoveryPresetResultEntity.kt +++ b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/entity/DiscoveryPresetResultEntity.kt @@ -43,6 +43,7 @@ data class DiscoveryPresetResultEntity( @ColumnInfo(name = "unique_nodes", defaultValue = "0") val uniqueNodes: Int = 0, @ColumnInfo(name = "direct_neighbor_count", defaultValue = "0") val directNeighborCount: Int = 0, @ColumnInfo(name = "mesh_neighbor_count", defaultValue = "0") val meshNeighborCount: Int = 0, + @ColumnInfo(name = "infrastructure_node_count", defaultValue = "0") val infrastructureNodeCount: Int = 0, @ColumnInfo(name = "message_count", defaultValue = "0") val messageCount: Int = 0, @ColumnInfo(name = "sensor_packet_count", defaultValue = "0") val sensorPacketCount: Int = 0, @ColumnInfo(name = "avg_channel_utilization", defaultValue = "0.0") val avgChannelUtilization: Double = 0.0, diff --git a/core/resources/src/commonMain/composeResources/values/strings.xml b/core/resources/src/commonMain/composeResources/values/strings.xml index bbc0f0702..2c7eb83d0 100644 --- a/core/resources/src/commonMain/composeResources/values/strings.xml +++ b/core/resources/src/commonMain/composeResources/values/strings.xml @@ -349,6 +349,7 @@ Shifting to %1$s Start Scan Start scan button disabled. %1$s + channel uses default encryption key no presets selected device not connected Analysis diff --git a/feature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/DiscoveryScanEngine.kt b/feature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/DiscoveryScanEngine.kt index ee08e32a6..bd9c35df3 100644 --- a/feature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/DiscoveryScanEngine.kt +++ b/feature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/DiscoveryScanEngine.kt @@ -129,6 +129,7 @@ class DiscoveryScanEngine( var hopCount: Int = 0, var messageCount: Int = 0, var sensorPacketCount: Int = 0, + var isInfrastructure: Boolean = false, ) private data class DeviceMetricsEntry(val timestamp: Long, val channelUtil: Double, val airUtilTx: Double) @@ -250,7 +251,7 @@ class DiscoveryScanEngine( } } - /** Backfills name and position from the local NodeDB when not yet received over-the-air. */ + /** Backfills name, position, and infrastructure role from the local NodeDB when not yet received over-the-air. */ private fun enrichNodeFromDb(node: CollectedNodeData) { val dbNode = nodeRepository.nodeDBbyNum.value[node.nodeNum.toInt()] ?: return if (node.shortName == null || node.longName == null) { @@ -263,6 +264,7 @@ class DiscoveryScanEngine( if (dbLat != null && dbLat != 0) node.latitude = dbLat.toDouble() / POSITION_DIVISOR if (dbLon != null && dbLon != 0) node.longitude = dbLon.toDouble() / POSITION_DIVISOR } + node.isInfrastructure = dbNode.user.role in INFRASTRUCTURE_ROLES } // endregion @@ -482,6 +484,7 @@ class DiscoveryScanEngine( val (avgChannelUtil, avgAirUtil) = computeAverageMetrics() val directCount = collectedNodes.values.count { it.neighborType == "direct" } val meshCount = collectedNodes.values.count { it.neighborType == "mesh" } + val infraCount = collectedNodes.values.count { it.isInfrastructure } val packetsRx = lastLocalStats?.num_packets_rx ?: 0 val packetsRxBad = lastLocalStats?.num_packets_rx_bad ?: 0 @@ -495,6 +498,7 @@ class DiscoveryScanEngine( uniqueNodes = collectedNodes.size, directNeighborCount = directCount, meshNeighborCount = meshCount, + infrastructureNodeCount = infraCount, messageCount = collectedNodes.values.sumOf { it.messageCount }, sensorPacketCount = collectedNodes.values.sumOf { it.sensorPacketCount }, avgChannelUtilization = avgChannelUtil, @@ -559,6 +563,7 @@ class DiscoveryScanEngine( rssi = rssi, messageCount = messageCount, sensorPacketCount = sensorPacketCount, + isInfrastructure = isInfrastructure, ) } @@ -660,5 +665,13 @@ class DiscoveryScanEngine( private const val POSITION_DIVISOR = 1e7 private const val MIN_DEVICE_METRICS_PACKETS = 2 private const val PERCENT_MULTIPLIER = 100.0 + + /** Node roles that indicate infrastructure (Router, RouterLate, ClientBase). */ + private val INFRASTRUCTURE_ROLES = + setOf( + Config.DeviceConfig.Role.ROUTER, + Config.DeviceConfig.Role.ROUTER_LATE, + Config.DeviceConfig.Role.CLIENT_BASE, + ) } } diff --git a/feature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/DiscoveryViewModel.kt b/feature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/DiscoveryViewModel.kt index e7f01ddb8..64235b508 100644 --- a/feature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/DiscoveryViewModel.kt +++ b/feature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/DiscoveryViewModel.kt @@ -65,9 +65,22 @@ class DiscoveryViewModel( .map { it is ConnectionState.Connected } .stateInWhileSubscribed(initialValue = false) + /** True when the primary channel uses the default (well-known) PSK — scanning is unsafe. */ + val usesDefaultKey: StateFlow = + radioConfigRepository.channelSetFlow + .map { channelSet -> + val primaryPsk = channelSet.settings.firstOrNull()?.psk + primaryPsk == null || primaryPsk.size == 0 || (primaryPsk.size == 1 && primaryPsk[0].toInt() <= 1) + } + .stateInWhileSubscribed(initialValue = true) + val sessions: StateFlow> = discoveryDao.getAllSessions().stateInWhileSubscribed(initialValue = emptyList()) + init { + safeLaunch(tag = "markInterruptedSessions") { discoveryDao.markInterruptedSessions() } + } + fun togglePreset(preset: ChannelOption) { _selectedPresets.update { current -> val updated = if (preset in current) current - preset else current + preset diff --git a/feature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/ui/DiscoveryScanScreen.kt b/feature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/ui/DiscoveryScanScreen.kt index 79ceb8272..f17913cba 100644 --- a/feature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/ui/DiscoveryScanScreen.kt +++ b/feature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/ui/DiscoveryScanScreen.kt @@ -82,6 +82,7 @@ import org.meshtastic.core.resources.discovery_scan_progress import org.meshtastic.core.resources.discovery_shifting_to import org.meshtastic.core.resources.discovery_start_scan import org.meshtastic.core.resources.discovery_start_scan_disabled +import org.meshtastic.core.resources.discovery_start_scan_reason_default_key import org.meshtastic.core.resources.discovery_start_scan_reason_no_presets import org.meshtastic.core.resources.discovery_start_scan_reason_not_connected import org.meshtastic.core.resources.discovery_stop_scan @@ -118,6 +119,7 @@ fun DiscoveryScanScreen( val selectedPresets by viewModel.selectedPresets.collectAsStateWithLifecycle() val dwellMinutes by viewModel.dwellDurationMinutes.collectAsStateWithLifecycle() val isConnected by viewModel.isConnected.collectAsStateWithLifecycle() + val usesDefaultKey by viewModel.usesDefaultKey.collectAsStateWithLifecycle() val currentSession by viewModel.currentSession.collectAsStateWithLifecycle() val homePreset by viewModel.homePreset.collectAsStateWithLifecycle() @@ -174,6 +176,7 @@ fun DiscoveryScanScreen( scanState = scanState, isConnected = isConnected, hasPresetsSelected = selectedPresets.isNotEmpty(), + usesDefaultKey = usesDefaultKey, onStart = viewModel::startScan, onStop = viewModel::stopScan, ) @@ -329,6 +332,7 @@ private fun ScanButton( scanState: DiscoveryScanState, isConnected: Boolean, hasPresetsSelected: Boolean, + usesDefaultKey: Boolean, onStart: () -> Unit, onStop: () -> Unit, modifier: Modifier = Modifier, @@ -344,10 +348,11 @@ private fun ScanButton( Text(stringResource(Res.string.discovery_stop_scan), modifier = Modifier.padding(start = 8.dp)) } } else { - val isEnabled = isConnected && hasPresetsSelected + val isEnabled = isConnected && hasPresetsSelected && !usesDefaultKey val disabledReason = when { !isConnected -> stringResource(Res.string.discovery_start_scan_reason_not_connected) + usesDefaultKey -> stringResource(Res.string.discovery_start_scan_reason_default_key) !hasPresetsSelected -> stringResource(Res.string.discovery_start_scan_reason_no_presets) else -> "" } diff --git a/feature/discovery/src/commonTest/kotlin/org/meshtastic/feature/discovery/DiscoveryHistoryBehaviorTest.kt b/feature/discovery/src/commonTest/kotlin/org/meshtastic/feature/discovery/DiscoveryHistoryBehaviorTest.kt index 8f38d0f0c..d4558cd06 100644 --- a/feature/discovery/src/commonTest/kotlin/org/meshtastic/feature/discovery/DiscoveryHistoryBehaviorTest.kt +++ b/feature/discovery/src/commonTest/kotlin/org/meshtastic/feature/discovery/DiscoveryHistoryBehaviorTest.kt @@ -249,6 +249,15 @@ private class HistoryTestDao : DiscoveryDao { .maxOrNull() override suspend fun getSessionWithResults(sessionId: Long) = sessions[sessionId] + + override suspend fun markInterruptedSessions() { + sessions.keys.toList().forEach { key -> + val session = sessions[key]!! + if (session.completionStatus == "in_progress") { + sessions[key] = session.copy(completionStatus = "interrupted") + } + } + } } // endregion diff --git a/feature/discovery/src/commonTest/kotlin/org/meshtastic/feature/discovery/DiscoveryMapFilterTest.kt b/feature/discovery/src/commonTest/kotlin/org/meshtastic/feature/discovery/DiscoveryMapFilterTest.kt index b8b5b6aaf..6a62fced8 100644 --- a/feature/discovery/src/commonTest/kotlin/org/meshtastic/feature/discovery/DiscoveryMapFilterTest.kt +++ b/feature/discovery/src/commonTest/kotlin/org/meshtastic/feature/discovery/DiscoveryMapFilterTest.kt @@ -234,6 +234,15 @@ private class MapTestDao : DiscoveryDao { .maxOrNull() override suspend fun getSessionWithResults(sessionId: Long) = sessions[sessionId] + + override suspend fun markInterruptedSessions() { + sessions.keys.toList().forEach { key -> + val session = sessions[key]!! + if (session.completionStatus == "in_progress") { + sessions[key] = session.copy(completionStatus = "interrupted") + } + } + } } // endregion diff --git a/feature/discovery/src/commonTest/kotlin/org/meshtastic/feature/discovery/DiscoveryPacketCollectionTest.kt b/feature/discovery/src/commonTest/kotlin/org/meshtastic/feature/discovery/DiscoveryPacketCollectionTest.kt index 9d8006df8..261a3771f 100644 --- a/feature/discovery/src/commonTest/kotlin/org/meshtastic/feature/discovery/DiscoveryPacketCollectionTest.kt +++ b/feature/discovery/src/commonTest/kotlin/org/meshtastic/feature/discovery/DiscoveryPacketCollectionTest.kt @@ -414,6 +414,13 @@ private class InMemoryDiscoveryDao : DiscoveryDao { .maxOrNull() override suspend fun getSessionWithResults(sessionId: Long) = sessions[sessionId] -} -// endregion + override suspend fun markInterruptedSessions() { + sessions.keys.toList().forEach { key -> + val session = sessions[key]!! + if (session.completionStatus == "in_progress") { + sessions[key] = session.copy(completionStatus = "interrupted") + } + } + } +} diff --git a/feature/discovery/src/commonTest/kotlin/org/meshtastic/feature/discovery/DiscoveryScanEngineTest.kt b/feature/discovery/src/commonTest/kotlin/org/meshtastic/feature/discovery/DiscoveryScanEngineTest.kt index c912a97e4..65b0f8503 100644 --- a/feature/discovery/src/commonTest/kotlin/org/meshtastic/feature/discovery/DiscoveryScanEngineTest.kt +++ b/feature/discovery/src/commonTest/kotlin/org/meshtastic/feature/discovery/DiscoveryScanEngineTest.kt @@ -154,6 +154,15 @@ private class FakeDiscoveryDao : DiscoveryDao { .maxOrNull() override suspend fun getSessionWithResults(sessionId: Long): DiscoverySessionEntity? = sessions[sessionId] + + override suspend fun markInterruptedSessions() { + sessions.keys.toList().forEach { key -> + val session = sessions[key]!! + if (session.completionStatus == "in_progress") { + sessions[key] = session.copy(completionStatus = "interrupted") + } + } + } } /** Simple fake collector registry that tracks registration. */