1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
// WorkbenchLogic semantics tests (final spec 2026-10-27).
// Pure-JVM: compile Workbench.kt + ScanResult.kt + this file with
// kotlinc against kotlin-stdlib only, then run TestMainKt.
// Verified 2026-10-27: all 21 checks pass.
import com.arblant.railwayipscanner.ScanResult
import com.arblant.railwayipscanner.WorkbenchLogic as W
var failures = 0
fun expect(name: String, cond: Boolean) {
println((if (cond) "PASS " else "FAIL ") + name)
if (!cond) failures++
}
fun main() {
// Results deliberately unsorted; resolve() must rank by latency.
val results = listOf(
ScanResult("10.0.0.3", true, 90),
ScanResult("10.0.0.1", true, 30),
ScanResult("10.0.0.2", true, 60),
ScanResult("10.0.0.9", false, 0) // failures are ignored
)
val sortedIps = listOf("10.0.0.1", "10.0.0.2", "10.0.0.3")
// Top N: distinct IPs ascending latency.
var rows = W.resolve(W.Mode.TOP_N, 3, results, emptyMap())
expect("topN distinct ascending", rows.map { it.ip } == sortedIps)
expect("topN latencies attached", rows.map { it.latencyMs } == listOf(30L, 60L, 90L))
expect("topN no stale/changed", rows.all { !it.stale && !it.changed })
expect("topN no shortage at count==unique", !W.hasShortage(W.Mode.TOP_N, 3, 3))
// Top N wrap: count > unique wraps from the top + shortage flagged.
rows = W.resolve(W.Mode.TOP_N, 5, results, emptyMap())
expect("topN wraps", rows.map { it.ip } == listOf("10.0.0.1", "10.0.0.2", "10.0.0.3", "10.0.0.1", "10.0.0.2"))
expect("shortage flagged", W.hasShortage(W.Mode.TOP_N, 5, 3))
expect("uniqueIps dedups wrapped rows", W.uniqueIps(rows) == sortedIps)
// Best for all: every row the fastest IP, one unique IP to test.
rows = W.resolve(W.Mode.BEST_ALL, 4, results, emptyMap())
expect("bestAll all fastest", rows.all { it.ip == "10.0.0.1" })
expect("bestAll unique = 1", W.uniqueIps(rows) == listOf("10.0.0.1"))
expect("bestAll no shortage concept", !W.hasShortage(W.Mode.BEST_ALL, 4, 3))
// Custom: pick wins over auto; differs-from-auto => changed.
rows = W.resolve(W.Mode.CUSTOM, 3, results, mapOf(1 to "10.0.0.3"))
expect("custom pick applied", rows[1].ip == "10.0.0.3" && rows[1].latencyMs == 90L)
expect("custom changed dot", rows[1].changed && !rows[0].changed && !rows[2].changed)
expect("custom others fall back to topN", rows[0].ip == "10.0.0.1" && rows[2].ip == "10.0.0.3")
// A pick equal to the auto assignment is NOT "changed".
rows = W.resolve(W.Mode.CUSTOM, 3, results, mapOf(0 to "10.0.0.1"))
expect("custom pick == auto not changed", !rows[0].changed && !rows[0].stale)
// Custom after a rescan where the pick vanished: kept + stale.
val fewer = listOf(ScanResult("10.0.0.1", true, 25), ScanResult("10.0.0.2", true, 55))
rows = W.resolve(W.Mode.CUSTOM, 3, fewer, mapOf(2 to "10.0.0.3"))
expect("stale pick kept", rows[2].ip == "10.0.0.3")
expect("stale flagged + no latency", rows[2].stale && rows[2].latencyMs == null)
expect("stale pick counts as changed vs new auto", rows[2].changed)
// Custom picks survive count changes (indexes stable) and mode hops:
// resolve with a bigger count keeps earlier picks.
rows = W.resolve(W.Mode.CUSTOM, 5, results, mapOf(1 to "10.0.0.3"))
expect("pick preserved on count change", rows[1].ip == "10.0.0.3" && rows.size == 5)
// Empty results => no rows in any mode.
expect("empty results no rows", W.resolve(W.Mode.TOP_N, 5, emptyList(), emptyMap()).isEmpty())
expect("empty results no rows custom", W.resolve(W.Mode.CUSTOM, 5, emptyList(), mapOf(0 to "1.2.3.4")).isEmpty())
// Count clamps are the UI's job; logic respects the given count.
rows = W.resolve(W.Mode.TOP_N, 1, results, emptyMap())
expect("count 1", rows.size == 1 && rows[0].ip == "10.0.0.1")
println(if (failures == 0) "ALL WORKBENCH TESTS PASSED" else "$failures FAILURES")
if (failures > 0) kotlin.system.exitProcess(1)
}