175ecfcdd2
gates / gates (push) Successful in 30s
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
68 lines
2.8 KiB
Go
68 lines
2.8 KiB
Go
package api
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func crashBody(boots []string, tripped bool, trippedAt, rearmedAt, bootID string, oops bool) []byte {
|
|
q := func(ss []string) string {
|
|
if len(ss) == 0 {
|
|
return "[]"
|
|
}
|
|
return `["` + strings.Join(ss, `","`) + `"]`
|
|
}
|
|
return []byte(fmt.Sprintf(`{"system":{"pve_version":"pve-manager/9.0.11/x","facts":{"host":{"oops_this_boot":%v,"kernel_panic":10,
|
|
"crash_guard":{"unclean_boots":%s,"unclean_boots_24h":%d,"tripped":%v,"tripped_at":%q,"tripped_reason":"2 unclean boots within 60 minutes",
|
|
"rearmed_at":%q,"rearmed_by":"operator","boot_id":%q}},"guest":{}}}}`, oops, q(boots), len(boots), tripped, trippedAt, rearmedAt, bootID))
|
|
}
|
|
|
|
func countEv(t *testing.T, h *Handler, cust, typ string) int {
|
|
t.Helper()
|
|
ev, err := h.store.GetEventsByType(cust, typ, time.Now().Add(-time.Hour))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return len(ev)
|
|
}
|
|
|
|
// The first report only records history (no flood); each NEW unclean boot is one operator event + one household line;
|
|
// the trip is one alarm; a repeat report is quiet. Red-proof: drop the `known[b]` skip and the repeat report emits again.
|
|
func TestCheckCrash_EventsOncePerFact(t *testing.T) {
|
|
h, st, _ := newTestHandler(t)
|
|
seedHost(t, st, "h1", "c1", "K1")
|
|
old := []string{"2026-10-01T10:00:00Z"}
|
|
h.checkCrash("h1", "c1", crashBody(old, false, "", "", "b0", false))
|
|
if n := countEv(t, h, "c1", EventHostCrashRestart); n != 0 {
|
|
t.Fatalf("first sighting flooded %d events", n)
|
|
}
|
|
two := append(old, "2026-10-04T15:40:00Z")
|
|
h.checkCrash("h1", "c1", crashBody(two, false, "", "", "b1", false))
|
|
h.checkCrash("h1", "c1", crashBody(two, false, "", "", "b1", false)) // the same report again
|
|
if countEv(t, h, "c1", EventHostCrashRestart) != 1 || countEv(t, h, "c1", EventHostRestartedAfterCrash) != 1 {
|
|
t.Fatalf("restart events: op=%d household=%d", countEv(t, h, "c1", EventHostCrashRestart), countEv(t, h, "c1", EventHostRestartedAfterCrash))
|
|
}
|
|
three := append(two, "2026-10-04T15:45:00Z")
|
|
h.checkCrash("h1", "c1", crashBody(three, true, "2026-10-04T15:45:30Z", "", "b2", false))
|
|
h.checkCrash("h1", "c1", crashBody(three, true, "2026-10-04T15:45:30Z", "", "b2", false))
|
|
if countEv(t, h, "c1", EventCrashGuardTripped) != 1 {
|
|
t.Fatalf("tripped events = %d", countEv(t, h, "c1", EventCrashGuardTripped))
|
|
}
|
|
h.checkCrash("h1", "c1", crashBody(three, false, "", "2026-10-04T16:10:00Z", "b2", true))
|
|
if countEv(t, h, "c1", EventCrashGuardRearmed) != 1 || countEv(t, h, "c1", EventKernelOops) != 1 {
|
|
t.Fatal("re-arm / oops not announced")
|
|
}
|
|
}
|
|
|
|
// An agent older than v0.142.0 (no system stanza) never produces a crash event.
|
|
func TestCheckCrash_NoStanzaIsQuiet(t *testing.T) {
|
|
h, st, _ := newTestHandler(t)
|
|
seedHost(t, st, "h1", "c1", "K1")
|
|
h.checkCrash("h1", "c1", []byte(`{"host":{}}`))
|
|
if st.CrashSeen("h1") != "" {
|
|
t.Fatal("a report without the stanza recorded crash state")
|
|
}
|
|
}
|