c97975c1df
Consumes the agent v0.94.0 smart payload (MinAgent floor unchanged; feature-detect by presence). One pure verdict fn agentapi.DiskVerdictFor shared by the dashboard card and the 6h check. Card via a 60s /disks TTL cache (anti-smartctl-storm); unreachable agent -> Nincs adat, page never blocks. disk-health-check (6h) emits disk_health_degraded on a degradation only vs an in-memory baseline (first run silent, recovery/UNKNOWN never notify, multi-attr -> one event). No global banner (deliberate). Pairs with the hub allowlist bump. Tests: verdict table (>=90 red-proof), notifier emit, check first-run-silent (red-proof), degradation-once, recovery-silent, UNKNOWN-excluded, FAILING-critical, nil-smart card, TTL cache.
168 lines
6.9 KiB
Go
168 lines
6.9 KiB
Go
package web
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
|
|
"gitea.dooplex.hu/admin/felhom-controller/internal/agentapi"
|
|
)
|
|
|
|
func smartPtr(v int) *int { return &v }
|
|
|
|
func physDisk(name string, sm *agentapi.SmartSummary) agentapi.DiskInfo {
|
|
return agentapi.DiskInfo{Name: name, BackingDevice: "/dev/" + name, DurableID: "uuid:" + name, Smart: sm}
|
|
}
|
|
|
|
// diskCheckHarness wires a Server with the disks source + notify sink seams and returns a captured
|
|
// list of emitted disk labels.
|
|
func diskCheckHarness(t *testing.T) (*Server, *[]string, *[]agentapi.DiskInfo) {
|
|
t.Helper()
|
|
s := testServer(t)
|
|
var fired []string
|
|
payload := &[]agentapi.DiskInfo{}
|
|
s.diskNotifyFn = func(label string, attrs []string, critical bool) { fired = append(fired, label) }
|
|
s.disksFn = func(ctx context.Context) (agentapi.DisksResponse, error) {
|
|
return agentapi.DisksResponse{Disks: *payload}, nil
|
|
}
|
|
return s, &fired, payload
|
|
}
|
|
|
|
// Scenario B — first run baselines silently; a real degradation (OK→Warn) emits exactly once; a
|
|
// steady-state re-check does not re-emit. Red-proof: remove the `firstRun || !had` guard → the first
|
|
// run emits and the "no notify on first run" assertion fails.
|
|
func TestDiskHealthCheck_DegradationOnce(t *testing.T) {
|
|
s, fired, payload := diskCheckHarness(t)
|
|
ctx := context.Background()
|
|
|
|
// First check: disk PASSED clean (verdict OK). Baseline only.
|
|
*payload = []agentapi.DiskInfo{physDisk("sdb", &agentapi.SmartSummary{Health: agentapi.SmartPassed})}
|
|
_ = s.RunDiskHealthCheck(ctx)
|
|
if len(*fired) != 0 {
|
|
t.Fatalf("first run must not notify, got %v", *fired)
|
|
}
|
|
|
|
// Degrade: pending sectors 0→5 (OK→Figyelmeztetés).
|
|
*payload = []agentapi.DiskInfo{physDisk("sdb", &agentapi.SmartSummary{Health: agentapi.SmartPassed, PendingSectors: smartPtr(5)})}
|
|
_ = s.RunDiskHealthCheck(ctx)
|
|
if len(*fired) != 1 {
|
|
t.Fatalf("degradation must emit exactly once, got %v", *fired)
|
|
}
|
|
|
|
// Steady state: still Figyelmeztetés — no repeat.
|
|
_ = s.RunDiskHealthCheck(ctx)
|
|
if len(*fired) != 1 {
|
|
t.Fatalf("steady-state degraded must not re-emit, got %v", *fired)
|
|
}
|
|
}
|
|
|
|
// Scenario B (cont.) — recovery (Figyelmeztetés→Rendben) notifies nothing.
|
|
func TestDiskHealthCheck_RecoverySilent(t *testing.T) {
|
|
s, fired, payload := diskCheckHarness(t)
|
|
ctx := context.Background()
|
|
|
|
*payload = []agentapi.DiskInfo{physDisk("sdb", &agentapi.SmartSummary{Health: agentapi.SmartPassed})}
|
|
_ = s.RunDiskHealthCheck(ctx) // baseline OK
|
|
*payload = []agentapi.DiskInfo{physDisk("sdb", &agentapi.SmartSummary{Health: agentapi.SmartPassed, PendingSectors: smartPtr(5)})}
|
|
_ = s.RunDiskHealthCheck(ctx) // OK→Warn: emits
|
|
if len(*fired) != 1 {
|
|
t.Fatalf("expected 1 emit on degradation, got %v", *fired)
|
|
}
|
|
// Recover back to clean.
|
|
*payload = []agentapi.DiskInfo{physDisk("sdb", &agentapi.SmartSummary{Health: agentapi.SmartPassed})}
|
|
_ = s.RunDiskHealthCheck(ctx)
|
|
if len(*fired) != 1 {
|
|
t.Errorf("recovery must not notify, got %v", *fired)
|
|
}
|
|
}
|
|
|
|
// Scenario C — UNKNOWN is excluded both directions: a UNKNOWN disk never baselines/emits, and an
|
|
// OK→UNKNOWN→Warn sequence fires on the real OK→Warn (the UNKNOWN blip is ignored, not treated as a
|
|
// transition). Red-proof: the truth of "excluded both directions" — if UNKNOWN were recorded as a
|
|
// verdict, UNKNOWN→Warn would look like a degradation from a low baseline.
|
|
func TestDiskHealthCheck_UnknownExcluded(t *testing.T) {
|
|
s, fired, payload := diskCheckHarness(t)
|
|
ctx := context.Background()
|
|
|
|
// A purely-UNKNOWN disk: first run + repeat, never notifies, never records.
|
|
*payload = []agentapi.DiskInfo{physDisk("sdb", &agentapi.SmartSummary{Health: agentapi.SmartUnknown})}
|
|
_ = s.RunDiskHealthCheck(ctx)
|
|
_ = s.RunDiskHealthCheck(ctx)
|
|
if len(*fired) != 0 {
|
|
t.Fatalf("UNKNOWN disk must never notify, got %v", *fired)
|
|
}
|
|
|
|
// OK baseline, then a UNKNOWN blip, then Warn — must fire once (OK→Warn), the blip ignored.
|
|
*payload = []agentapi.DiskInfo{physDisk("sdc", &agentapi.SmartSummary{Health: agentapi.SmartPassed})}
|
|
_ = s.RunDiskHealthCheck(ctx) // baseline OK
|
|
*payload = []agentapi.DiskInfo{physDisk("sdc", &agentapi.SmartSummary{Health: agentapi.SmartUnknown})}
|
|
_ = s.RunDiskHealthCheck(ctx) // UNKNOWN blip: no change, no emit
|
|
if len(*fired) != 0 {
|
|
t.Fatalf("UNKNOWN blip must not emit, got %v", *fired)
|
|
}
|
|
*payload = []agentapi.DiskInfo{physDisk("sdc", &agentapi.SmartSummary{Health: agentapi.SmartPassed, PendingSectors: smartPtr(3)})}
|
|
_ = s.RunDiskHealthCheck(ctx) // OK→Warn (the blip was ignored): fire once
|
|
if len(*fired) != 1 {
|
|
t.Fatalf("real OK→Warn after a UNKNOWN blip must fire once, got %v", *fired)
|
|
}
|
|
}
|
|
|
|
// Scenario C — the card renders gracefully with nil SMART (old agent / no data): the row shows
|
|
// "Nincs adat" and never errors; a physical disk with no smart field is still listed.
|
|
func TestDiskHealthRows_NilSmart(t *testing.T) {
|
|
s, _, payload := diskCheckHarness(t)
|
|
*payload = []agentapi.DiskInfo{
|
|
{Name: "sdb", BackingDevice: "/dev/sdb", Smart: nil}, // physical, no smart → Nincs adat
|
|
{Name: "felhom-pbs", Type: "pbs"}, // non-physical → excluded
|
|
{Name: "sdc", BackingDevice: "/dev/sdc", Smart: &agentapi.SmartSummary{Health: agentapi.SmartPassed, TemperatureC: smartPtr(31)}}, // Rendben, 31°C
|
|
}
|
|
rows := s.diskHealthRows(context.Background())
|
|
if len(rows) != 2 {
|
|
t.Fatalf("want 2 physical-disk rows (pbs excluded), got %d: %+v", len(rows), rows)
|
|
}
|
|
byLabel := map[string]DiskHealthRow{}
|
|
for _, r := range rows {
|
|
byLabel[r.Label] = r
|
|
}
|
|
if byLabel["sdb"].ChipLabel != "Nincs adat" {
|
|
t.Errorf("nil-smart disk chip = %q, want Nincs adat", byLabel["sdb"].ChipLabel)
|
|
}
|
|
if byLabel["sdc"].ChipLabel != "Rendben" || byLabel["sdc"].Temp != "31" {
|
|
t.Errorf("sdc row = %+v, want Rendben / 31", byLabel["sdc"])
|
|
}
|
|
}
|
|
|
|
// A degraded verdict is FAILING → critical (Scenario B, Hiba→critical path).
|
|
func TestDiskHealthCheck_FailingCritical(t *testing.T) {
|
|
s := testServer(t)
|
|
var crit []bool
|
|
s.diskNotifyFn = func(label string, attrs []string, critical bool) { crit = append(crit, critical) }
|
|
payload := &[]agentapi.DiskInfo{}
|
|
s.disksFn = func(ctx context.Context) (agentapi.DisksResponse, error) {
|
|
return agentapi.DisksResponse{Disks: *payload}, nil
|
|
}
|
|
ctx := context.Background()
|
|
*payload = []agentapi.DiskInfo{physDisk("sdb", &agentapi.SmartSummary{Health: agentapi.SmartPassed})}
|
|
_ = s.RunDiskHealthCheck(ctx) // baseline OK
|
|
*payload = []agentapi.DiskInfo{physDisk("sdb", &agentapi.SmartSummary{Health: agentapi.SmartFailing})}
|
|
_ = s.RunDiskHealthCheck(ctx) // OK→Fail
|
|
if len(crit) != 1 || !crit[0] {
|
|
t.Fatalf("OK→FAILING must emit one critical event, got %v", crit)
|
|
}
|
|
}
|
|
|
|
// TTL cache (Scenario A): two card fetches inside 60s hit the agent once.
|
|
func TestCachedDisks_TTL(t *testing.T) {
|
|
s := testServer(t)
|
|
calls := 0
|
|
s.disksFn = func(ctx context.Context) (agentapi.DisksResponse, error) {
|
|
calls++
|
|
return agentapi.DisksResponse{}, nil
|
|
}
|
|
ctx := context.Background()
|
|
_, _ = s.cachedDisks(ctx)
|
|
_, _ = s.cachedDisks(ctx)
|
|
if calls != 1 {
|
|
t.Errorf("two fetches within the TTL should call the agent once, got %d", calls)
|
|
}
|
|
}
|