Очередь job получила lease и fencing, телеметрия и прогресс deploy сохраняются, ревизия пишет manifest входов. Обновление префиксов не переписывает неизменённый снимок, большие наборы идут через COPY, а устаревшая ревизия не попадает в deploy. Бандл ноды кэшируется, prune и постановка job больше не держат горячий путь. Co-authored-by: Cursor <cursoragent@cursor.com>
103 lines
5.2 KiB
Go
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")
|
|
}
|
|
}
|