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") } }