Очередь job получила lease и fencing, телеметрия и прогресс deploy сохраняются, ревизия пишет manifest входов. Обновление префиксов не переписывает неизменённый снимок, большие наборы идут через COPY, а устаревшая ревизия не попадает в deploy. Бандл ноды кэшируется, prune и постановка job больше не держат горячий путь. Co-authored-by: Cursor <[email protected]>
153 lines
6.6 KiB
Go
153 lines
6.6 KiB
Go
package httpapi
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
|
|
"evobgp/internal/birdfmt"
|
|
"evobgp/internal/store"
|
|
)
|
|
|
|
func TestApplyPeerLiveFieldsObservationStates(t *testing.T) {
|
|
peer := &store.BGPPeer{ID: "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", Neighbor: "198.51.100.2"}
|
|
now := time.Date(2026, 10, 26, 10, 0, 0, 0, time.UTC)
|
|
cases := []struct {
|
|
name string
|
|
views []speakerBGPLive
|
|
wantState any
|
|
wantQuality string
|
|
}{
|
|
{"no collector", nil, nil, "unknown"},
|
|
{"successful empty poll", []speakerBGPLive{{Polled: true, Source: "agent_bird_protocols", ObservedAt: now, ReceivedAt: now, LastSuccessAt: now}}, "absent", "ok"},
|
|
{"first failure", []speakerBGPLive{{Polled: true, Error: "timeout", Source: "agent_bird_protocols", ObservedAt: now}}, nil, "unreachable"},
|
|
{"failure after success", []speakerBGPLive{{Polled: true, Error: "timeout", Source: "agent_bird_protocols", ObservedAt: now, LastSuccessAt: now.Add(-time.Minute)}}, nil, "stale"},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
row := map[string]any{"session_state": "Established"}
|
|
applyPeerLiveFields(row, peer, tc.views)
|
|
if row["session_state"] != tc.wantState || row["last_known_session_state"] != "Established" {
|
|
t.Fatalf("session_state=%v last_known=%v", row["session_state"], row["last_known_session_state"])
|
|
}
|
|
observation := row["live_observation"].(liveObservation)
|
|
if observation.Quality != tc.wantQuality {
|
|
t.Fatalf("observation=%+v", observation)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestApplyPeerLiveFieldsExpectedSpeakerCoverage(t *testing.T) {
|
|
now := time.Date(2026, 10, 26, 10, 0, 0, 0, time.UTC)
|
|
assigned := "speaker-a"
|
|
peer := &store.BGPPeer{ID: "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", Neighbor: "198.51.100.2", SpeakerID: &assigned}
|
|
viewAError := speakerBGPLive{SpeakerID: assigned, Polled: true, Error: "timeout", ObservedAt: now, Source: "agent_bird_protocols"}
|
|
viewAEmpty := speakerBGPLive{SpeakerID: assigned, Polled: true, ObservedAt: now, LastSuccessAt: now, Source: "agent_bird_protocols"}
|
|
viewBEmpty := speakerBGPLive{SpeakerID: "speaker-b", Polled: true, ObservedAt: now.Add(time.Minute), LastSuccessAt: now.Add(time.Minute), Source: "agent_bird_protocols"}
|
|
viewBError := speakerBGPLive{SpeakerID: "speaker-b", Polled: true, Error: "timeout", ObservedAt: now.Add(time.Minute), Source: "agent_bird_protocols"}
|
|
cases := []struct {
|
|
name string
|
|
assigned bool
|
|
views []speakerBGPLive
|
|
wantState any
|
|
wantQuality string
|
|
wantObserved string
|
|
}{
|
|
{"assigned timeout unrelated success", true, []speakerBGPLive{viewAError, viewBEmpty}, nil, "unreachable", now.Format(time.RFC3339Nano)},
|
|
{"assigned success unrelated failure", true, []speakerBGPLive{viewAEmpty, viewBError}, "absent", "ok", now.Format(time.RFC3339Nano)},
|
|
{"global mixed outcome", false, []speakerBGPLive{viewAEmpty, viewBError}, nil, "unknown", now.Add(time.Minute).Format(time.RFC3339Nano)},
|
|
{"assigned speaker not observed", true, []speakerBGPLive{viewBEmpty}, nil, "unknown", ""},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
candidate := *peer
|
|
if !tc.assigned {
|
|
candidate.SpeakerID = nil
|
|
}
|
|
row := map[string]any{"session_state": "Established"}
|
|
applyPeerLiveFields(row, &candidate, tc.views)
|
|
obs := row["live_observation"].(liveObservation)
|
|
if row["session_state"] != tc.wantState || obs.Quality != tc.wantQuality {
|
|
t.Fatalf("state=%v observation=%+v", row["session_state"], obs)
|
|
}
|
|
if tc.wantObserved == "" && obs.ObservedAt != nil {
|
|
t.Fatalf("unexpected timestamp %v", *obs.ObservedAt)
|
|
}
|
|
if tc.wantObserved != "" && (obs.ObservedAt == nil || *obs.ObservedAt != tc.wantObserved) {
|
|
t.Fatalf("observed_at=%v want=%q", obs.ObservedAt, tc.wantObserved)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestMatchPeerOnSpeakers_establishedOnReplica(t *testing.T) {
|
|
peer := &store.BGPPeer{
|
|
ID: "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
|
|
Neighbor: "198.51.100.2",
|
|
}
|
|
views := []speakerBGPLive{
|
|
{
|
|
SpeakerID: "master-id",
|
|
Label: "CP · bgp.shz.su",
|
|
Sessions: []birdfmt.BGPSession{
|
|
{Name: birdfmt.PeerProtocolName(peer.ID), Neighbor: "198.51.100.2", State: "Established"},
|
|
},
|
|
},
|
|
{
|
|
SpeakerID: "replica-id",
|
|
Label: "bgp2.shz.su",
|
|
Sessions: []birdfmt.BGPSession{
|
|
{Name: birdfmt.PeerProtocolName(peer.ID), Neighbor: "198.51.100.2", State: "Established"},
|
|
},
|
|
},
|
|
}
|
|
state, connID, connLabel, established, on, mismatch := matchPeerOnSpeakers(peer, views)
|
|
if state != "Established" || connID != "" || connLabel != "CP · bgp.shz.su, bgp2.shz.su" {
|
|
t.Fatalf("got state=%q conn=%q label=%q", state, connID, connLabel)
|
|
}
|
|
if mismatch || len(on) != 2 || len(established) != 2 {
|
|
t.Fatalf("on=%+v established=%+v mismatch=%v", on, established, mismatch)
|
|
}
|
|
}
|
|
|
|
func TestMatchPeerOnSpeakers_multipleEstablished(t *testing.T) {
|
|
peer := &store.BGPPeer{ID: "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", Neighbor: "198.51.100.2/32"}
|
|
views := []speakerBGPLive{
|
|
{SpeakerID: "a", Label: "n1", Sessions: []birdfmt.BGPSession{{Name: birdfmt.PeerProtocolName(peer.ID), State: "Established"}}},
|
|
{SpeakerID: "b", Label: "n2", Sessions: []birdfmt.BGPSession{{Name: birdfmt.PeerProtocolName(peer.ID), State: "Established"}}},
|
|
}
|
|
_, connID, label, established, on, mismatch := matchPeerOnSpeakers(peer, views)
|
|
if mismatch || connID != "" || label != "n1, n2" || len(established) != 2 || len(on) != 2 {
|
|
t.Fatalf("connID=%q label=%q established=%+v on=%+v mismatch=%v", connID, label, established, on, mismatch)
|
|
}
|
|
}
|
|
|
|
func TestMatchPeerOnSpeakers_mismatchConfiguredSpeaker(t *testing.T) {
|
|
replica := "replica-id"
|
|
peer := &store.BGPPeer{
|
|
ID: "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
|
|
Neighbor: "198.51.100.2",
|
|
SpeakerID: &replica,
|
|
}
|
|
views := []speakerBGPLive{
|
|
{SpeakerID: "master-id", Label: "CP", Sessions: []birdfmt.BGPSession{{Name: birdfmt.PeerProtocolName(peer.ID), State: "Established"}}},
|
|
{SpeakerID: replica, Label: "bgp2", Sessions: []birdfmt.BGPSession{{Name: birdfmt.PeerProtocolName(peer.ID), State: "Idle"}}},
|
|
}
|
|
_, _, _, _, _, mismatch := matchPeerOnSpeakers(peer, views)
|
|
if !mismatch {
|
|
t.Fatal("expected mismatch when configured replica has no Established")
|
|
}
|
|
}
|
|
|
|
func TestMatchPeerOnSpeakers_pollError(t *testing.T) {
|
|
peer := &store.BGPPeer{ID: "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", Neighbor: "198.51.100.2"}
|
|
views := []speakerBGPLive{
|
|
{Label: "CP (local BIRD)", Sessions: []birdfmt.BGPSession{{Name: birdfmt.PeerProtocolName(peer.ID), State: "Established"}}},
|
|
{SpeakerID: "replica-id", Label: "bgp2.shz.su", Error: "HTTP 404: Not Found"},
|
|
}
|
|
_, _, _, established, on, _ := matchPeerOnSpeakers(peer, views)
|
|
if len(established) != 1 || len(on) != 2 || on[1].PollError == "" {
|
|
t.Fatalf("on=%+v established=%+v", on, established)
|
|
}
|
|
}
|