Skip to content

Commit 8556fcc

Browse files
jamesarichCopilot
andcommitted
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>
1 parent f6bfefd commit 8556fcc

1 file changed

Lines changed: 316 additions & 0 deletions

File tree

Lines changed: 316 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,316 @@
1+
/*
2+
* Copyright (c) 2026 Meshtastic LLC
3+
*
4+
* This program is free software: you can redistribute it and/or modify
5+
* it under the terms of the GNU General Public License as published by
6+
* the Free Software Foundation, either version 3 of the License, or
7+
* (at your option) any later version.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
* GNU General Public License for more details.
13+
*
14+
* You should have received a copy of the GNU General Public License
15+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
16+
*/
17+
@file:Suppress("MagicNumber")
18+
19+
package org.meshtastic.feature.discovery
20+
21+
import org.meshtastic.core.database.entity.DiscoveryPresetResultEntity
22+
import org.meshtastic.core.database.entity.DiscoverySessionEntity
23+
import kotlin.test.Test
24+
import kotlin.test.assertContains
25+
import kotlin.test.assertEquals
26+
import kotlin.test.assertFalse
27+
import kotlin.test.assertTrue
28+
29+
class DiscoverySummaryGeneratorTest {
30+
31+
private val generator = DiscoverySummaryGenerator()
32+
33+
// ---- Helpers ----
34+
35+
private fun session(
36+
id: Long = 1,
37+
totalUniqueNodes: Int = 10,
38+
completionStatus: String = "complete",
39+
avgChannelUtilization: Double = 0.0,
40+
) = DiscoverySessionEntity(
41+
id = id,
42+
timestamp = 1_000_000L,
43+
presetsScanned = "LongFast,ShortFast",
44+
homePreset = "LongFast",
45+
totalUniqueNodes = totalUniqueNodes,
46+
avgChannelUtilization = avgChannelUtilization,
47+
completionStatus = completionStatus,
48+
)
49+
50+
private fun preset(
51+
id: Long = 1,
52+
sessionId: Long = 1,
53+
name: String = "LongFast",
54+
uniqueNodes: Int = 5,
55+
directNeighborCount: Int = 3,
56+
meshNeighborCount: Int = 2,
57+
messageCount: Int = 10,
58+
sensorPacketCount: Int = 5,
59+
avgChannelUtilization: Double = 15.0,
60+
avgAirtimeRate: Double = 3.0,
61+
packetSuccessRate: Double = 0.95,
62+
packetFailureRate: Double = 0.05,
63+
) = DiscoveryPresetResultEntity(
64+
id = id,
65+
sessionId = sessionId,
66+
presetName = name,
67+
uniqueNodes = uniqueNodes,
68+
directNeighborCount = directNeighborCount,
69+
meshNeighborCount = meshNeighborCount,
70+
messageCount = messageCount,
71+
sensorPacketCount = sensorPacketCount,
72+
avgChannelUtilization = avgChannelUtilization,
73+
avgAirtimeRate = avgAirtimeRate,
74+
packetSuccessRate = packetSuccessRate,
75+
packetFailureRate = packetFailureRate,
76+
)
77+
78+
// ---- generateSessionSummary ----
79+
80+
@Test
81+
fun emptyPresetsReturnsNoPresetsMessage() {
82+
val result = generator.generateSessionSummary(session(), emptyList())
83+
assertEquals("No presets were scanned during this session.", result)
84+
}
85+
86+
@Test
87+
fun singlePresetSessionMentionsPresetName() {
88+
val p = preset(name = "LongFast", uniqueNodes = 7)
89+
val result = generator.generateSessionSummary(session(), listOf(p))
90+
assertContains(result, "LongFast")
91+
assertContains(result, "7")
92+
}
93+
94+
@Test
95+
fun singlePresetSessionIncludesChannelUtilization() {
96+
val p = preset(name = "LongFast", avgChannelUtilization = 12.5)
97+
val result = generator.generateSessionSummary(session(), listOf(p))
98+
assertContains(result, "12.5%")
99+
}
100+
101+
@Test
102+
fun multiPresetSessionRanksByNodeCount() {
103+
val winner = preset(id = 1, name = "LongFast", uniqueNodes = 12, avgChannelUtilization = 20.0)
104+
val loser = preset(id = 2, name = "ShortFast", uniqueNodes = 4, avgChannelUtilization = 10.0)
105+
val result = generator.generateSessionSummary(session(), listOf(loser, winner))
106+
assertContains(result, "LongFast")
107+
assertContains(result, "most nodes")
108+
}
109+
110+
@Test
111+
fun multiPresetSessionMentionsAlternativePresets() {
112+
val winner = preset(id = 1, name = "LongFast", uniqueNodes = 12, avgChannelUtilization = 20.0)
113+
val loser = preset(id = 2, name = "ShortFast", uniqueNodes = 4, avgChannelUtilization = 10.0)
114+
val result = generator.generateSessionSummary(session(), listOf(loser, winner))
115+
assertContains(result, "ShortFast")
116+
assertContains(result, "4 node")
117+
}
118+
119+
@Test
120+
fun highCongestionGeneratesWarning() {
121+
val congested = preset(name = "LongFast", avgChannelUtilization = 35.0)
122+
val result = generator.generateSessionSummary(session(), listOf(congested))
123+
assertContains(result, "congestion")
124+
assertContains(result, "LongFast")
125+
}
126+
127+
@Test
128+
fun lowCongestionNoWarning() {
129+
val clear = preset(name = "LongFast", avgChannelUtilization = 10.0)
130+
val result = generator.generateSessionSummary(session(), listOf(clear))
131+
assertFalse(result.contains("congestion"), "Should not mention congestion at 10%")
132+
}
133+
134+
@Test
135+
fun chatDominatedTrafficNoted() {
136+
val chatHeavy = preset(name = "LongFast", messageCount = 100, sensorPacketCount = 5)
137+
val result = generator.generateSessionSummary(session(), listOf(chatHeavy))
138+
assertContains(result, "chat-dominated")
139+
}
140+
141+
@Test
142+
fun sensorDominatedTrafficNoted() {
143+
val sensorHeavy = preset(name = "LongFast", messageCount = 2, sensorPacketCount = 50)
144+
val result = generator.generateSessionSummary(session(), listOf(sensorHeavy))
145+
assertContains(result, "sensor-dominated")
146+
}
147+
148+
@Test
149+
fun equalTrafficMixNoNote() {
150+
val balanced = preset(name = "LongFast", messageCount = 0, sensorPacketCount = 0)
151+
val result = generator.generateSessionSummary(session(), listOf(balanced))
152+
assertFalse(result.contains("dominated"), "Should not mention traffic mix when counts are zero")
153+
}
154+
155+
@Test
156+
fun completedSessionRecommendationSaysCompleted() {
157+
val p = preset(name = "LongFast")
158+
val result = generator.generateSessionSummary(session(completionStatus = "complete"), listOf(p))
159+
assertContains(result, "completed")
160+
assertContains(result, "Recommendation")
161+
}
162+
163+
@Test
164+
fun stoppedSessionRecommendationSaysPartial() {
165+
val p = preset(name = "LongFast")
166+
val result = generator.generateSessionSummary(session(completionStatus = "stopped"), listOf(p))
167+
assertContains(result, "partially completed")
168+
}
169+
170+
@Test
171+
fun recommendationIncludesBestPresetName() {
172+
val winner = preset(id = 1, name = "MediumSlow", uniqueNodes = 15, avgChannelUtilization = 5.0)
173+
val loser = preset(id = 2, name = "LongFast", uniqueNodes = 3, avgChannelUtilization = 5.0)
174+
val result = generator.generateSessionSummary(session(), listOf(loser, winner))
175+
assertContains(result, "Recommendation: Use MediumSlow")
176+
}
177+
178+
// ---- generatePresetSummary ----
179+
180+
@Test
181+
fun presetSummaryIncludesPresetName() {
182+
val result = generator.generatePresetSummary(preset(name = "LongFast"))
183+
assertTrue(result.startsWith("LongFast"))
184+
}
185+
186+
@Test
187+
fun presetSummaryIncludesNodeCounts() {
188+
val p = preset(uniqueNodes = 8, directNeighborCount = 5, meshNeighborCount = 3)
189+
val result = generator.generatePresetSummary(p)
190+
assertContains(result, "8 nodes")
191+
assertContains(result, "5 direct")
192+
assertContains(result, "3 mesh")
193+
}
194+
195+
@Test
196+
fun presetSummaryIncludesChannelUtilization() {
197+
val p = preset(avgChannelUtilization = 42.7)
198+
val result = generator.generatePresetSummary(p)
199+
assertContains(result, "42.7%")
200+
assertContains(result, "channel utilization")
201+
}
202+
203+
@Test
204+
fun presetSummaryHighCongestionMarked() {
205+
val p = preset(avgChannelUtilization = 30.0)
206+
val result = generator.generatePresetSummary(p)
207+
assertContains(result, "congested")
208+
}
209+
210+
@Test
211+
fun presetSummaryLowCongestionNotMarked() {
212+
val p = preset(avgChannelUtilization = 20.0)
213+
val result = generator.generatePresetSummary(p)
214+
assertFalse(result.contains("congested"))
215+
}
216+
217+
@Test
218+
fun presetSummaryChatDominated() {
219+
val p = preset(messageCount = 50, sensorPacketCount = 5)
220+
val result = generator.generatePresetSummary(p)
221+
assertContains(result, "chat-dominated")
222+
}
223+
224+
@Test
225+
fun presetSummarySensorDominated() {
226+
val p = preset(messageCount = 2, sensorPacketCount = 40)
227+
val result = generator.generatePresetSummary(p)
228+
assertContains(result, "sensor-dominated")
229+
}
230+
231+
@Test
232+
fun presetSummaryKnownPresetIncludesDataRate() {
233+
val p = preset(name = "Long Fast")
234+
val result = generator.generatePresetSummary(p)
235+
// "Long Fast" matches LoRaPresetReference key and should include data rate
236+
assertTrue(result.contains("kbps") || result.contains("bps"), "Should include data rate for known preset")
237+
}
238+
239+
// ---- buildSessionPrompt ----
240+
241+
@Test
242+
fun sessionPromptContainsInstructions() {
243+
val p = preset(name = "LongFast", uniqueNodes = 5)
244+
val result = generator.buildSessionPrompt(session(), listOf(p))
245+
assertContains(result, "Analyze this Meshtastic mesh radio discovery scan")
246+
assertContains(result, "recommend the best modem preset")
247+
assertContains(result, "concise")
248+
}
249+
250+
@Test
251+
fun sessionPromptContainsSessionMetadata() {
252+
val s = session(totalUniqueNodes = 15, completionStatus = "complete")
253+
val p = preset(name = "LongFast")
254+
val result = generator.buildSessionPrompt(s, listOf(p))
255+
assertContains(result, "15 unique nodes")
256+
assertContains(result, "complete")
257+
}
258+
259+
@Test
260+
fun sessionPromptContainsPresetData() {
261+
val p = preset(name = "ShortFast", uniqueNodes = 8, messageCount = 20, sensorPacketCount = 3)
262+
val result = generator.buildSessionPrompt(session(), listOf(p))
263+
assertContains(result, "ShortFast")
264+
assertContains(result, "Nodes: 8")
265+
assertContains(result, "Messages: 20")
266+
}
267+
268+
@Test
269+
fun sessionPromptContainsChannelUtilization() {
270+
val p = preset(name = "LongFast", avgChannelUtilization = 33.5, avgAirtimeRate = 5.2)
271+
val result = generator.buildSessionPrompt(session(), listOf(p))
272+
assertContains(result, "33.5")
273+
assertContains(result, "5.2")
274+
}
275+
276+
@Test
277+
fun sessionPromptContainsCongestionGuidance() {
278+
val p = preset(name = "LongFast")
279+
val result = generator.buildSessionPrompt(session(), listOf(p))
280+
assertContains(result, "Channel util >25% indicates congestion")
281+
}
282+
283+
// ---- buildPresetPrompt ----
284+
285+
@Test
286+
fun presetPromptContainsPresetName() {
287+
val p = preset(name = "MediumFast")
288+
val result = generator.buildPresetPrompt(p)
289+
assertContains(result, "MediumFast")
290+
assertContains(result, "summarize")
291+
}
292+
293+
@Test
294+
fun presetPromptContainsMetrics() {
295+
val p = preset(
296+
name = "LongFast",
297+
uniqueNodes = 6,
298+
directNeighborCount = 4,
299+
meshNeighborCount = 2,
300+
avgChannelUtilization = 18.0,
301+
)
302+
val result = generator.buildPresetPrompt(p)
303+
assertContains(result, "Nodes: 6")
304+
assertContains(result, "Direct: 4")
305+
assertContains(result, "Mesh: 2")
306+
assertContains(result, "18.0")
307+
}
308+
309+
@Test
310+
fun presetPromptContainsGuidanceContext() {
311+
val p = preset(name = "LongFast")
312+
val result = generator.buildPresetPrompt(p)
313+
assertContains(result, "traffic pattern")
314+
assertContains(result, "node density")
315+
}
316+
}

0 commit comments

Comments
 (0)