Files
felhom.eu/hub/internal/osupdates/kernel_approval_test.go
T

109 lines
3.8 KiB
Go

package osupdates
import (
"encoding/json"
"strings"
"testing"
)
// Operator ruling 2026-10-10 (`09` §3, `11` §5.11): "Approve kernel set" offers the NEWEST kernel version that EVERY
// ring-0 box booted healthily at least once after a night step — a box may have moved on since. Before, every box's
// newest step had to be the SAME kernel, and with kernels released on three days in one week (demo-felhom -22 then
// -23, demo-hp -22) the button showed „the ring-0 boxes booted different kernels" and could chase a moving kernel for
// ever. COMPANION RED-PROOF (observed): the pre-ruling KernelStatus fails the -22/-23 cases below.
func kstep(outcome, trigger string, healthy bool, from, to string) Report {
return Report{Layer: LayerKernel, Trigger: trigger, Mode: "apply", Outcome: outcome, Healthy: healthy, ReleaseID: to,
Kernel: json.RawMessage(`{"from":"` + from + `","to":"` + to + `"}`)}
}
const k20, k22, k23 = "7.0.14-20-pve", "7.0.14-22-pve", "7.0.14-23-pve"
func (f *fix) booted(t *testing.T, host, from, to string) {
t.Helper()
f.ingest(t, host, kstep("staged", "night", true, from, to))
f.ingest(t, host, kstep("applied", "boot", true, from, to))
}
func offered(t *testing.T, f *fix) string {
t.Helper()
st, err := f.s.KernelStatus()
if err != nil {
t.Fatal(err)
}
if st.Waiting != "" || st.Fingerprint == "" {
return "WAITING: " + st.Waiting
}
pj := f.s.candidatePackages(st.Fingerprint)
for _, k := range []string{k20, k22, k23} {
if strings.Contains(pj, "proxmox-kernel-"+k+"-signed") {
return k
}
}
return "UNKNOWN: " + pj
}
func TestKernelApproval_OffersNewestEveryBoxBooted(t *testing.T) {
f := newFix(t)
f.booted(t, "n100", k20, k22)
f.booted(t, "n100", k22, k23) // moved on
f.booted(t, "hp", k20, k22)
if got := offered(t, f); got != k22 {
t.Fatalf("n100 booted -22,-23 and hp booted -22: offered %q, want %s", got, k22)
}
}
func TestKernelApproval_BothBootedNewerOffersNewer(t *testing.T) {
f := newFix(t)
f.booted(t, "n100", k20, k22)
f.booted(t, "n100", k22, k23)
f.booted(t, "hp", k20, k22)
f.booted(t, "hp", k22, k23)
if got := offered(t, f); got != k23 {
t.Fatalf("both booted -23: offered %q, want %s", got, k23)
}
}
func TestKernelApproval_ABoxThatNeverBootedOneBlocks(t *testing.T) {
f := newFix(t)
f.booted(t, "n100", k20, k22)
got := offered(t, f)
if !strings.HasPrefix(got, "WAITING: hp ") {
t.Fatalf("hp never booted a new kernel: offered %q, want waiting", got)
}
if _, err := f.s.ApproveKernel(); err == nil {
t.Fatal("approved while one ring-0 box never booted a new kernel")
}
}
// A kernel whose NEWEST step on a box failed is not in that box's set; an older healthy kernel still is.
func TestKernelApproval_AFailedLatestStepDropsThatKernelOnly(t *testing.T) {
f := newFix(t)
f.booted(t, "n100", k20, k22)
f.booted(t, "n100", k22, k23)
f.booted(t, "hp", k20, k22)
f.booted(t, "hp", k22, k23)
f.ingest(t, "hp", kstep("staged", "night", true, k22, k23))
f.ingest(t, "hp", kstep("fell_back", "boot", false, k22, k23))
if got := offered(t, f); got != k22 {
t.Fatalf("-23 fell back on hp (its newest step): offered %q, want %s", got, k22)
}
}
// A healthy boot without a night stage does not count (the night rule is unchanged).
func TestKernelApproval_ByDayBootDoesNotCount(t *testing.T) {
f := newFix(t)
f.booted(t, "n100", k20, k22)
f.ingest(t, "hp", kstep("staged", "signed", true, k20, k22))
f.ingest(t, "hp", kstep("applied", "boot", true, k20, k22))
if got := offered(t, f); !strings.Contains(got, "not after a night step") {
t.Fatalf("offered %q, want waiting on the night step", got)
}
}
func TestKernelKeyOrdersNumerically(t *testing.T) {
if !kernelLess("7.0.14-9-pve", "7.0.14-22-pve") || !kernelLess("7.0.14-22-pve", "7.1.0-1-pve") || kernelLess(k23, k22) {
t.Fatal("kernel versions must order numerically, not as text")
}
}