mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-06-10 14:21:42 +00:00
test(#1290): red — assert listener-only observers excluded from path-hop candidates
Adds TestResolveWithContext_ExcludesNonRelayObservers_Issue1290 covering: - repeat:off pubkey → not a candidate - repeat:on pubkey → still a candidate (regression guard) - legacy obs / no field → still a candidate (back-compat) Stub prefixMap.markNonRelay added so the test compiles and runs to its assertion. Filtering inside resolveWithContext is not yet wired, so case-1 fails on assertion (case repeat:off — expected nil, got "ListenerOnly"). Green commit will plumb the filter through pm.m construction.
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Issue #1290 — exclude observers that advertised `repeat:off` (listener-only)
|
||||
// from the path-hop disambiguator candidate set. Three cases:
|
||||
// 1. repeat:off pubkey → NOT a candidate
|
||||
// 2. repeat:on pubkey → IS a candidate (regression guard)
|
||||
// 3. legacy / no field → IS a candidate (back-compat preserve current behavior)
|
||||
func TestResolveWithContext_ExcludesNonRelayObservers_Issue1290(t *testing.T) {
|
||||
nodes := []nodeInfo{
|
||||
{Role: "repeater", PublicKey: "a1aaaaaa", Name: "RealRepeater"},
|
||||
{Role: "repeater", PublicKey: "a1bbbbbb", Name: "ListenerOnly"},
|
||||
}
|
||||
|
||||
// Case 1: marked non-relay → excluded from candidate set.
|
||||
pm := buildPrefixMap(nodes)
|
||||
pm.markNonRelay([]string{"a1bbbbbb"})
|
||||
ni, conf, _ := pm.resolveWithContext("a1bbbbbb", nil, nil)
|
||||
if ni != nil {
|
||||
t.Fatalf("case repeat:off — expected nil (listener-only excluded), got name=%q confidence=%q", ni.Name, conf)
|
||||
}
|
||||
if conf != "no_match" {
|
||||
t.Fatalf("case repeat:off — expected no_match confidence after exclusion, got %q", conf)
|
||||
}
|
||||
|
||||
// Case 2: repeat:on (i.e. not in nonRelay set) → still resolves.
|
||||
pm2 := buildPrefixMap(nodes)
|
||||
pm2.markNonRelay([]string{"a1bbbbbb"})
|
||||
ni2, _, _ := pm2.resolveWithContext("a1aaaaaa", nil, nil)
|
||||
if ni2 == nil || ni2.Name != "RealRepeater" {
|
||||
t.Fatalf("case repeat:on — expected RealRepeater, got %+v", ni2)
|
||||
}
|
||||
|
||||
// Case 3: legacy back-compat — no markNonRelay call → behavior unchanged.
|
||||
pm3 := buildPrefixMap(nodes)
|
||||
ni3, _, _ := pm3.resolveWithContext("a1bbbbbb", nil, nil)
|
||||
if ni3 == nil || ni3.Name != "ListenerOnly" {
|
||||
t.Fatalf("case legacy — expected ListenerOnly (back-compat), got %+v", ni3)
|
||||
}
|
||||
}
|
||||
@@ -6121,6 +6121,28 @@ func (s *PacketStore) getAllNodes() []nodeInfo {
|
||||
|
||||
type prefixMap struct {
|
||||
m map[string][]nodeInfo
|
||||
// nonRelay holds lowercase pubkeys of observer-known nodes that have
|
||||
// advertised `repeat:off` in their MQTT /status message (issue #1290).
|
||||
// Such nodes are pure listeners and must never be selected as a
|
||||
// path-hop candidate by resolveWithContext, since by firmware
|
||||
// contract they do not forward packets. nil/empty preserves the
|
||||
// pre-#1290 behavior (every prefix-matching node is a candidate).
|
||||
nonRelay map[string]struct{}
|
||||
}
|
||||
|
||||
// markNonRelay registers a set of lowercase pubkeys as listener-only.
|
||||
// Called by the server when wiring the prefix map after reading the
|
||||
// observers table's can_relay column. Issue #1290.
|
||||
func (pm *prefixMap) markNonRelay(pubkeys []string) {
|
||||
if pm == nil {
|
||||
return
|
||||
}
|
||||
if pm.nonRelay == nil {
|
||||
pm.nonRelay = make(map[string]struct{}, len(pubkeys))
|
||||
}
|
||||
for _, pk := range pubkeys {
|
||||
pm.nonRelay[strings.ToLower(pk)] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
// maxPrefixLen caps prefix map entries. MeshCore path hops use 2–6 char
|
||||
|
||||
Reference in New Issue
Block a user