Files
EvoBGP/internal/httpapi/telemetry_test.go
T
DenozordecandCursor f8e0520817 feat(httpapi): harden control plane and speed up prefix refresh
Очередь job получила lease и fencing, телеметрия и прогресс deploy сохраняются, ревизия пишет manifest входов. Обновление префиксов не переписывает неизменённый снимок, большие наборы идут через COPY, а устаревшая ревизия не попадает в deploy. Бандл ноды кэшируется, prune и постановка job больше не держат горячий путь.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-11-02 17:23:59 +07:00

103 lines
5.2 KiB
Go

package httpapi
import (
"evobgp/internal/birdfmt"
"evobgp/internal/store"
"testing"
"time"
)
func TestTelemetryHydrationFreshnessAndConfiguration(t *testing.T) {
now := time.Now().UTC()
sp := &store.Speaker{ID: "replica", Role: "replica", Endpoint: "old"}
observation := store.TelemetryObservation{ObservedAt: now.Add(-time.Hour), ReceivedAt: now, LastSuccessAt: now, Source: "agent"}
base := store.TelemetrySnapshot{SpeakerID: sp.ID, Kind: "remote_agent", ConfigFingerprint: store.TelemetrySpeakerFingerprint(sp), BGP: store.TelemetryBGP{TelemetryObservation: observation, Sessions: []birdfmt.BGPSession{{Name: "peer", State: "Established"}}}, Health: store.TelemetryHealth{TelemetryObservation: observation, OK: true}}
cases := []struct {
name string
mutate func(*store.TelemetrySnapshot)
bgp, health bool
quality string
}{
{"fresh receipt despite remote clock", func(*store.TelemetrySnapshot) {}, true, true, "ok"},
{"expired BGP independent of health", func(s *store.TelemetrySnapshot) { s.BGP.ReceivedAt = now.Add(-2 * time.Minute) }, false, true, "stale"},
{"expired health independent of BGP", func(s *store.TelemetrySnapshot) { s.Health.ReceivedAt = now.Add(-2 * time.Minute) }, true, false, "ok"},
{"future receipt is not fresh", func(s *store.TelemetrySnapshot) { s.BGP.ReceivedAt = now.Add(time.Hour) }, false, true, "stale"},
{"configuration changed", func(s *store.TelemetrySnapshot) { s.ConfigFingerprint = "old" }, false, false, "unknown"},
{"failed BGP", func(s *store.TelemetrySnapshot) { s.BGP.Error = "timeout" }, false, true, "stale"},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
snapshot := base
tc.mutate(&snapshot)
views, statuses := hydrateTelemetry([]*store.Speaker{sp}, []store.TelemetrySnapshot{snapshot}, now, time.Minute)
status := statuses[sp.ID]
if status["bgp_poll_ok"] != tc.bgp || status["agent_ok"] != tc.health || views[0].observation().Quality != tc.quality {
t.Fatalf("status=%+v observation=%+v", status, views[0].observation())
}
if !tc.bgp && (status["bgp_established"] != nil || len(views[0].Sessions) != 0) {
t.Fatal("stale BGP exposed live sessions")
}
})
}
}
func TestTelemetryHydrationMissingAndAmbiguousLocal(t *testing.T) {
now := time.Now()
sp := &store.Speaker{ID: "master", Role: "master"}
for _, rows := range [][]store.TelemetrySnapshot{nil, {{SpeakerID: sp.ID, Kind: "local_bird", LocalAuthority: "a"}, {SpeakerID: sp.ID, Kind: "local_bird", LocalAuthority: "b"}}} {
views, statuses := hydrateTelemetry([]*store.Speaker{sp}, rows, now, time.Minute)
if views[0].observation().Quality != "unknown" || statuses[sp.ID]["agent_ok"] == true || statuses[sp.ID]["bgp_poll_ok"] == true {
t.Fatalf("unexpected healthy master: %+v", statuses)
}
}
}
func TestTelemetryHydrationLocalAuthorityRestart(t *testing.T) {
now := time.Now()
sp := &store.Speaker{ID: "master", Role: "master"}
fresh := store.TelemetrySnapshot{SpeakerID: sp.ID, Kind: "local_bird", LocalAuthority: "new", ConfigFingerprint: store.TelemetrySpeakerFingerprint(sp), BGP: store.TelemetryBGP{TelemetryObservation: store.TelemetryObservation{ReceivedAt: now}}}
old := fresh
old.LocalAuthority = "old"
old.BGP.ReceivedAt = now.Add(-2 * time.Minute)
_, status := hydrateTelemetry([]*store.Speaker{sp}, []store.TelemetrySnapshot{old, fresh}, now, time.Minute)
if status[sp.ID]["bgp_poll_ok"] != true {
t.Fatal("expired authority hides current local snapshot")
}
old.BGP.ReceivedAt = now
_, status = hydrateTelemetry([]*store.Speaker{sp}, []store.TelemetrySnapshot{old, fresh}, now, time.Minute)
if status[sp.ID]["bgp_poll_ok"] != false {
t.Fatal("two current local authorities are ambiguous")
}
}
func TestTelemetryPeerObservationPreservesUnknownConfiguration(t *testing.T) {
now := time.Now()
sp := &store.Speaker{ID: "replica", Role: "replica"}
row := store.TelemetrySnapshot{SpeakerID: sp.ID, Kind: "remote_agent", ConfigFingerprint: "previous", BGP: store.TelemetryBGP{TelemetryObservation: store.TelemetryObservation{ReceivedAt: now, LastSuccessAt: now}}}
views, _ := hydrateTelemetry([]*store.Speaker{sp}, []store.TelemetrySnapshot{row}, now, time.Minute)
observation := peerLiveObservation(views)
if observation.Quality != "unknown" || observation.ErrorCode != "configuration_changed" {
t.Fatalf("changed config is not a failed current poll: %+v", observation)
}
}
func TestTelemetryExpiryUsesConfiguredIntervalAndEarliestSource(t *testing.T) {
now := time.Now().UTC()
old := now.Add(-30 * time.Second)
a := snapshotObservation(store.TelemetryObservation{ReceivedAt: old}, now, 5*time.Minute, "")
b := snapshotObservation(store.TelemetryObservation{ReceivedAt: now}, now, 5*time.Minute, "")
want := old.Add(5 * time.Minute).Format(time.RFC3339Nano)
if a.ExpiresAt == nil || *a.ExpiresAt != want {
t.Fatalf("expiry ignores configured interval: %+v", a)
}
views := []speakerBGPLive{{Polled: true, ObservationOverride: &b}, {Polled: true, ObservationOverride: &a}}
combined := peerLiveObservation(views)
if combined.ExpiresAt == nil || *combined.ExpiresAt != want {
t.Fatalf("aggregate extends oldest source: %+v", combined)
}
missing := snapshotObservation(store.TelemetryObservation{}, now, time.Minute, "")
if missing.ExpiresAt != nil {
t.Fatal("unobserved source has expiry")
}
}