package config import ( "os" "path/filepath" "strings" "testing" ) func TestLoadAcceptsUPSTargets(t *testing.T) { tmp := t.TempDir() cfgPath := filepath.Join(tmp, "ananke.yaml") raw := ` control_planes: [titan-0a, titan-0b, titan-0c] expected_flux_branch: main iac_repo_path: /opt/titan-iac ups: enabled: true provider: nut targets: - name: pyrphoros target: pyrphoros@localhost shutdown: default_budget_seconds: 300 state: run_history_path: /tmp/runs.json lock_path: /tmp/ananke.lock ` if err := os.WriteFile(cfgPath, []byte(strings.TrimSpace(raw)), 0o644); err != nil { t.Fatalf("write config: %v", err) } cfg, err := Load(cfgPath) if err != nil { t.Fatalf("load config: %v", err) } if len(cfg.UPS.Targets) != 1 || cfg.UPS.Targets[0].Target != "pyrphoros@localhost" { t.Fatalf("unexpected UPS targets: %#v", cfg.UPS.Targets) } } func TestValidateForwardShutdownRequiresConfigPath(t *testing.T) { cfg := defaults() cfg.Coordination.ForwardShutdownHost = "titan-db" cfg.Coordination.ForwardShutdownConfig = "" if err := cfg.Validate(); err == nil { t.Fatalf("expected validation error for missing forward_shutdown_config") } } func TestValidateRejectsUnknownRole(t *testing.T) { cfg := defaults() cfg.Coordination.Role = "unknown" if err := cfg.Validate(); err == nil { t.Fatalf("expected validation error for unknown coordination role") } } func TestValidateRejectsEmptyPeerHostEntry(t *testing.T) { cfg := defaults() cfg.Coordination.PeerHosts = []string{"titan-24", " "} if err := cfg.Validate(); err == nil { t.Fatalf("expected validation error for empty peer_hosts entry") } } func TestValidateRejectsUnknownEtcdRestoreControlPlane(t *testing.T) { cfg := defaults() cfg.Startup.EtcdRestoreControlPlane = "titan-missing" if err := cfg.Validate(); err == nil { t.Fatalf("expected validation error for unknown etcd restore control plane") } } func TestLoadSetsCoordinationGuardDefaults(t *testing.T) { tmp := t.TempDir() cfgPath := filepath.Join(tmp, "ananke.yaml") raw := ` control_planes: [titan-0a, titan-0b, titan-0c] expected_flux_branch: main iac_repo_path: /opt/titan-iac coordination: role: coordinator ups: enabled: false state: run_history_path: /tmp/runs.json lock_path: /tmp/ananke.lock ` if err := os.WriteFile(cfgPath, []byte(strings.TrimSpace(raw)), 0o644); err != nil { t.Fatalf("write config: %v", err) } cfg, err := Load(cfgPath) if err != nil { t.Fatalf("load config: %v", err) } if cfg.Coordination.StartupGuardMaxAgeSec <= 0 { t.Fatalf("expected startup guard max age default > 0, got %d", cfg.Coordination.StartupGuardMaxAgeSec) } if cfg.Startup.EtcdRestoreControlPlane == "" { t.Fatalf("expected startup etcd restore control plane default to be set") } if cfg.Startup.TimeSyncMode == "" { t.Fatalf("expected startup time sync mode default to be set") } if cfg.Startup.VaultUnsealKeyFile == "" { t.Fatalf("expected startup vault unseal key file default to be set") } if cfg.Startup.ShutdownCooldownSeconds <= 0 { t.Fatalf("expected startup shutdown cooldown default > 0, got %d", cfg.Startup.ShutdownCooldownSeconds) } if cfg.Startup.VaultUnsealBreakglassTimeout <= 0 { t.Fatalf("expected startup break-glass timeout default > 0, got %d", cfg.Startup.VaultUnsealBreakglassTimeout) } } func TestValidateRejectsInvalidStartupShutdownCooldown(t *testing.T) { cfg := defaults() cfg.Startup.ShutdownCooldownSeconds = 0 if err := cfg.Validate(); err == nil { t.Fatalf("expected validation error for invalid startup shutdown_cooldown_seconds") } } func TestValidateRejectsInvalidTimeSyncMode(t *testing.T) { cfg := defaults() cfg.Startup.TimeSyncMode = "invalid" if err := cfg.Validate(); err == nil { t.Fatalf("expected validation error for invalid time_sync_mode") } } func TestValidateRejectsBadStoragePVCFormat(t *testing.T) { cfg := defaults() cfg.Startup.StorageCriticalPVCs = []string{"vault-data-vault-0"} if err := cfg.Validate(); err == nil { t.Fatalf("expected validation error for invalid storage_critical_pvcs entry") } } func TestValidateRejectsMissingPostStartProbesWhenRequired(t *testing.T) { cfg := defaults() cfg.Startup.RequirePostStartProbes = true cfg.Startup.PostStartProbes = nil if err := cfg.Validate(); err == nil { t.Fatalf("expected validation error when post start probes are required but empty") } } func TestValidateRejectsMissingServiceChecklistWhenRequired(t *testing.T) { cfg := defaults() cfg.Startup.RequireServiceChecklist = true cfg.Startup.ServiceChecklist = nil if err := cfg.Validate(); err == nil { t.Fatalf("expected validation error when service checklist is required but empty") } } func TestValidateRejectsBadServiceChecklistURL(t *testing.T) { cfg := defaults() cfg.Startup.ServiceChecklist = []ServiceChecklistCheck{ { Name: "grafana", URL: "not-a-url", AcceptedStatuses: []int{200}, TimeoutSeconds: 12, }, } if err := cfg.Validate(); err == nil { t.Fatalf("expected validation error for invalid service checklist url") } } func TestValidateRejectsBadIgnoreFluxKustomizationFormat(t *testing.T) { cfg := defaults() cfg.Startup.IgnoreFluxKustomizations = []string{"jellyfin"} if err := cfg.Validate(); err == nil { t.Fatalf("expected validation error for invalid ignore_flux_kustomizations entry") } } func TestValidateRejectsBadIgnoreWorkloadFormat(t *testing.T) { cfg := defaults() cfg.Startup.IgnoreWorkloads = []string{"maintenance/metis/extra/value"} if err := cfg.Validate(); err == nil { t.Fatalf("expected validation error for invalid ignore_workloads entry") } } func TestValidateRejectsInvalidRequiredNodeLabel(t *testing.T) { cfg := defaults() cfg.Startup.RequiredNodeLabels = map[string]map[string]string{ "titan-09": { "": "true", }, } if err := cfg.Validate(); err == nil { t.Fatalf("expected validation error for invalid required_node_labels entry") } }