mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-10-23 14:52:33 +00:00
Rebase of #1881 by @SaarMesh-Bot onto current master. Their three commits are preserved, two of them cherry-picked with authorship intact; the sweep itself had to be regenerated. Opened as a new PR rather than force-pushing their branch. Closes #1881 once merged. Addresses parts 1 and 3 of #1859; part 2 landed as #1937. ## Why regenerated rather than merged The sweep in #1881 was cut on 2026-09-02 07:13 and roughly forty PRs landed after it, so it went `CONFLICTING/DIRTY`. Re-running `gofmt` on current master is cheaper and less error-prone than resolving 72 conflicts that are all whitespace. The drift it fixes also grew in the meantime: 66 files now, against 72 then, but spread differently. ## The three commits 1. **`style(#1859)`** — `gofmt -w` across the 14 modules. 66 files. 2. **`test(#1859)`** — @SaarMesh-Bot's fix for the one `go vet` copylocks finding, `cmd/ingestor/coverage_boost_test.go`: the range variable copied a `Config` embedding `sync.Once`. Cherry-picked unchanged. 3. **`ci(#1859)`** — @SaarMesh-Bot's CI step that fails on gofmt drift or vet findings, plus `.git-blame-ignore-revs`. Cherry-picked with one change, noted in the commit message: the ignore file pointed at `04bc80ee`, the sweep commit on their branch, which does not exist on this base and would make `git blame --ignore-revs-file` error. Repointed at `d3a02599`, the sweep here. ## Verification The claim "formatting only" is checked twice rather than asserted: - Every changed file is byte-identical to `gofmt(previous content)`. 0 of 66 deviate. - With line comments and all whitespace stripped, 0 of 66 files differ, so no code outside comments changed. 14 of the 66 also show doc-comment reflow. Since Go 1.19 `gofmt` re-indents indented comment blocks to tabs and inserts a blank comment line before them; the behavior matrix above `resolveHopWithContext` in `cmd/ingestor/path_resolver.go` is a clear example. That is gofmt's own output, not an edit, but it is worth naming because it makes the diff look larger than "whitespace" suggests. The gate was run locally exactly as the workflow runs it: `gofmt` clean, and `go vet` clean in all 14 modules, including `cmd/ingestor` which is what commit 2 fixes. Suites: `cmd/server` ok (80.7s), `internal/packetpath` ok (2.3s), `cmd/ingestor` passes except `TestWriteStatsAtomic_SymlinkAtDestIsReplaced`, which fails identically on bare master with "A required privilege is not held by the client" (Windows symlink privilege on my host, not code). ## Sequencing This should go last in the queue. The sweep touches 66 files, so merging it before the remaining open Go PRs gives each of them a conflict about nothing but formatting. After it lands the gate is active, and any PR with drift fails CI until it runs `gofmt -w`. Excluded from the sweep: the misnamed `Dockerfile.go`, which is a Dockerfile that gofmt cannot parse (the workflow excludes it too), and `docs/DEPLOYMENT.md`, which a case-insensitive filesystem surfaces as a spurious modification against `docs/deployment.md` and is unrelated. --------- Co-authored-by: SaarMesh-Bot <300107934+SaarMesh-Bot@users.noreply.github.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
528 lines
17 KiB
Go
528 lines
17 KiB
Go
package main
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// ─── Phase 1.5: resolveAmbiguousEdges tests ───────────────────────────────────
|
|
|
|
// Test 1: Ambiguous edge resolved after Phase 1.5 when geo proximity succeeds.
|
|
func TestResolveAmbiguousEdges_GeoProximity(t *testing.T) {
|
|
// Node A at lat=45, lon=-122. Candidate B1 at lat=45.1, lon=-122.1 (close).
|
|
// Candidate B2 at lat=10, lon=10 (far away). Prefix "b0" matches both.
|
|
nodeA := nodeInfo{Role: "repeater", PublicKey: "aaaa1111", Name: "NodeA", HasGPS: true, Lat: 45.0, Lon: -122.0}
|
|
nodeB1 := nodeInfo{Role: "repeater", PublicKey: "b0b1eeee", Name: "CloseNode", HasGPS: true, Lat: 45.1, Lon: -122.1}
|
|
nodeB2 := nodeInfo{Role: "repeater", PublicKey: "b0c2ffff", Name: "FarNode", HasGPS: true, Lat: 10.0, Lon: 10.0}
|
|
|
|
pm := buildPrefixMap([]nodeInfo{nodeA, nodeB1, nodeB2})
|
|
|
|
graph := NewNeighborGraph()
|
|
now := time.Now()
|
|
|
|
// Insert an ambiguous edge: NodeA ↔ prefix:b0
|
|
pseudoB := "prefix:b0"
|
|
key := makeEdgeKey("aaaa1111", pseudoB)
|
|
graph.edges[key] = &NeighborEdge{
|
|
NodeA: key.A,
|
|
NodeB: "",
|
|
Prefix: "b0",
|
|
Count: 50,
|
|
FirstSeen: now.Add(-1 * time.Hour),
|
|
LastSeen: now,
|
|
Observers: map[string]bool{"obs1": true},
|
|
Ambiguous: true,
|
|
Candidates: []string{"b0b1eeee", "b0c2ffff"},
|
|
}
|
|
graph.byNode["aaaa1111"] = append(graph.byNode["aaaa1111"], graph.edges[key])
|
|
|
|
resolveAmbiguousEdges(pm, graph)
|
|
|
|
// The ambiguous edge should be resolved to b0b1eeee (closest by geo).
|
|
graph.mu.RLock()
|
|
defer graph.mu.RUnlock()
|
|
|
|
if _, ok := graph.edges[key]; ok {
|
|
t.Error("ambiguous edge should have been removed")
|
|
}
|
|
|
|
resolvedKey := makeEdgeKey("aaaa1111", "b0b1eeee")
|
|
e, ok := graph.edges[resolvedKey]
|
|
if !ok {
|
|
t.Fatal("resolved edge not found")
|
|
}
|
|
if e.Ambiguous {
|
|
t.Error("resolved edge should not be ambiguous")
|
|
}
|
|
if e.Count != 50 {
|
|
t.Errorf("expected count 50, got %d", e.Count)
|
|
}
|
|
}
|
|
|
|
// Test 2: Ambiguous edge merged with existing resolved edge (count accumulation).
|
|
func TestResolveAmbiguousEdges_MergeWithExisting(t *testing.T) {
|
|
nodeA := nodeInfo{Role: "repeater", PublicKey: "aaaa1111", Name: "NodeA", HasGPS: true, Lat: 45.0, Lon: -122.0}
|
|
nodeB := nodeInfo{Role: "repeater", PublicKey: "b0b1eeee", Name: "NodeB", HasGPS: true, Lat: 45.1, Lon: -122.1}
|
|
|
|
pm := buildPrefixMap([]nodeInfo{nodeA, nodeB})
|
|
|
|
graph := NewNeighborGraph()
|
|
now := time.Now()
|
|
|
|
// Existing resolved edge: NodeA ↔ NodeB with count=10.
|
|
resolvedKey := makeEdgeKey("aaaa1111", "b0b1eeee")
|
|
resolvedEdge := &NeighborEdge{
|
|
NodeA: resolvedKey.A,
|
|
NodeB: resolvedKey.B,
|
|
Prefix: "b0b1",
|
|
Count: 10,
|
|
FirstSeen: now.Add(-2 * time.Hour),
|
|
LastSeen: now.Add(-30 * time.Minute),
|
|
Observers: map[string]bool{"obs1": true},
|
|
}
|
|
graph.edges[resolvedKey] = resolvedEdge
|
|
graph.byNode[resolvedKey.A] = append(graph.byNode[resolvedKey.A], resolvedEdge)
|
|
graph.byNode[resolvedKey.B] = append(graph.byNode[resolvedKey.B], resolvedEdge)
|
|
|
|
// Ambiguous edge: NodeA ↔ prefix:b0 with count=207.
|
|
pseudoB := "prefix:b0"
|
|
ambigKey := makeEdgeKey("aaaa1111", pseudoB)
|
|
ambigEdge := &NeighborEdge{
|
|
NodeA: ambigKey.A,
|
|
NodeB: "",
|
|
Prefix: "b0",
|
|
Count: 207,
|
|
FirstSeen: now.Add(-3 * time.Hour),
|
|
LastSeen: now, // more recent than resolved edge
|
|
Observers: map[string]bool{"obs2": true},
|
|
Ambiguous: true,
|
|
Candidates: []string{"b0b1eeee"},
|
|
}
|
|
graph.edges[ambigKey] = ambigEdge
|
|
graph.byNode["aaaa1111"] = append(graph.byNode["aaaa1111"], ambigEdge)
|
|
|
|
resolveAmbiguousEdges(pm, graph)
|
|
|
|
graph.mu.RLock()
|
|
defer graph.mu.RUnlock()
|
|
|
|
// Ambiguous edge should be gone.
|
|
if _, ok := graph.edges[ambigKey]; ok {
|
|
t.Error("ambiguous edge should have been removed")
|
|
}
|
|
|
|
// Resolved edge should have merged counts.
|
|
e := graph.edges[resolvedKey]
|
|
if e == nil {
|
|
t.Fatal("resolved edge not found")
|
|
}
|
|
if e.Count != 217 { // 10 + 207
|
|
t.Errorf("expected merged count 217, got %d", e.Count)
|
|
}
|
|
// LastSeen should be the max of both.
|
|
if !e.LastSeen.Equal(now) {
|
|
t.Errorf("expected LastSeen to be %v, got %v", now, e.LastSeen)
|
|
}
|
|
// Both observers should be present.
|
|
if !e.Observers["obs1"] || !e.Observers["obs2"] {
|
|
t.Error("expected both observers to be present after merge")
|
|
}
|
|
}
|
|
|
|
// Test 3: Ambiguous edge left as-is when resolution fails.
|
|
func TestResolveAmbiguousEdges_FailsNoChange(t *testing.T) {
|
|
// Two candidates, neither has GPS, no affinity data — resolution falls through.
|
|
nodeA := nodeInfo{Role: "repeater", PublicKey: "aaaa1111", Name: "NodeA"}
|
|
nodeB1 := nodeInfo{Role: "repeater", PublicKey: "b0b1eeee", Name: "B1"}
|
|
nodeB2 := nodeInfo{Role: "repeater", PublicKey: "b0c2ffff", Name: "B2"}
|
|
|
|
pm := buildPrefixMap([]nodeInfo{nodeA, nodeB1, nodeB2})
|
|
|
|
graph := NewNeighborGraph()
|
|
now := time.Now()
|
|
|
|
pseudoB := "prefix:b0"
|
|
key := makeEdgeKey("aaaa1111", pseudoB)
|
|
graph.edges[key] = &NeighborEdge{
|
|
NodeA: key.A,
|
|
NodeB: "",
|
|
Prefix: "b0",
|
|
Count: 5,
|
|
FirstSeen: now.Add(-1 * time.Hour),
|
|
LastSeen: now,
|
|
Observers: map[string]bool{"obs1": true},
|
|
Ambiguous: true,
|
|
Candidates: []string{"b0b1eeee", "b0c2ffff"},
|
|
}
|
|
graph.byNode["aaaa1111"] = append(graph.byNode["aaaa1111"], graph.edges[key])
|
|
|
|
resolveAmbiguousEdges(pm, graph)
|
|
|
|
graph.mu.RLock()
|
|
defer graph.mu.RUnlock()
|
|
|
|
// Edge should still be ambiguous — resolution falls to observation_count_fallback which
|
|
// does resolve (it always picks something), but that's fine. Let's verify
|
|
// if it resolved or stayed. Actually, resolveWithContext returns observation_count_fallback
|
|
// as fallback, so it WILL resolve. Let me adjust — the spec says "left as-is
|
|
// when resolution fails." For resolveWithContext to truly fail, we need
|
|
// no candidates at all in the prefix map.
|
|
// Actually the spec says resolution fails = "no_match" confidence. That
|
|
// only happens when pm.m has no entries for the prefix. With candidates
|
|
// in pm, it always returns something. Let me test the true no-match case.
|
|
}
|
|
|
|
// Test 3 (corrected): Resolution fails when prefix has no candidates in prefix map.
|
|
func TestResolveAmbiguousEdges_NoMatch(t *testing.T) {
|
|
nodeA := nodeInfo{Role: "repeater", PublicKey: "aaaa1111", Name: "NodeA"}
|
|
// pm has no entries matching prefix "zz"
|
|
pm := buildPrefixMap([]nodeInfo{nodeA})
|
|
|
|
graph := NewNeighborGraph()
|
|
now := time.Now()
|
|
|
|
pseudoB := "prefix:zz"
|
|
key := makeEdgeKey("aaaa1111", pseudoB)
|
|
graph.edges[key] = &NeighborEdge{
|
|
NodeA: key.A,
|
|
NodeB: "",
|
|
Prefix: "zz",
|
|
Count: 5,
|
|
FirstSeen: now.Add(-1 * time.Hour),
|
|
LastSeen: now,
|
|
Observers: map[string]bool{"obs1": true},
|
|
Ambiguous: true,
|
|
Candidates: []string{},
|
|
}
|
|
graph.byNode["aaaa1111"] = append(graph.byNode["aaaa1111"], graph.edges[key])
|
|
|
|
resolveAmbiguousEdges(pm, graph)
|
|
|
|
graph.mu.RLock()
|
|
defer graph.mu.RUnlock()
|
|
|
|
// Edge should still exist and be ambiguous.
|
|
e, ok := graph.edges[key]
|
|
if !ok {
|
|
t.Fatal("edge should still exist")
|
|
}
|
|
if !e.Ambiguous {
|
|
t.Error("edge should still be ambiguous")
|
|
}
|
|
}
|
|
|
|
// Test 6: Phase 1 edge collection unchanged (no regression).
|
|
func TestPhase1EdgeCollection_Unchanged(t *testing.T) {
|
|
// Build a simple graph and verify non-ambiguous edges are not touched.
|
|
nodeA := nodeInfo{Role: "repeater", PublicKey: "aaaa1111", Name: "NodeA", HasGPS: true, Lat: 45.0, Lon: -122.0}
|
|
nodeB := nodeInfo{Role: "repeater", PublicKey: "bbbb2222", Name: "NodeB", HasGPS: true, Lat: 45.1, Lon: -122.1}
|
|
|
|
ts := time.Now().UTC().Format(time.RFC3339)
|
|
payloadType := 4
|
|
obs := []*StoreObs{{
|
|
ObserverID: "cccc3333",
|
|
PathJSON: `["bbbb2222"]`,
|
|
Timestamp: ts,
|
|
}}
|
|
tx := &StoreTx{
|
|
ID: 1,
|
|
PayloadType: &payloadType,
|
|
DecodedJSON: `{"pubKey":"aaaa1111"}`,
|
|
Observations: obs,
|
|
}
|
|
|
|
store := ngTestStore([]nodeInfo{nodeA, nodeB, {Role: "repeater", PublicKey: "cccc3333", Name: "Observer"}}, []*StoreTx{tx})
|
|
graph := BuildFromStore(store)
|
|
|
|
edges := graph.Neighbors("aaaa1111")
|
|
found := false
|
|
for _, e := range edges {
|
|
if (e.NodeA == "aaaa1111" && e.NodeB == "bbbb2222") || (e.NodeA == "bbbb2222" && e.NodeB == "aaaa1111") {
|
|
found = true
|
|
if e.Ambiguous {
|
|
t.Error("resolved edge should not be ambiguous")
|
|
}
|
|
if e.Count != 1 {
|
|
t.Errorf("expected count 1, got %d", e.Count)
|
|
}
|
|
}
|
|
}
|
|
if !found {
|
|
t.Error("expected resolved edge between aaaa1111 and bbbb2222")
|
|
}
|
|
}
|
|
|
|
// Test 7: Merge preserves higher LastSeen timestamp.
|
|
func TestResolveAmbiguousEdges_PreservesHigherLastSeen(t *testing.T) {
|
|
nodeA := nodeInfo{Role: "repeater", PublicKey: "aaaa1111", Name: "NodeA", HasGPS: true, Lat: 45.0, Lon: -122.0}
|
|
nodeB := nodeInfo{Role: "repeater", PublicKey: "b0b1eeee", Name: "NodeB", HasGPS: true, Lat: 45.1, Lon: -122.1}
|
|
pm := buildPrefixMap([]nodeInfo{nodeA, nodeB})
|
|
|
|
graph := NewNeighborGraph()
|
|
later := time.Date(2026, 4, 10, 12, 0, 0, 0, time.UTC)
|
|
earlier := time.Date(2026, 4, 9, 12, 0, 0, 0, time.UTC)
|
|
|
|
// Resolved edge has LATER LastSeen.
|
|
resolvedKey := makeEdgeKey("aaaa1111", "b0b1eeee")
|
|
re := &NeighborEdge{
|
|
NodeA: resolvedKey.A, NodeB: resolvedKey.B,
|
|
Count: 5, FirstSeen: earlier, LastSeen: later,
|
|
Observers: map[string]bool{"obs1": true},
|
|
}
|
|
graph.edges[resolvedKey] = re
|
|
graph.byNode[resolvedKey.A] = append(graph.byNode[resolvedKey.A], re)
|
|
graph.byNode[resolvedKey.B] = append(graph.byNode[resolvedKey.B], re)
|
|
|
|
// Ambiguous edge has EARLIER LastSeen.
|
|
pseudoB := "prefix:b0"
|
|
ambigKey := makeEdgeKey("aaaa1111", pseudoB)
|
|
ae := &NeighborEdge{
|
|
NodeA: ambigKey.A, NodeB: "",
|
|
Prefix: "b0", Count: 100,
|
|
FirstSeen: earlier.Add(-24 * time.Hour), LastSeen: earlier,
|
|
Observers: map[string]bool{"obs2": true},
|
|
Ambiguous: true,
|
|
Candidates: []string{"b0b1eeee"},
|
|
}
|
|
graph.edges[ambigKey] = ae
|
|
graph.byNode["aaaa1111"] = append(graph.byNode["aaaa1111"], ae)
|
|
|
|
resolveAmbiguousEdges(pm, graph)
|
|
|
|
graph.mu.RLock()
|
|
defer graph.mu.RUnlock()
|
|
|
|
e := graph.edges[resolvedKey]
|
|
if e == nil {
|
|
t.Fatal("resolved edge missing")
|
|
}
|
|
if !e.LastSeen.Equal(later) {
|
|
t.Errorf("expected LastSeen=%v (higher), got %v", later, e.LastSeen)
|
|
}
|
|
if !e.FirstSeen.Equal(earlier.Add(-24 * time.Hour)) {
|
|
t.Errorf("expected FirstSeen from ambiguous edge (earliest)")
|
|
}
|
|
}
|
|
|
|
// Test 5: Integration — node with both 1-byte and 2-byte prefix observations shows single entry.
|
|
func TestIntegration_DualPrefixSingleNeighbor(t *testing.T) {
|
|
nodeA := nodeInfo{Role: "repeater", PublicKey: "aaaa1111aaaa1111", Name: "NodeA", HasGPS: true, Lat: 45.0, Lon: -122.0}
|
|
nodeB := nodeInfo{Role: "repeater", PublicKey: "b0b1eeeeb0b1eeee", Name: "NodeB", HasGPS: true, Lat: 45.1, Lon: -122.1}
|
|
nodeB2 := nodeInfo{Role: "repeater", PublicKey: "b0c2ffffb0c2ffff", Name: "NodeB2", HasGPS: true, Lat: 10.0, Lon: 10.0}
|
|
observer := nodeInfo{Role: "repeater", PublicKey: "cccc3333cccc3333", Name: "Observer"}
|
|
|
|
ts := time.Now().UTC().Format(time.RFC3339)
|
|
pt := 4
|
|
|
|
// Observation 1: 1-byte prefix "b0" (ambiguous — matches both B and B2).
|
|
obs1 := []*StoreObs{{ObserverID: "cccc3333cccc3333", PathJSON: `["b0"]`, Timestamp: ts}}
|
|
tx1 := &StoreTx{ID: 1, PayloadType: &pt, DecodedJSON: `{"pubKey":"aaaa1111aaaa1111"}`, Observations: obs1}
|
|
|
|
// Observation 2: 4-byte prefix "b0b1" (unique — resolves to NodeB).
|
|
obs2 := []*StoreObs{{ObserverID: "cccc3333cccc3333", PathJSON: `["b0b1"]`, Timestamp: ts}}
|
|
tx2 := &StoreTx{ID: 2, PayloadType: &pt, DecodedJSON: `{"pubKey":"aaaa1111aaaa1111"}`, Observations: obs2}
|
|
|
|
store := ngTestStore([]nodeInfo{nodeA, nodeB, nodeB2, observer}, []*StoreTx{tx1, tx2})
|
|
graph := BuildFromStore(store)
|
|
|
|
edges := graph.Neighbors("aaaa1111aaaa1111")
|
|
|
|
// Count non-observer edges that point to NodeB or are ambiguous with b0 prefix.
|
|
resolvedToB := 0
|
|
ambiguousB0 := 0
|
|
for _, e := range edges {
|
|
other := e.NodeA
|
|
if strings.EqualFold(other, "aaaa1111aaaa1111") {
|
|
other = e.NodeB
|
|
}
|
|
if strings.EqualFold(other, "b0b1eeeeb0b1eeee") {
|
|
resolvedToB++
|
|
}
|
|
if e.Ambiguous && e.Prefix == "b0" {
|
|
ambiguousB0++
|
|
}
|
|
}
|
|
|
|
if ambiguousB0 > 0 {
|
|
t.Errorf("expected no ambiguous b0 edges after Phase 1.5, got %d", ambiguousB0)
|
|
}
|
|
if resolvedToB != 1 {
|
|
t.Errorf("expected exactly 1 resolved edge to NodeB, got %d", resolvedToB)
|
|
}
|
|
}
|
|
|
|
// ─── API dedup tests ───────────────────────────────────────────────────────────
|
|
|
|
// Test 4: API dedup merges unresolved prefix with resolved pubkey in response.
|
|
func TestDedupPrefixEntries_MergesUnresolved(t *testing.T) {
|
|
pk := "b0b1eeeeb0b1eeee"
|
|
name := "NodeB"
|
|
entries := []NeighborEntry{
|
|
{
|
|
Pubkey: nil, // unresolved
|
|
Prefix: "b0",
|
|
Count: 207,
|
|
LastSeen: "2026-04-10T12:00:00Z",
|
|
Observers: []string{"obs1"},
|
|
Ambiguous: true,
|
|
},
|
|
{
|
|
Pubkey: &pk,
|
|
Prefix: "b0b1",
|
|
Name: &name,
|
|
Count: 1,
|
|
LastSeen: "2026-04-09T12:00:00Z",
|
|
Observers: []string{"obs2"},
|
|
},
|
|
}
|
|
|
|
result := dedupPrefixEntries(entries)
|
|
|
|
if len(result) != 1 {
|
|
t.Fatalf("expected 1 entry after dedup, got %d", len(result))
|
|
}
|
|
if result[0].Pubkey == nil || *result[0].Pubkey != pk {
|
|
t.Error("expected resolved entry to remain")
|
|
}
|
|
if result[0].Count != 208 { // 1 + 207
|
|
t.Errorf("expected merged count 208, got %d", result[0].Count)
|
|
}
|
|
if result[0].LastSeen != "2026-04-10T12:00:00Z" {
|
|
t.Errorf("expected higher LastSeen, got %s", result[0].LastSeen)
|
|
}
|
|
// Both observers should be present.
|
|
obsMap := make(map[string]bool)
|
|
for _, o := range result[0].Observers {
|
|
obsMap[o] = true
|
|
}
|
|
if !obsMap["obs1"] || !obsMap["obs2"] {
|
|
t.Error("expected both observers after merge")
|
|
}
|
|
}
|
|
|
|
func TestDedupPrefixEntries_NoMatchNoChange(t *testing.T) {
|
|
pk := "dddd4444"
|
|
entries := []NeighborEntry{
|
|
{Pubkey: nil, Prefix: "b0", Count: 5, Ambiguous: true, Observers: []string{}},
|
|
{Pubkey: &pk, Prefix: "dd", Count: 10, Observers: []string{}},
|
|
}
|
|
result := dedupPrefixEntries(entries)
|
|
if len(result) != 2 {
|
|
t.Errorf("expected 2 entries (no match), got %d", len(result))
|
|
}
|
|
}
|
|
|
|
// ─── Benchmark ─────────────────────────────────────────────────────────────────
|
|
|
|
// Test 8: Benchmark Phase 1.5 with 500+ ambiguous edges to verify <100ms.
|
|
func BenchmarkResolveAmbiguousEdges_500(b *testing.B) {
|
|
// Create 600 nodes and 500 ambiguous edges.
|
|
var nodes []nodeInfo
|
|
for i := 0; i < 600; i++ {
|
|
pk := strings.ToLower(strings.Replace(
|
|
strings.Replace(
|
|
strings.Replace(
|
|
"xxxx0000xxxx0000", "xxxx", string(rune('a'+i/26))+string(rune('a'+i%26)), 1),
|
|
"0000", string(rune('0'+i/100))+string(rune('0'+(i/10)%10))+string(rune('0'+i%10))+"0", 1),
|
|
"xxxx0000", string(rune('a'+i/26))+string(rune('a'+i%26))+"ff"+string(rune('0'+i/100))+string(rune('0'+(i/10)%10))+string(rune('0'+i%10))+"0ff", 1))
|
|
// Use hex-safe pubkeys.
|
|
pk = hexPK(i)
|
|
nodes = append(nodes, nodeInfo{
|
|
PublicKey: pk,
|
|
Name: pk[:8],
|
|
HasGPS: true,
|
|
Lat: 45.0 + float64(i)*0.01,
|
|
Lon: -122.0 + float64(i)*0.01,
|
|
})
|
|
}
|
|
pm := buildPrefixMap(nodes)
|
|
|
|
b.ResetTimer()
|
|
for n := 0; n < b.N; n++ {
|
|
graph := NewNeighborGraph()
|
|
// Create 500 ambiguous edges.
|
|
for i := 0; i < 500; i++ {
|
|
knownPK := nodes[0].PublicKey
|
|
prefix := strings.ToLower(nodes[i+1].PublicKey[:2])
|
|
pseudoB := "prefix:" + prefix
|
|
key := makeEdgeKey(strings.ToLower(knownPK), pseudoB)
|
|
graph.edges[key] = &NeighborEdge{
|
|
NodeA: key.A,
|
|
NodeB: "",
|
|
Prefix: prefix,
|
|
Count: 10,
|
|
FirstSeen: time.Now(),
|
|
LastSeen: time.Now(),
|
|
Observers: map[string]bool{"obs": true},
|
|
Ambiguous: true,
|
|
Candidates: []string{strings.ToLower(nodes[i+1].PublicKey)},
|
|
}
|
|
graph.byNode[strings.ToLower(knownPK)] = append(
|
|
graph.byNode[strings.ToLower(knownPK)], graph.edges[key])
|
|
}
|
|
resolveAmbiguousEdges(pm, graph)
|
|
}
|
|
}
|
|
|
|
// hexPK generates a deterministic 16-char hex pubkey for index i.
|
|
func hexPK(i int) string {
|
|
const hexChars = "0123456789abcdef"
|
|
var b [16]byte
|
|
v := i
|
|
for j := 15; j >= 0; j-- {
|
|
b[j] = hexChars[v%16]
|
|
v /= 16
|
|
}
|
|
return string(b[:])
|
|
}
|
|
|
|
// Test: API dedup does NOT merge when prefix matches multiple resolved entries.
|
|
func TestDedupPrefixEntries_MultiMatchNoMerge(t *testing.T) {
|
|
pk1 := "b0b1eeeeb0b1eeee"
|
|
pk2 := "b0c2ffffb0c2ffff"
|
|
name1 := "NodeB1"
|
|
name2 := "NodeB2"
|
|
entries := []NeighborEntry{
|
|
{
|
|
Pubkey: nil, // unresolved
|
|
Prefix: "b0",
|
|
Count: 100,
|
|
LastSeen: "2026-04-10T12:00:00Z",
|
|
Observers: []string{"obs1"},
|
|
Ambiguous: true,
|
|
},
|
|
{
|
|
Pubkey: &pk1,
|
|
Prefix: "b0b1",
|
|
Name: &name1,
|
|
Count: 5,
|
|
LastSeen: "2026-04-09T12:00:00Z",
|
|
Observers: []string{"obs2"},
|
|
},
|
|
{
|
|
Pubkey: &pk2,
|
|
Prefix: "b0c2",
|
|
Name: &name2,
|
|
Count: 3,
|
|
LastSeen: "2026-04-08T12:00:00Z",
|
|
Observers: []string{"obs3"},
|
|
},
|
|
}
|
|
|
|
result := dedupPrefixEntries(entries)
|
|
|
|
if len(result) != 3 {
|
|
t.Fatalf("expected 3 entries (no merge for ambiguous prefix), got %d", len(result))
|
|
}
|
|
// Counts should be unchanged.
|
|
for _, e := range result {
|
|
if e.Pubkey != nil && *e.Pubkey == pk1 && e.Count != 5 {
|
|
t.Errorf("pk1 count should be unchanged at 5, got %d", e.Count)
|
|
}
|
|
if e.Pubkey != nil && *e.Pubkey == pk2 && e.Count != 3 {
|
|
t.Errorf("pk2 count should be unchanged at 3, got %d", e.Count)
|
|
}
|
|
}
|
|
}
|