mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-04-25 13:22:13 +00:00
## Problem Nodes that only appear as relay hops in packet paths (via `resolved_path`) were never indexed in `byNode`, so `last_heard` was never computed for them. This made relay-only nodes show as dead/stale even when actively forwarding traffic. Fixes #660 ## Root Cause `indexByNode()` only indexed pubkeys from decoded JSON fields (`pubKey`, `destPubKey`, `srcPubKey`). Relay nodes appearing in `resolved_path` were ignored entirely. ## Fix `indexByNode()` now also iterates: 1. `ResolvedPath` entries from each observation 2. `tx.ResolvedPath` (best observation's resolved path, used for DB-loaded packets) A per-call `indexed` set prevents double-indexing when the same pubkey appears in both decoded JSON and resolved path. Extracted `addToByNode()` helper to deduplicate the nodeHashes/byNode append logic. ## Scope **Phase 1 only** — server-side in-memory indexing. No DB changes, no ingestor changes. This makes `last_heard` reflect relay activity with zero risk to persistence. ## Tests 5 new test cases in `TestIndexByNodeResolvedPath`: - Resolved path pubkeys from observations get indexed - Null entries in resolved path are skipped - Relay-only nodes (no decoded JSON match) appear in `byNode` - Dedup between decoded JSON and resolved path - `tx.ResolvedPath` indexed when observations are empty All existing tests pass unchanged. ## Complexity O(observations × path_length) per packet — typically 1-3 observations × 1-3 hops. No hot-path regression. --------- Co-authored-by: you <you@example.com>
138 lines
3.6 KiB
Go
138 lines
3.6 KiB
Go
package main
|
|
|
|
import (
|
|
"database/sql"
|
|
"testing"
|
|
"time"
|
|
|
|
_ "modernc.org/sqlite"
|
|
)
|
|
|
|
func TestTouchNodeLastSeen_UpdatesDB(t *testing.T) {
|
|
db := setupTestDB(t)
|
|
defer db.Close()
|
|
|
|
// Insert a node with no last_seen
|
|
db.conn.Exec("INSERT INTO nodes (public_key, name, role) VALUES (?, ?, ?)", "abc123", "relay1", "REPEATER")
|
|
|
|
err := db.TouchNodeLastSeen("abc123", "2026-04-12T04:00:00Z")
|
|
if err != nil {
|
|
t.Fatalf("TouchNodeLastSeen returned error: %v", err)
|
|
}
|
|
|
|
var lastSeen sql.NullString
|
|
db.conn.QueryRow("SELECT last_seen FROM nodes WHERE public_key = ?", "abc123").Scan(&lastSeen)
|
|
if !lastSeen.Valid || lastSeen.String != "2026-04-12T04:00:00Z" {
|
|
t.Fatalf("expected last_seen=2026-04-12T04:00:00Z, got %v", lastSeen)
|
|
}
|
|
}
|
|
|
|
func TestTouchNodeLastSeen_DoesNotGoBackwards(t *testing.T) {
|
|
db := setupTestDB(t)
|
|
defer db.Close()
|
|
|
|
db.conn.Exec("INSERT INTO nodes (public_key, name, role, last_seen) VALUES (?, ?, ?, ?)",
|
|
"abc123", "relay1", "REPEATER", "2026-04-12T05:00:00Z")
|
|
|
|
// Try to set an older timestamp
|
|
err := db.TouchNodeLastSeen("abc123", "2026-04-12T04:00:00Z")
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
var lastSeen string
|
|
db.conn.QueryRow("SELECT last_seen FROM nodes WHERE public_key = ?", "abc123").Scan(&lastSeen)
|
|
if lastSeen != "2026-04-12T05:00:00Z" {
|
|
t.Fatalf("last_seen went backwards: got %s", lastSeen)
|
|
}
|
|
}
|
|
|
|
func TestTouchNodeLastSeen_NonExistentNode(t *testing.T) {
|
|
db := setupTestDB(t)
|
|
defer db.Close()
|
|
|
|
// Should not error for non-existent node
|
|
err := db.TouchNodeLastSeen("nonexistent", "2026-04-12T04:00:00Z")
|
|
if err != nil {
|
|
t.Fatalf("unexpected error for non-existent node: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestTouchRelayLastSeen_Debouncing(t *testing.T) {
|
|
db := setupTestDB(t)
|
|
defer db.Close()
|
|
|
|
db.conn.Exec("INSERT INTO nodes (public_key, name, role) VALUES (?, ?, ?)", "relay1", "R1", "REPEATER")
|
|
|
|
s := &PacketStore{
|
|
db: db,
|
|
lastSeenTouched: make(map[string]time.Time),
|
|
}
|
|
|
|
pk := "relay1"
|
|
tx := &StoreTx{
|
|
ResolvedPath: []*string{&pk},
|
|
}
|
|
|
|
now := time.Now()
|
|
s.touchRelayLastSeen(tx, now)
|
|
|
|
// Verify it was written
|
|
var lastSeen sql.NullString
|
|
db.conn.QueryRow("SELECT last_seen FROM nodes WHERE public_key = ?", "relay1").Scan(&lastSeen)
|
|
if !lastSeen.Valid {
|
|
t.Fatal("expected last_seen to be set after first touch")
|
|
}
|
|
|
|
// Reset last_seen to check debounce prevents second write
|
|
db.conn.Exec("UPDATE nodes SET last_seen = NULL WHERE public_key = ?", "relay1")
|
|
|
|
// Call again within 5 minutes — should be debounced (no write)
|
|
s.touchRelayLastSeen(tx, now.Add(2*time.Minute))
|
|
|
|
db.conn.QueryRow("SELECT last_seen FROM nodes WHERE public_key = ?", "relay1").Scan(&lastSeen)
|
|
if lastSeen.Valid {
|
|
t.Fatal("expected debounce to prevent second write within 5 minutes")
|
|
}
|
|
|
|
// Call after 5 minutes — should write again
|
|
s.touchRelayLastSeen(tx, now.Add(6*time.Minute))
|
|
db.conn.QueryRow("SELECT last_seen FROM nodes WHERE public_key = ?", "relay1").Scan(&lastSeen)
|
|
if !lastSeen.Valid {
|
|
t.Fatal("expected write after debounce interval expired")
|
|
}
|
|
}
|
|
|
|
func TestTouchRelayLastSeen_SkipsNilResolvedPath(t *testing.T) {
|
|
db := setupTestDB(t)
|
|
defer db.Close()
|
|
|
|
s := &PacketStore{
|
|
db: db,
|
|
lastSeenTouched: make(map[string]time.Time),
|
|
}
|
|
|
|
// tx with nil entries and empty resolved_path
|
|
tx := &StoreTx{
|
|
ResolvedPath: []*string{nil, nil},
|
|
}
|
|
|
|
// Should not panic or error
|
|
s.touchRelayLastSeen(tx, time.Now())
|
|
}
|
|
|
|
func TestTouchRelayLastSeen_NilDB(t *testing.T) {
|
|
s := &PacketStore{
|
|
db: nil,
|
|
lastSeenTouched: make(map[string]time.Time),
|
|
}
|
|
|
|
pk := "abc"
|
|
tx := &StoreTx{
|
|
ResolvedPath: []*string{&pk},
|
|
}
|
|
|
|
// Should not panic with nil db
|
|
s.touchRelayLastSeen(tx, time.Now())
|
|
}
|