mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-09-12 21:30:02 -04:00
fix(node): stop presenting MQTT-only nodes as unheard on the current LoRa (#7138)
This commit is contained in:
1 parent
848e124b9d
commit
6d972ea406
14 files changed
+295
-27
No files matched your search
+5
-3
@@ -163,9 +163,11 @@ data class NodeEntity(
|
||||
/** True when this node signs its broadcasts via XEdDSA (NodeInfo.has_xeddsa_signed). */
|
||||
@ColumnInfo(name = "has_xeddsa_signed", defaultValue = "0") var signsPackets: Boolean = false,
|
||||
/**
|
||||
* True when the radio has heard this node over RF since its current LoRa config took effect
|
||||
* (NodeInfo.heard_on_current_lora). Defaults true so nodes stored before this column existed, and nodes from
|
||||
* firmware that does not report it, are never shown as unheard.
|
||||
* True when the radio has heard this node over RF on the LoRa configuration it is using now
|
||||
* (NodeInfo.heard_on_current_lora). The radio derives this from the slot each node was heard on rather than
|
||||
* clearing it on a config change, so returning to a configuration restores the previous answers. Defaults true so
|
||||
* nodes stored before this column existed, and nodes from firmware that does not report it, are never shown as
|
||||
* unheard.
|
||||
*/
|
||||
@ColumnInfo(name = "heard_on_current_lora", defaultValue = "1") var heardOnCurrentLora: Boolean = true,
|
||||
/**
|
||||
|
||||
@@ -98,9 +98,9 @@ data class Capabilities(val firmwareVersion: String?, internal val forceEnableAl
|
||||
val supportsMeshBeacon = atLeast(V2_8_0)
|
||||
|
||||
/**
|
||||
* Whether the node reports [NodeInfo.heard_on_current_lora] - whether it has heard each node since its current LoRa
|
||||
* config took effect. Gated to [UNRELEASED] until the firmware side ships. Older firmware never sends the field,
|
||||
* and a proto3 bool defaults to false, so an ungated read marks every node as unheard.
|
||||
* Whether the node reports [NodeInfo.heard_on_current_lora] - whether it has heard each node over RF on the LoRa
|
||||
* configuration it is using now. Gated to [UNRELEASED] until the firmware side ships. Older firmware never sends
|
||||
* the field, and a proto3 bool defaults to false, so an ungated read marks every node as unheard.
|
||||
*
|
||||
* Deliberately outside [forceEnableAll]: every other capability being wrong in a debug build shows a UI the
|
||||
* firmware ignores, but this one being wrong persists false into the node DB and offers real nodes for removal.
|
||||
|
||||
@@ -69,7 +69,11 @@ data class Node(
|
||||
val manuallyVerified: Boolean = false,
|
||||
/** True when this node signs its broadcasts via XEdDSA (NodeInfo.has_xeddsa_signed). Automatic trust. */
|
||||
val signsPackets: Boolean = false,
|
||||
/** False when the radio has not heard this node since its current LoRa config took effect. */
|
||||
/**
|
||||
* False when the radio has not heard this node over RF on the LoRa configuration it is using now. The radio derives
|
||||
* this rather than storing it, so returning to a configuration restores the previous answers. Ask
|
||||
* [isUnheardOnCurrentLora] rather than this flag: false alone does not mean the node became unreachable.
|
||||
*/
|
||||
val heardOnCurrentLora: Boolean = true,
|
||||
val nodeStatus: String? = null,
|
||||
/** The transport mechanism this node was last heard over (see [MeshPacket.TransportMechanism]). */
|
||||
@@ -84,6 +88,24 @@ data class Node(
|
||||
) {
|
||||
val capabilities: Capabilities by lazy { Capabilities(metadata?.firmware_version) }
|
||||
|
||||
/**
|
||||
* True when the radio does not report having heard this node over RF on the LoRa configuration in force now, and it
|
||||
* is not an [viaMqtt] node.
|
||||
*
|
||||
* This is not a claim that the node was ever heard over RF. `heard_on_current_lora` is a single bool, and the radio
|
||||
* does not expose its "heard over RF at least once" bit separately, so a node heard under different settings and a
|
||||
* node never heard at all (one added as a shared contact, say) are indistinguishable here. Both read true. Callers
|
||||
* that must not act on the second case need another signal - the removal offer relies on favourites, which is what
|
||||
* firmware marks a contact.
|
||||
*
|
||||
* [viaMqtt] nodes are excluded because they arrive over the internet rather than over our own radio, so the radio
|
||||
* never marks them heard over RF and [heardOnCurrentLora] is false for them permanently - not because a setting
|
||||
* changed. Presenting those as unreachable would badge every node on an MQTT-uplinked mesh and offer it for
|
||||
* removal, and removing one is pointless because it returns on the next uplinked packet.
|
||||
*/
|
||||
val isUnheardOnCurrentLora: Boolean
|
||||
get() = !heardOnCurrentLora && !viaMqtt
|
||||
|
||||
val isOnline: Boolean
|
||||
get() = isOnline(onlineTimeThreshold())
|
||||
|
||||
|
||||
@@ -164,4 +164,19 @@ class NodeTest {
|
||||
|
||||
private fun nodeWithPosition(num: Int, latitudeI: Int, longitudeI: Int): Node =
|
||||
Node(num = num, position = Position(latitude_i = latitudeI, longitude_i = longitudeI))
|
||||
|
||||
@Test
|
||||
fun `isUnheardOnCurrentLora is true for a node not heard on the config in force now`() {
|
||||
assertTrue(Node(num = 1, heardOnCurrentLora = false).isUnheardOnCurrentLora)
|
||||
assertFalse(Node(num = 1, heardOnCurrentLora = true).isUnheardOnCurrentLora)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `isUnheardOnCurrentLora excludes MQTT nodes the radio never heard over RF`() {
|
||||
// The radio sets the mark only on an RF hear, so a node that only ever arrives over MQTT reports false for
|
||||
// the lifetime of the entry. Presenting that as "unreachable since you changed settings" would badge every
|
||||
// node on an MQTT-uplinked mesh and offer it for removal, and removing one is pointless.
|
||||
assertFalse(Node(num = 1, heardOnCurrentLora = false, viaMqtt = true).isUnheardOnCurrentLora)
|
||||
assertFalse(Node(num = 1, heardOnCurrentLora = true, viaMqtt = true).isUnheardOnCurrentLora)
|
||||
}
|
||||
}
|
||||
@@ -1354,7 +1354,7 @@
|
||||
<string name="node_list_help_node_details">Node Details</string>
|
||||
<string name="node_list_help_title">Node List Help</string>
|
||||
<string name="node_list_long_click_label">Node options</string>
|
||||
<string name="node_not_heard_on_current_lora">Not heard since you changed settings</string>
|
||||
<string name="node_not_heard_on_current_lora">Not heard on your current LoRa settings</string>
|
||||
<string name="node_number">Node Number</string>
|
||||
<string name="node_request_send_failed">Couldn't send request. Try again.</string>
|
||||
<string name="node_restarting">Restarting…</string>
|
||||
@@ -1377,8 +1377,8 @@
|
||||
<string name="nodes_empty_searching_hint">Nearby nodes will appear here as they're discovered.</string>
|
||||
<string name="nodes_empty_searching_title">Searching for nodes</string>
|
||||
<string name="nodes_queued_for_deletion">%1$d nodes queued for deletion:</string>
|
||||
<string name="nodes_unheard_banner">%1$d nodes not heard since you changed settings</string>
|
||||
<string name="nodes_unheard_banner_one">1 node not heard since you changed settings</string>
|
||||
<string name="nodes_unheard_banner">%1$d nodes not heard on your current LoRa settings</string>
|
||||
<string name="nodes_unheard_banner_one">1 node not heard on your current LoRa settings</string>
|
||||
<string name="nodes_unheard_keep">Keep</string>
|
||||
<string name="nodes_unheard_remove">Remove</string>
|
||||
<string name="noise_floor">Noise Floor</string>
|
||||
|
||||
@@ -461,9 +461,10 @@ private fun MetricsGrid(items: List<@Composable () -> Unit>) {
|
||||
* [LastHeardInfo] tinted StatusGreen when [online] — the "online" affordance — rendered plain otherwise. Shared by the
|
||||
* complete and compact node rows.
|
||||
*
|
||||
* When [heardOnCurrentLora] is false the radio has not heard this node since its LoRa settings changed, so it cannot be
|
||||
* reached from here. That is a different claim from "offline" and takes precedence: an online node can still be
|
||||
* unreachable, and the online tint would say the opposite.
|
||||
* When [heardOnCurrentLora] is false the radio has not heard this node on the LoRa configuration it is using now, so it
|
||||
* cannot be reached from here. That is a different claim from "offline" and takes precedence: an online node can still
|
||||
* be unreachable, and the online tint would say the opposite. Callers pass Node.isUnheardOnCurrentLora inverted, not
|
||||
* the raw flag, so MQTT-only nodes are not presented as unreachable.
|
||||
*/
|
||||
@Composable
|
||||
internal fun StatusAwareLastHeard(
|
||||
@@ -560,7 +561,7 @@ private fun NodeItemHeader(
|
||||
lastHeard = thatNode.lastHeard,
|
||||
online = !isThisNode && thatNode.isOnline,
|
||||
contentColor = contentColor,
|
||||
heardOnCurrentLora = isThisNode || thatNode.heardOnCurrentLora,
|
||||
heardOnCurrentLora = isThisNode || !thatNode.isUnheardOnCurrentLora,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -329,7 +329,7 @@ private fun CompactHealthRow(
|
||||
online = thatNode.isOnline,
|
||||
contentColor = contentColor,
|
||||
relative = lastHeardIsRelative,
|
||||
heardOnCurrentLora = isThisNode || thatNode.heardOnCurrentLora,
|
||||
heardOnCurrentLora = isThisNode || !thatNode.isUnheardOnCurrentLora,
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
* 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 org.meshtastic.core.ui.component
|
||||
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.ui.test.ComposeUiTest
|
||||
import androidx.compose.ui.test.ExperimentalTestApi
|
||||
import androidx.compose.ui.test.assertCountEquals
|
||||
import androidx.compose.ui.test.onAllNodesWithContentDescription
|
||||
import androidx.compose.ui.test.v2.runComposeUiTest
|
||||
import org.meshtastic.core.common.util.MeasurementSystem
|
||||
import org.meshtastic.core.common.util.nowSeconds
|
||||
import org.meshtastic.core.model.ConnectionState
|
||||
import org.meshtastic.core.model.Node
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.getString
|
||||
import org.meshtastic.core.resources.node_not_heard_on_current_lora
|
||||
import org.meshtastic.proto.User
|
||||
import kotlin.test.Test
|
||||
|
||||
/**
|
||||
* The unreachable marker on both node rows. Each feeds [StatusAwareLastHeard] from `Node.isUnheardOnCurrentLora`, not
|
||||
* the raw `heardOnCurrentLora` flag, so an MQTT-only node must not wear it: the radio sets the mark only on an RF hear,
|
||||
* so such a node reports false for the life of the entry and would otherwise be badged forever and offered for removal.
|
||||
*
|
||||
* Driven through the real rows rather than [StatusAwareLastHeard] directly - the wiring is what can regress, so the
|
||||
* via_mqtt cases fail if either row goes back to reading the raw flag.
|
||||
*/
|
||||
@OptIn(ExperimentalTestApi::class)
|
||||
class NodeItemUnheardMarkerTest {
|
||||
|
||||
private fun node(viaMqtt: Boolean) = Node(
|
||||
num = 1928,
|
||||
user = User(long_name = "Minnie Mouse", short_name = "MiMo", id = "!minnie"),
|
||||
lastHeard = (nowSeconds - 300).toInt(),
|
||||
heardOnCurrentLora = false,
|
||||
viaMqtt = viaMqtt,
|
||||
)
|
||||
|
||||
private fun ComposeUiTest.markerCount() =
|
||||
onAllNodesWithContentDescription(getString(Res.string.node_not_heard_on_current_lora))
|
||||
|
||||
private fun ComposeUiTest.setNodeItem(viaMqtt: Boolean) = setContent {
|
||||
MaterialTheme {
|
||||
NodeItem(
|
||||
thisNode = null,
|
||||
thatNode = node(viaMqtt),
|
||||
distanceUnits = MeasurementSystem.METRIC,
|
||||
tempInFahrenheit = false,
|
||||
connectionState = ConnectionState.Connected,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun ComposeUiTest.setNodeItemCompact(viaMqtt: Boolean) = setContent {
|
||||
MaterialTheme {
|
||||
NodeItemCompact(thisNode = null, thatNode = node(viaMqtt), distanceUnits = MeasurementSystem.METRIC)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun nodeItem_marksANodeNotHeardOnTheConfigInForceNow() = runComposeUiTest {
|
||||
setNodeItem(viaMqtt = false)
|
||||
markerCount().assertCountEquals(1)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun nodeItem_withholdsTheMarkerFromAnMqttNode() = runComposeUiTest {
|
||||
setNodeItem(viaMqtt = true)
|
||||
markerCount().assertCountEquals(0)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun nodeItemCompact_marksANodeNotHeardOnTheConfigInForceNow() = runComposeUiTest {
|
||||
setNodeItemCompact(viaMqtt = false)
|
||||
markerCount().assertCountEquals(1)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun nodeItemCompact_withholdsTheMarkerFromAnMqttNode() = runComposeUiTest {
|
||||
setNodeItemCompact(viaMqtt = true)
|
||||
markerCount().assertCountEquals(0)
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -84,8 +84,8 @@ fun NodeItemCompleteActivePreview() {
|
||||
@PreviewLightDark
|
||||
@Composable
|
||||
fun NodeItemUnheardPreview() {
|
||||
// Heard recently enough to count as online, but not since the radio's LoRa config changed. The unheard marker
|
||||
// has to win over the online tint here: green would claim it is reachable.
|
||||
// Heard recently enough to count as online, but not on the LoRa config the radio is using now. The unheard
|
||||
// marker has to win over the online tint here: green would claim it is reachable.
|
||||
val unheardNode =
|
||||
previewNodes.minnieMouse.copy(
|
||||
lastHeard = (org.meshtastic.core.common.util.nowSeconds - 300).toInt(),
|
||||
|
||||
+1
-1
@@ -66,7 +66,7 @@ open class GetFilteredNodesUseCase constructor(private val nodeRepository: NodeR
|
||||
.filter { node -> if (filter.excludeMqtt) !node.viaMqtt else true }
|
||||
// The connected node is never unreachable from itself, and both row renderers already exempt it.
|
||||
.filter { node ->
|
||||
if (filter.excludeUnheard) node.heardOnCurrentLora || node.num == ourNum else true
|
||||
if (filter.excludeUnheard) !node.isUnheardOnCurrentLora || node.num == ourNum else true
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-9
@@ -144,15 +144,7 @@ fun NodeListScreen(
|
||||
// radio is not something the node list may delete. ourNode and unfilteredNodes come from independent flows, so
|
||||
// the list can already contain the local node while ourNode is still null. Offer nothing until it is known,
|
||||
// rather than risk removing the user's own node from the radio.
|
||||
val unheardNodes =
|
||||
remember(unfilteredNodes, ourNode) {
|
||||
val ourNum = ourNode?.num
|
||||
if (ourNum == null) {
|
||||
emptyList()
|
||||
} else {
|
||||
unfilteredNodes.filter { !it.heardOnCurrentLora && !it.isFavorite && it.num != ourNum }
|
||||
}
|
||||
}
|
||||
val unheardNodes = remember(unfilteredNodes, ourNode) { selectRemovableUnheardNodes(unfilteredNodes, ourNode?.num) }
|
||||
val deviceImageUrls by viewModel.deviceImageUrls.collectAsStateWithLifecycle()
|
||||
val ignoredNodeCount = unfilteredNodes.count { it.isIgnored }
|
||||
|
||||
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* 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 org.meshtastic.feature.node.list
|
||||
|
||||
import org.meshtastic.core.model.Node
|
||||
|
||||
/**
|
||||
* The nodes the unheard banner counts and its one-tap clear would remove.
|
||||
*
|
||||
* Deliberately narrower than [Node.isUnheardOnCurrentLora] alone, because this one deletes:
|
||||
* - **Favourites are exempt.** An explicit keep, and it is also what firmware marks a node added as a shared contact,
|
||||
* which has never been heard over RF and would otherwise always qualify.
|
||||
* - **The connected radio is exempt**, and nothing is offered at all until [ourNum] is known. It and the node list
|
||||
* arrive on independent flows, so the list can hold the local node while [ourNum] is still null; offering then would
|
||||
* risk removing the user's own node.
|
||||
* - **MQTT nodes are exempt** via [Node.isUnheardOnCurrentLora]: they read unheard permanently, and removing one
|
||||
* achieves nothing because the next uplinked packet brings it back.
|
||||
*/
|
||||
internal fun selectRemovableUnheardNodes(nodes: List<Node>, ourNum: Int?): List<Node> {
|
||||
if (ourNum == null) return emptyList()
|
||||
return nodes.filter { it.isUnheardOnCurrentLora && !it.isFavorite && it.num != ourNum }
|
||||
}
|
||||
+25
@@ -58,6 +58,7 @@ class GetFilteredNodesUseCaseTest {
|
||||
viaMqtt: Boolean = false,
|
||||
signsPackets: Boolean = false,
|
||||
publicKey: ByteString? = null,
|
||||
heardOnCurrentLora: Boolean = true,
|
||||
): Node {
|
||||
val user = User(id = "!$num", long_name = name, short_name = "N$num", role = role)
|
||||
return Node(
|
||||
@@ -67,6 +68,7 @@ class GetFilteredNodesUseCaseTest {
|
||||
viaMqtt = viaMqtt,
|
||||
signsPackets = signsPackets,
|
||||
publicKey = publicKey,
|
||||
heardOnCurrentLora = heardOnCurrentLora,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -193,4 +195,27 @@ class GetFilteredNodesUseCaseTest {
|
||||
|
||||
assertEquals(listOf(2), result.map { it.num })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `the unheard filter drops a node the radio can no longer reach`() = runTest {
|
||||
val nodes = listOf(createNode(1), createNode(2, heardOnCurrentLora = false))
|
||||
every { nodeRepository.getNodes() } returns flowOf(nodes)
|
||||
|
||||
val result = useCase(NodeFilterState(excludeUnheard = true), NodeSortOption.LAST_HEARD).first()
|
||||
|
||||
assertEquals(listOf(1), result.map { it.num })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `the unheard filter keeps an MQTT node which reads unheard permanently`() = runTest {
|
||||
// The radio only marks a node heard on the current LoRa config when it arrives over RF, so an MQTT-only node
|
||||
// reads false permanently. That is not the same as "went unreachable when the settings changed", and hiding it
|
||||
// would empty the list on an MQTT-uplinked mesh.
|
||||
val nodes = listOf(createNode(1, viaMqtt = true, heardOnCurrentLora = false), createNode(2))
|
||||
every { nodeRepository.getNodes() } returns flowOf(nodes)
|
||||
|
||||
val result = useCase(NodeFilterState(excludeUnheard = true), NodeSortOption.LAST_HEARD).first()
|
||||
|
||||
assertEquals(listOf(1, 2), result.map { it.num }.sorted())
|
||||
}
|
||||
}
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* 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 org.meshtastic.feature.node.list
|
||||
|
||||
import org.meshtastic.core.model.Node
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
/**
|
||||
* What the unheard banner counts and its one-tap clear would delete. This is the highest-consequence consumer of
|
||||
* [Node.isUnheardOnCurrentLora] - it removes nodes - so each exemption is pinned separately.
|
||||
*/
|
||||
class RemovableUnheardNodesTest {
|
||||
|
||||
private val ourNum = 1
|
||||
|
||||
private fun node(
|
||||
num: Int,
|
||||
heardOnCurrentLora: Boolean = false,
|
||||
viaMqtt: Boolean = false,
|
||||
isFavorite: Boolean = false,
|
||||
) = Node(num = num, heardOnCurrentLora = heardOnCurrentLora, viaMqtt = viaMqtt, isFavorite = isFavorite)
|
||||
|
||||
@Test
|
||||
fun offersANodeNotHeardOnTheConfigInForceNow() {
|
||||
val nodes = listOf(node(2), node(3, heardOnCurrentLora = true))
|
||||
|
||||
assertEquals(listOf(2), selectRemovableUnheardNodes(nodes, ourNum).map { it.num })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun neverOffersAnMqttNode() {
|
||||
// Reads unheard for the life of the entry, and deleting it achieves nothing - the next uplinked packet
|
||||
// brings it straight back.
|
||||
val nodes = listOf(node(2, viaMqtt = true), node(3))
|
||||
|
||||
assertEquals(listOf(3), selectRemovableUnheardNodes(nodes, ourNum).map { it.num })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun neverOffersAFavourite() {
|
||||
// Also covers shared contacts: firmware marks a contact favourite, and one has never been heard over RF.
|
||||
val nodes = listOf(node(2, isFavorite = true), node(3))
|
||||
|
||||
assertEquals(listOf(3), selectRemovableUnheardNodes(nodes, ourNum).map { it.num })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun neverOffersTheConnectedRadio() {
|
||||
val nodes = listOf(node(ourNum), node(3))
|
||||
|
||||
assertEquals(listOf(3), selectRemovableUnheardNodes(nodes, ourNum).map { it.num })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun offersNothingUntilTheLocalNodeNumberIsKnown() {
|
||||
// ourNode and the node list arrive on independent flows; offering here could delete the user's own node.
|
||||
val nodes = listOf(node(2), node(3))
|
||||
|
||||
assertTrue(selectRemovableUnheardNodes(nodes, ourNum = null).isEmpty())
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user