From 950bd187a9513eb3a17d2a9cb34e85e418b33169 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Wed, 13 May 2026 06:35:54 +0100 Subject: [PATCH] seed DiskFillByType defaults and normalize on load Seeds AlertConfig.DiskFillByType in defaultAlertConfig() with the three forward-compatible keys (nvme 92/87, sata 90/85, hdd 85/80). Adds NormalizeDiskFillByType to lowercase keys on load, seed defaults when the map is nil, and reset non-positive trigger/clear values to the default for that key. NormalizeAgentDefaults invokes the new helper so it runs through the existing UpdateConfig normalization chain. Round trip tests cover nil seed, customized survival, negative reset, and mixed case normalization, plus a defaultAlertConfig seed assertion. Only the "nvme" key is consulted by the host evaluation today (the inference helper only matches /dev/nvme*); "sata" and "hdd" are forward-compatible placeholders for when the agent protocol carries hardware type explicitly. --- internal/alerts/config/normalize.go | 55 +++++++++ internal/alerts/default_config.go | 7 +- internal/alerts/default_normalization_test.go | 114 +++++++++++++++++- 3 files changed, 174 insertions(+), 2 deletions(-) diff --git a/internal/alerts/config/normalize.go b/internal/alerts/config/normalize.go index 6448aee99..50074b0fe 100644 --- a/internal/alerts/config/normalize.go +++ b/internal/alerts/config/normalize.go @@ -282,6 +282,61 @@ func NormalizeAgentDefaults(config *AlertConfig) { } } EnsureValidHysteresis(config.AgentDefaults.DiskTemperature, "agent.diskTemperature") + + NormalizeDiskFillByType(config) +} + +// diskFillByTypeDefaults returns the canonical per-type fill-% defaults. +// Keys are lowercase hardware type strings. +func diskFillByTypeDefaults() map[string]HysteresisThreshold { + return map[string]HysteresisThreshold{ + "nvme": {Trigger: 92, Clear: 87}, + "sata": {Trigger: 90, Clear: 85}, + "hdd": {Trigger: 85, Clear: 80}, + } +} + +// NormalizeDiskFillByType ensures AlertConfig.DiskFillByType is seeded with +// lowercase nvme/sata/hdd defaults when nil, lowercases any existing keys, +// and resets non-positive trigger or clear values to the default for that +// key. Operator-customized positive values are preserved. +func NormalizeDiskFillByType(config *AlertConfig) { + defaults := diskFillByTypeDefaults() + if config.DiskFillByType == nil { + copyMap := make(map[string]HysteresisThreshold, len(defaults)) + for k, v := range defaults { + copyMap[k] = v + } + config.DiskFillByType = copyMap + return + } + + // Lowercase any non-lowercase keys, moving values into the canonical position. + for key, value := range config.DiskFillByType { + lower := strings.ToLower(strings.TrimSpace(key)) + if lower == key { + continue + } + delete(config.DiskFillByType, key) + if lower == "" { + continue + } + if _, exists := config.DiskFillByType[lower]; !exists { + config.DiskFillByType[lower] = value + } + } + + // Ensure all canonical keys are present and have positive trigger/clear values. + for key, defaultVal := range defaults { + current, ok := config.DiskFillByType[key] + if !ok { + config.DiskFillByType[key] = defaultVal + continue + } + if current.Trigger <= 0 || current.Clear <= 0 { + config.DiskFillByType[key] = defaultVal + } + } } func NormalizeGeneralSettings(config *AlertConfig) { diff --git a/internal/alerts/default_config.go b/internal/alerts/default_config.go index 4bb3ea6d4..cbae0e30f 100644 --- a/internal/alerts/default_config.go +++ b/internal/alerts/default_config.go @@ -76,7 +76,12 @@ func defaultAlertConfig() AlertConfig { CPU: &HysteresisThreshold{Trigger: 80, Clear: 75}, Memory: &HysteresisThreshold{Trigger: 85, Clear: 80}, }, - StorageDefault: HysteresisThreshold{Trigger: 85, Clear: 80}, + StorageDefault: HysteresisThreshold{Trigger: 85, Clear: 80}, + DiskFillByType: map[string]HysteresisThreshold{ + "nvme": {Trigger: 92, Clear: 87}, + "sata": {Trigger: 90, Clear: 85}, + "hdd": {Trigger: 85, Clear: 80}, + }, MinimumDelta: 2.0, // 2% minimum change SuppressionWindow: 5, // 5 minutes HysteresisMargin: 5.0, // 5% default margin diff --git a/internal/alerts/default_normalization_test.go b/internal/alerts/default_normalization_test.go index 20d9dea77..2da85e4c0 100644 --- a/internal/alerts/default_normalization_test.go +++ b/internal/alerts/default_normalization_test.go @@ -1,6 +1,10 @@ package alerts -import "testing" +import ( + "testing" + + alertconfig "github.com/rcourtman/pulse-go-rewrite/internal/alerts/config" +) func TestNormalizeSnapshotDefaults(t *testing.T) { tests := []struct { @@ -189,3 +193,111 @@ func TestBackupIgnoreVMID(t *testing.T) { }) } } + +func TestNormalizeDiskFillByType(t *testing.T) { + t.Run("nil map seeds nvme/sata/hdd defaults", func(t *testing.T) { + cfg := &AlertConfig{} + alertconfig.NormalizeDiskFillByType(cfg) + + if cfg.DiskFillByType == nil { + t.Fatal("expected DiskFillByType to be seeded, got nil") + } + nvme, ok := cfg.DiskFillByType["nvme"] + if !ok { + t.Fatal("nvme key not seeded") + } + if nvme.Trigger != 92 || nvme.Clear != 87 { + t.Fatalf("nvme = %+v, want {Trigger:92 Clear:87}", nvme) + } + sata, ok := cfg.DiskFillByType["sata"] + if !ok { + t.Fatal("sata key not seeded") + } + if sata.Trigger != 90 || sata.Clear != 85 { + t.Fatalf("sata = %+v, want {Trigger:90 Clear:85}", sata) + } + hdd, ok := cfg.DiskFillByType["hdd"] + if !ok { + t.Fatal("hdd key not seeded") + } + if hdd.Trigger != 85 || hdd.Clear != 80 { + t.Fatalf("hdd = %+v, want {Trigger:85 Clear:80}", hdd) + } + }) + + t.Run("operator customized values survive", func(t *testing.T) { + cfg := &AlertConfig{ + DiskFillByType: map[string]HysteresisThreshold{ + "nvme": {Trigger: 95, Clear: 90}, + }, + } + alertconfig.NormalizeDiskFillByType(cfg) + + nvme := cfg.DiskFillByType["nvme"] + if nvme.Trigger != 95 || nvme.Clear != 90 { + t.Fatalf("nvme = %+v, want operator value {Trigger:95 Clear:90}", nvme) + } + // Missing keys still seeded. + if sata, ok := cfg.DiskFillByType["sata"]; !ok || sata.Trigger != 90 || sata.Clear != 85 { + t.Fatalf("sata = %+v ok=%v, want default {Trigger:90 Clear:85}", sata, ok) + } + if hdd, ok := cfg.DiskFillByType["hdd"]; !ok || hdd.Trigger != 85 || hdd.Clear != 80 { + t.Fatalf("hdd = %+v ok=%v, want default {Trigger:85 Clear:80}", hdd, ok) + } + }) + + t.Run("negative trigger resets to default", func(t *testing.T) { + cfg := &AlertConfig{ + DiskFillByType: map[string]HysteresisThreshold{ + "nvme": {Trigger: -1, Clear: 10}, + "sata": {Trigger: 88, Clear: -5}, + }, + } + alertconfig.NormalizeDiskFillByType(cfg) + + nvme := cfg.DiskFillByType["nvme"] + if nvme.Trigger != 92 || nvme.Clear != 87 { + t.Fatalf("nvme = %+v, want default reset {Trigger:92 Clear:87}", nvme) + } + sata := cfg.DiskFillByType["sata"] + if sata.Trigger != 90 || sata.Clear != 85 { + t.Fatalf("sata = %+v, want default reset {Trigger:90 Clear:85}", sata) + } + }) + + t.Run("mixed case keys lowercase to canonical", func(t *testing.T) { + cfg := &AlertConfig{ + DiskFillByType: map[string]HysteresisThreshold{ + "NVMe": {Trigger: 93, Clear: 88}, + }, + } + alertconfig.NormalizeDiskFillByType(cfg) + + if _, exists := cfg.DiskFillByType["NVMe"]; exists { + t.Fatalf("expected mixed-case key NVMe to be removed, map=%+v", cfg.DiskFillByType) + } + nvme, ok := cfg.DiskFillByType["nvme"] + if !ok { + t.Fatalf("expected lowercase nvme key, map=%+v", cfg.DiskFillByType) + } + if nvme.Trigger != 93 || nvme.Clear != 88 { + t.Fatalf("nvme = %+v, want preserved operator value {Trigger:93 Clear:88}", nvme) + } + }) +} + +func TestDefaultAlertConfigSeedsDiskFillByType(t *testing.T) { + cfg := defaultAlertConfig() + if cfg.DiskFillByType == nil { + t.Fatal("expected defaultAlertConfig to seed DiskFillByType, got nil") + } + if nvme, ok := cfg.DiskFillByType["nvme"]; !ok || nvme.Trigger != 92 || nvme.Clear != 87 { + t.Fatalf("nvme = %+v ok=%v, want {Trigger:92 Clear:87}", nvme, ok) + } + if sata, ok := cfg.DiskFillByType["sata"]; !ok || sata.Trigger != 90 || sata.Clear != 85 { + t.Fatalf("sata = %+v ok=%v, want {Trigger:90 Clear:85}", sata, ok) + } + if hdd, ok := cfg.DiskFillByType["hdd"]; !ok || hdd.Trigger != 85 || hdd.Clear != 80 { + t.Fatalf("hdd = %+v ok=%v, want {Trigger:85 Clear:80}", hdd, ok) + } +}