hub v0.132.0: the System page (versions + OS updates with the ring/switch/approve buttons, R-852), the Hosts Proxmox/kernel column, the operator-approved Docker engine release (2 healthy ring-0 nights), the crash-guard events (R-851); evidence audits/os-docker-crash-2026-10-04
gates / gates (push) Successful in 30s
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
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user