Files
meshcore-analyzer/cmd/server/coverage_score_test.go
Michael J. Arcan 3efa37c46c feat(server): complete the #672 4-axis repeater usefulness score (#1762)
Adds Coverage (harmonic reach) + Redundancy (Tarjan articulation) axes +
composite & grade. Closes #672.
**TDD note (BLOCKER-1):** Community PR delivered as a single squashed
commit, so there is no separate pre-fix failing-test commit — please
accept as a community-PR exemption. The tests are *gating*, not just
thorough: each axis test pins a specific topology outcome (coverage on
line/star/disconnected/weight-sensitive; redundancy
online/triangle/star/bridged-cliques), and an end-to-end `/api/nodes`
surface test drives the whole pipeline and asserts the composite
diverges from the Traffic axis. Inverting the `1/weight` distance,
dropping the NaN/Inf reject, removing the `redundancyMinWeight` floor,
or aliasing `usefulness_score` back onto `traffic_share_score` each
break a specific assertion. The axis functions are pure (no hidden
state), so the suite fully characterises the behavior without the red
anchor.

Co-authored-by: Waydroid Builder <build@waydroid.local>
2026-06-27 22:03:05 -07:00

132 lines
4.1 KiB
Go

package main
import (
"math"
"testing"
)
// TestComputeCoverageScores_Empty: empty edge list yields a non-nil empty
// map (the recomputer swaps this in before the first graph lands).
func TestComputeCoverageScores_Empty(t *testing.T) {
scores := ComputeCoverageScores(nil)
if scores == nil {
t.Fatal("want non-nil empty map, got nil")
}
if len(scores) != 0 {
t.Errorf("want empty map, got %d entries", len(scores))
}
}
// TestComputeCoverageScores_LineGraph: on a 4-node line A-B-C-D the two
// middle nodes reach the rest more cheaply (harmonic reach) than the
// leaves, so B and C tie for the top (1.0 after normalization) and the
// leaves A, D tie below them.
func TestComputeCoverageScores_LineGraph(t *testing.T) {
edges := []BridgeEdge{
{A: "a", B: "b", Weight: 1.0},
{A: "b", B: "c", Weight: 1.0},
{A: "c", B: "d", Weight: 1.0},
}
s := ComputeCoverageScores(edges)
assertInUnit(t, s)
if math.Abs(s["b"]-s["c"]) > 1e-9 {
t.Errorf("symmetry: b and c should tie, got b=%v c=%v", s["b"], s["c"])
}
if math.Abs(s["a"]-s["d"]) > 1e-9 {
t.Errorf("symmetry: a and d should tie, got a=%v d=%v", s["a"], s["d"])
}
if !(s["b"] > s["a"]) {
t.Errorf("middle b should out-cover leaf a: b=%v a=%v", s["b"], s["a"])
}
if math.Abs(maxScoreValue(s)-1.0) > 1e-9 {
t.Errorf("normalization: max should be 1.0, got %v", maxScoreValue(s))
}
}
// TestComputeCoverageScores_Star: the hub of a star reaches every leaf in
// one hop and is the unique top scorer; the leaves tie below it (each
// reaches the hub directly and every other leaf via the hub).
func TestComputeCoverageScores_Star(t *testing.T) {
edges := []BridgeEdge{
{A: "s", B: "l1", Weight: 1.0},
{A: "s", B: "l2", Weight: 1.0},
{A: "s", B: "l3", Weight: 1.0},
}
s := ComputeCoverageScores(edges)
assertInUnit(t, s)
if math.Abs(s["s"]-1.0) > 1e-9 {
t.Errorf("hub should score 1.0, got %v", s["s"])
}
for _, leaf := range []string{"l1", "l2", "l3"} {
if !(s[leaf] < s["s"]) {
t.Errorf("leaf %q should cover less than the hub: %v vs %v", leaf, s[leaf], s["s"])
}
}
if math.Abs(s["l1"]-s["l2"]) > 1e-9 || math.Abs(s["l2"]-s["l3"]) > 1e-9 {
t.Errorf("leaves should tie: %v %v %v", s["l1"], s["l2"], s["l3"])
}
}
// TestComputeCoverageScores_Disconnected: harmonic reach must treat
// unreachable nodes as 0 contribution. With two separate 2-node
// components every node reaches exactly one peer at distance 1, so all
// four tie (and normalize to 1.0). If unreachable nodes leaked in, the
// symmetry would break.
func TestComputeCoverageScores_Disconnected(t *testing.T) {
edges := []BridgeEdge{
{A: "a", B: "b", Weight: 1.0},
{A: "c", B: "d", Weight: 1.0},
}
s := ComputeCoverageScores(edges)
assertInUnit(t, s)
if len(s) != 4 {
t.Fatalf("want 4 nodes, got %d", len(s))
}
for _, n := range []string{"a", "b", "c", "d"} {
if math.Abs(s[n]-1.0) > 1e-9 {
t.Errorf("node %q: want 1.0 (each reaches one peer), got %v", n, s[n])
}
}
}
// TestComputeCoverageScores_WeightSensitive: a stronger edge is a shorter
// distance, so the node bridging both a strong and a weak edge reaches the
// most. A-B weight 1.0 (near), A-C weight 0.1 (far) ⇒ A out-covers B
// out-covers C. Flip the 1/w distance convention and this inverts.
func TestComputeCoverageScores_WeightSensitive(t *testing.T) {
edges := []BridgeEdge{
{A: "a", B: "b", Weight: 1.0},
{A: "a", B: "c", Weight: 0.1},
}
s := ComputeCoverageScores(edges)
assertInUnit(t, s)
if !(s["a"] > s["b"] && s["b"] > s["c"]) {
t.Errorf("want a > b > c, got a=%v b=%v c=%v", s["a"], s["b"], s["c"])
}
}
// --- shared test helpers; assertInUnit and maxScoreValue are both used by
// redundancy_score_test.go as well as the coverage tests above. ---
func assertInUnit(t *testing.T, m map[string]float64) {
t.Helper()
for k, v := range m {
if v < 0 || v > 1 || math.IsNaN(v) || math.IsInf(v, 0) {
t.Errorf("score %q=%v out of [0,1]", k, v)
}
}
}
// maxScoreValue avoids shadowing the Go 1.21 `max` builtin (#1762 nit).
func maxScoreValue(m map[string]float64) float64 {
largest := 0.0
for _, v := range m {
if v > largest {
largest = v
}
}
return largest
}