From 8556fccfb0dc141cabcb65c55860470b71b618e6 Mon Sep 17 00:00:00 2001 From: James Rich Date: Mon, 18 May 2026 08:07:33 -0500 Subject: [PATCH] test(discovery): add comprehensive DiscoverySummaryGenerator tests Add 31 tests covering: - generateSessionSummary: empty presets, single/multi preset, ranking, congestion detection, traffic mix, completion status, recommendations - generatePresetSummary: node counts, channel util, congestion marking, traffic dominance, known preset data rate inclusion - buildSessionPrompt: instructions, session metadata, preset data, channel utilization, congestion guidance - buildPresetPrompt: preset name, metrics, guidance context Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../DiscoverySummaryGeneratorTest.kt | 316 ++++++++++++++++++ 1 file changed, 316 insertions(+) create mode 100644 feature/discovery/src/commonTest/kotlin/org/meshtastic/feature/discovery/DiscoverySummaryGeneratorTest.kt diff --git a/feature/discovery/src/commonTest/kotlin/org/meshtastic/feature/discovery/DiscoverySummaryGeneratorTest.kt b/feature/discovery/src/commonTest/kotlin/org/meshtastic/feature/discovery/DiscoverySummaryGeneratorTest.kt new file mode 100644 index 000000000..4f54b25d2 --- /dev/null +++ b/feature/discovery/src/commonTest/kotlin/org/meshtastic/feature/discovery/DiscoverySummaryGeneratorTest.kt @@ -0,0 +1,316 @@ +/* + * 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 . + */ +@file:Suppress("MagicNumber") + +package org.meshtastic.feature.discovery + +import org.meshtastic.core.database.entity.DiscoveryPresetResultEntity +import org.meshtastic.core.database.entity.DiscoverySessionEntity +import kotlin.test.Test +import kotlin.test.assertContains +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class DiscoverySummaryGeneratorTest { + + private val generator = DiscoverySummaryGenerator() + + // ---- Helpers ---- + + private fun session( + id: Long = 1, + totalUniqueNodes: Int = 10, + completionStatus: String = "complete", + avgChannelUtilization: Double = 0.0, + ) = DiscoverySessionEntity( + id = id, + timestamp = 1_000_000L, + presetsScanned = "LongFast,ShortFast", + homePreset = "LongFast", + totalUniqueNodes = totalUniqueNodes, + avgChannelUtilization = avgChannelUtilization, + completionStatus = completionStatus, + ) + + private fun preset( + id: Long = 1, + sessionId: Long = 1, + name: String = "LongFast", + uniqueNodes: Int = 5, + directNeighborCount: Int = 3, + meshNeighborCount: Int = 2, + messageCount: Int = 10, + sensorPacketCount: Int = 5, + avgChannelUtilization: Double = 15.0, + avgAirtimeRate: Double = 3.0, + packetSuccessRate: Double = 0.95, + packetFailureRate: Double = 0.05, + ) = DiscoveryPresetResultEntity( + id = id, + sessionId = sessionId, + presetName = name, + uniqueNodes = uniqueNodes, + directNeighborCount = directNeighborCount, + meshNeighborCount = meshNeighborCount, + messageCount = messageCount, + sensorPacketCount = sensorPacketCount, + avgChannelUtilization = avgChannelUtilization, + avgAirtimeRate = avgAirtimeRate, + packetSuccessRate = packetSuccessRate, + packetFailureRate = packetFailureRate, + ) + + // ---- generateSessionSummary ---- + + @Test + fun emptyPresetsReturnsNoPresetsMessage() { + val result = generator.generateSessionSummary(session(), emptyList()) + assertEquals("No presets were scanned during this session.", result) + } + + @Test + fun singlePresetSessionMentionsPresetName() { + val p = preset(name = "LongFast", uniqueNodes = 7) + val result = generator.generateSessionSummary(session(), listOf(p)) + assertContains(result, "LongFast") + assertContains(result, "7") + } + + @Test + fun singlePresetSessionIncludesChannelUtilization() { + val p = preset(name = "LongFast", avgChannelUtilization = 12.5) + val result = generator.generateSessionSummary(session(), listOf(p)) + assertContains(result, "12.5%") + } + + @Test + fun multiPresetSessionRanksByNodeCount() { + val winner = preset(id = 1, name = "LongFast", uniqueNodes = 12, avgChannelUtilization = 20.0) + val loser = preset(id = 2, name = "ShortFast", uniqueNodes = 4, avgChannelUtilization = 10.0) + val result = generator.generateSessionSummary(session(), listOf(loser, winner)) + assertContains(result, "LongFast") + assertContains(result, "most nodes") + } + + @Test + fun multiPresetSessionMentionsAlternativePresets() { + val winner = preset(id = 1, name = "LongFast", uniqueNodes = 12, avgChannelUtilization = 20.0) + val loser = preset(id = 2, name = "ShortFast", uniqueNodes = 4, avgChannelUtilization = 10.0) + val result = generator.generateSessionSummary(session(), listOf(loser, winner)) + assertContains(result, "ShortFast") + assertContains(result, "4 node") + } + + @Test + fun highCongestionGeneratesWarning() { + val congested = preset(name = "LongFast", avgChannelUtilization = 35.0) + val result = generator.generateSessionSummary(session(), listOf(congested)) + assertContains(result, "congestion") + assertContains(result, "LongFast") + } + + @Test + fun lowCongestionNoWarning() { + val clear = preset(name = "LongFast", avgChannelUtilization = 10.0) + val result = generator.generateSessionSummary(session(), listOf(clear)) + assertFalse(result.contains("congestion"), "Should not mention congestion at 10%") + } + + @Test + fun chatDominatedTrafficNoted() { + val chatHeavy = preset(name = "LongFast", messageCount = 100, sensorPacketCount = 5) + val result = generator.generateSessionSummary(session(), listOf(chatHeavy)) + assertContains(result, "chat-dominated") + } + + @Test + fun sensorDominatedTrafficNoted() { + val sensorHeavy = preset(name = "LongFast", messageCount = 2, sensorPacketCount = 50) + val result = generator.generateSessionSummary(session(), listOf(sensorHeavy)) + assertContains(result, "sensor-dominated") + } + + @Test + fun equalTrafficMixNoNote() { + val balanced = preset(name = "LongFast", messageCount = 0, sensorPacketCount = 0) + val result = generator.generateSessionSummary(session(), listOf(balanced)) + assertFalse(result.contains("dominated"), "Should not mention traffic mix when counts are zero") + } + + @Test + fun completedSessionRecommendationSaysCompleted() { + val p = preset(name = "LongFast") + val result = generator.generateSessionSummary(session(completionStatus = "complete"), listOf(p)) + assertContains(result, "completed") + assertContains(result, "Recommendation") + } + + @Test + fun stoppedSessionRecommendationSaysPartial() { + val p = preset(name = "LongFast") + val result = generator.generateSessionSummary(session(completionStatus = "stopped"), listOf(p)) + assertContains(result, "partially completed") + } + + @Test + fun recommendationIncludesBestPresetName() { + val winner = preset(id = 1, name = "MediumSlow", uniqueNodes = 15, avgChannelUtilization = 5.0) + val loser = preset(id = 2, name = "LongFast", uniqueNodes = 3, avgChannelUtilization = 5.0) + val result = generator.generateSessionSummary(session(), listOf(loser, winner)) + assertContains(result, "Recommendation: Use MediumSlow") + } + + // ---- generatePresetSummary ---- + + @Test + fun presetSummaryIncludesPresetName() { + val result = generator.generatePresetSummary(preset(name = "LongFast")) + assertTrue(result.startsWith("LongFast")) + } + + @Test + fun presetSummaryIncludesNodeCounts() { + val p = preset(uniqueNodes = 8, directNeighborCount = 5, meshNeighborCount = 3) + val result = generator.generatePresetSummary(p) + assertContains(result, "8 nodes") + assertContains(result, "5 direct") + assertContains(result, "3 mesh") + } + + @Test + fun presetSummaryIncludesChannelUtilization() { + val p = preset(avgChannelUtilization = 42.7) + val result = generator.generatePresetSummary(p) + assertContains(result, "42.7%") + assertContains(result, "channel utilization") + } + + @Test + fun presetSummaryHighCongestionMarked() { + val p = preset(avgChannelUtilization = 30.0) + val result = generator.generatePresetSummary(p) + assertContains(result, "congested") + } + + @Test + fun presetSummaryLowCongestionNotMarked() { + val p = preset(avgChannelUtilization = 20.0) + val result = generator.generatePresetSummary(p) + assertFalse(result.contains("congested")) + } + + @Test + fun presetSummaryChatDominated() { + val p = preset(messageCount = 50, sensorPacketCount = 5) + val result = generator.generatePresetSummary(p) + assertContains(result, "chat-dominated") + } + + @Test + fun presetSummarySensorDominated() { + val p = preset(messageCount = 2, sensorPacketCount = 40) + val result = generator.generatePresetSummary(p) + assertContains(result, "sensor-dominated") + } + + @Test + fun presetSummaryKnownPresetIncludesDataRate() { + val p = preset(name = "Long Fast") + val result = generator.generatePresetSummary(p) + // "Long Fast" matches LoRaPresetReference key and should include data rate + assertTrue(result.contains("kbps") || result.contains("bps"), "Should include data rate for known preset") + } + + // ---- buildSessionPrompt ---- + + @Test + fun sessionPromptContainsInstructions() { + val p = preset(name = "LongFast", uniqueNodes = 5) + val result = generator.buildSessionPrompt(session(), listOf(p)) + assertContains(result, "Analyze this Meshtastic mesh radio discovery scan") + assertContains(result, "recommend the best modem preset") + assertContains(result, "concise") + } + + @Test + fun sessionPromptContainsSessionMetadata() { + val s = session(totalUniqueNodes = 15, completionStatus = "complete") + val p = preset(name = "LongFast") + val result = generator.buildSessionPrompt(s, listOf(p)) + assertContains(result, "15 unique nodes") + assertContains(result, "complete") + } + + @Test + fun sessionPromptContainsPresetData() { + val p = preset(name = "ShortFast", uniqueNodes = 8, messageCount = 20, sensorPacketCount = 3) + val result = generator.buildSessionPrompt(session(), listOf(p)) + assertContains(result, "ShortFast") + assertContains(result, "Nodes: 8") + assertContains(result, "Messages: 20") + } + + @Test + fun sessionPromptContainsChannelUtilization() { + val p = preset(name = "LongFast", avgChannelUtilization = 33.5, avgAirtimeRate = 5.2) + val result = generator.buildSessionPrompt(session(), listOf(p)) + assertContains(result, "33.5") + assertContains(result, "5.2") + } + + @Test + fun sessionPromptContainsCongestionGuidance() { + val p = preset(name = "LongFast") + val result = generator.buildSessionPrompt(session(), listOf(p)) + assertContains(result, "Channel util >25% indicates congestion") + } + + // ---- buildPresetPrompt ---- + + @Test + fun presetPromptContainsPresetName() { + val p = preset(name = "MediumFast") + val result = generator.buildPresetPrompt(p) + assertContains(result, "MediumFast") + assertContains(result, "summarize") + } + + @Test + fun presetPromptContainsMetrics() { + val p = preset( + name = "LongFast", + uniqueNodes = 6, + directNeighborCount = 4, + meshNeighborCount = 2, + avgChannelUtilization = 18.0, + ) + val result = generator.buildPresetPrompt(p) + assertContains(result, "Nodes: 6") + assertContains(result, "Direct: 4") + assertContains(result, "Mesh: 2") + assertContains(result, "18.0") + } + + @Test + fun presetPromptContainsGuidanceContext() { + val p = preset(name = "LongFast") + val result = generator.buildPresetPrompt(p) + assertContains(result, "traffic pattern") + assertContains(result, "node density") + } +}