mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-09-26 16:07:55 +00:00
## Problem Node detail's bimodal-clock warning showed only `⚠️ N of last M adverts had nonsense timestamps (likely RTC reset)` — no way to tell which packets, no way to verify the heuristic, no way to drill in. ## Fix Additive, two-sides: **Backend** (`cmd/server/clock_skew.go`) - New type `BadSample { Hash, AdvertTS, SkewSec }`. - New field `NodeClockSkew.RecentBadSamples []BadSample` (`omitempty`). - Populated from the **same** bimodal-bad classification pass that produces `RecentBadSampleCount` — no heuristic change. `tsSkewPair` carries `hash` + `advertTS` so the classifier can record per-sample evidence without a second walk; drift code is unaffected (reads only `ts`/`skew`). **Frontend** (`public/nodes.js`) - `bimodalWarning` preserves the existing count summary line, then renders a `<ul>` of bad samples: each `<li>` is `<a href="#/packets/HASH">hash[:8]</a> → formatTimestamp(advertTS)` with ISO tooltip. Defensive `Array.isArray` so older API responses still render the summary alone. ## TDD - **Red:** `cmd/server/clock_skew_issue1094_test.go::TestIssue1094_RecentBadSamples_ExposesHashAndTimestamp` — seeds 3 healthy + 2 bimodal-bad adverts, asserts `RecentBadSamples` has length 2 with the expected hashes and advert timestamps. Fails on the assertion (`len = 0, want 2`) with the stub-only commit. - **Green:** classifier populates the slice; existing #1285 and bimodal tests stay green. - Red commit: `ed501f4b` - Green commit: `54305b06` ## Cross-stack Backend + frontend ship together (`cross-stack: justified` commit). API stays backward compatible (`omitempty` server, `Array.isArray` client) but the feature only lights up with both halves present. ## Preflight Clean — PII, branch scope, red-commit, CSS vars, XSS sinks, migrations, fixture coverage all pass. ## Acceptance - [x] Warning lists specific packet hashes - [x] Each hash links to `#/packets/<hash>` - [x] Bad advert timestamp shown next to the hash - [x] Pattern is reusable — `BadSample` is a clean shape any future heuristic that flags specific packets can adopt Fixes #1094 --------- Co-authored-by: openclaw-bot <bot@openclaw.local>
110 lines
3.3 KiB
Go
110 lines
3.3 KiB
Go
package main
|
|
|
|
// Regression test for #1094: the bimodal-clock warning currently exposes only
|
|
// RecentBadSampleCount, leaving the UI to render "⚠️ N of M adverts had
|
|
// nonsense timestamps" without telling the operator WHICH packets were bad.
|
|
//
|
|
// This test pins the additive API contract: alongside the count, the response
|
|
// must expose RecentBadSamples — a slice of (hash, advertTS, skewSec) — so the
|
|
// frontend can render each offending hash as a clickable link with its bad
|
|
// timestamp.
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// Seeds 5 recent adverts: 3 healthy (~-20s skew) and 2 with a "nonsense"
|
|
// bimodal-bad timestamp (|skew| in (1h, 24h]). The recent window is exactly
|
|
// 5 samples, so all five are inside it.
|
|
func seedIssue1094Repro(t *testing.T) (*PacketStore, []string, []int64) {
|
|
t.Helper()
|
|
ps := NewPacketStore(nil, nil)
|
|
pt := 4 // ADVERT
|
|
|
|
const pubkey = "BADTS1094"
|
|
baseObs := int64(1779000000)
|
|
|
|
var txs []*StoreTx
|
|
var badHashes []string
|
|
var badAdvertTSs []int64
|
|
|
|
// 3 healthy adverts (skew = -20s).
|
|
for i := 0; i < 3; i++ {
|
|
obsTS := baseObs + int64(i)*60
|
|
advTS := obsTS - 20
|
|
txs = append(txs, &StoreTx{
|
|
Hash: "healthy-1094-" + formatInt64(int64(i)),
|
|
PayloadType: &pt,
|
|
DecodedJSON: `{"payload":{"timestamp":` + formatInt64(advTS) + `}}`,
|
|
Observations: []*StoreObs{
|
|
{ObserverID: "obs1", Timestamp: time.Unix(obsTS, 0).UTC().Format(time.RFC3339)},
|
|
},
|
|
})
|
|
}
|
|
|
|
// 2 nonsense-timestamp adverts (skew = -7200s = -2h — bimodal-bad,
|
|
// below the 24h RTC-reset exclusion so they DO count in recentBadCount).
|
|
for i := 0; i < 2; i++ {
|
|
obsTS := baseObs + int64(3+i)*60
|
|
advTS := obsTS - 7200
|
|
hash := "bad-1094-" + formatInt64(int64(i))
|
|
txs = append(txs, &StoreTx{
|
|
Hash: hash,
|
|
PayloadType: &pt,
|
|
DecodedJSON: `{"payload":{"timestamp":` + formatInt64(advTS) + `}}`,
|
|
Observations: []*StoreObs{
|
|
{ObserverID: "obs1", Timestamp: time.Unix(obsTS, 0).UTC().Format(time.RFC3339)},
|
|
},
|
|
})
|
|
badHashes = append(badHashes, hash)
|
|
badAdvertTSs = append(badAdvertTSs, advTS)
|
|
}
|
|
|
|
ps.mu.Lock()
|
|
ps.byNode[pubkey] = txs
|
|
for _, tx := range txs {
|
|
ps.byPayloadType[4] = append(ps.byPayloadType[4], tx)
|
|
}
|
|
ps.clockSkew.computeInterval = 0
|
|
ps.mu.Unlock()
|
|
return ps, badHashes, badAdvertTSs
|
|
}
|
|
|
|
func TestIssue1094_RecentBadSamples_ExposesHashAndTimestamp(t *testing.T) {
|
|
ps, wantHashes, wantAdvertTSs := seedIssue1094Repro(t)
|
|
r := ps.GetNodeClockSkew("BADTS1094")
|
|
if r == nil {
|
|
t.Fatal("expected clock skew result")
|
|
}
|
|
|
|
// Pre-condition: count must already be 2 (gates the test against the
|
|
// existing field — if this drops we'd be measuring the wrong thing).
|
|
if r.RecentBadSampleCount != 2 {
|
|
t.Fatalf("RecentBadSampleCount = %d, want 2 (seed bug, not the field-under-test)",
|
|
r.RecentBadSampleCount)
|
|
}
|
|
|
|
if len(r.RecentBadSamples) != 2 {
|
|
t.Fatalf("RecentBadSamples len = %d, want 2 — operators need to see which "+
|
|
"adverts had nonsense timestamps, not just the count",
|
|
len(r.RecentBadSamples))
|
|
}
|
|
|
|
gotByHash := map[string]int64{}
|
|
for _, bs := range r.RecentBadSamples {
|
|
gotByHash[bs.Hash] = bs.AdvertTS
|
|
}
|
|
for i, h := range wantHashes {
|
|
ts, ok := gotByHash[h]
|
|
if !ok {
|
|
t.Errorf("RecentBadSamples missing hash %q", h)
|
|
continue
|
|
}
|
|
if ts != wantAdvertTSs[i] {
|
|
t.Errorf("RecentBadSamples[%q].AdvertTS = %d, want %d (the bad advertTS)",
|
|
h, ts, wantAdvertTSs[i])
|
|
}
|
|
}
|
|
}
|