package orchestrator import ( "io" "log" "strings" "testing" "time" "scm.bstein.dev/bstein/ananke/internal/cluster" "scm.bstein.dev/bstein/ananke/internal/execx" "scm.bstein.dev/bstein/ananke/internal/state" ) // TestHookPureHelperCoverage runs one orchestration or CLI step. // Signature: TestHookPureHelperCoverage(t *testing.T). // Why: covers exported helper wrappers so parser/normalizer utility paths stay stable and test-visible. func TestHookPureHelperCoverage(t *testing.T) { if !cluster.TestHookProbeStatusAccepted(200) { t.Fatalf("expected 200 accepted") } if !cluster.TestHookProbeStatusAccepted(401) { t.Fatalf("expected 401 accepted for auth-fronted probes") } if cluster.TestHookProbeStatusAccepted(500) { t.Fatalf("expected 500 rejected") } if !cluster.TestHookChecklistContains(`{"database":"ok"}`, `"database":"ok"`) { t.Fatalf("expected exact marker match") } if !cluster.TestHookChecklistContains("{\n \"database\": \"ok\"\n}", `"database":"ok"`) { t.Fatalf("expected compact whitespace-insensitive marker match") } if cluster.TestHookChecklistContains(`{"status":"bad"}`, `"database":"ok"`) { t.Fatalf("unexpected marker match") } if !cluster.TestHookIsLikelyHostname("metrics.bstein.dev") { t.Fatalf("expected hostname heuristic true") } if cluster.TestHookIsLikelyHostname("not a host/path") { t.Fatalf("expected hostname heuristic false") } if got := cluster.TestHookHostFromURL("https://metrics.bstein.dev/api/health"); got != "metrics.bstein.dev" { t.Fatalf("unexpected parsed host: %q", got) } now := time.Now().UTC() if age := cluster.TestHookIntentAge(state.Intent{UpdatedAt: now.Add(-2 * time.Minute)}); age <= 0 { t.Fatalf("expected positive intent age, got %s", age) } if !cluster.TestHookIntentFresh(state.Intent{UpdatedAt: now.Add(-30 * time.Second)}, 2*time.Minute) { t.Fatalf("expected fresh intent") } if cluster.TestHookIntentFresh(state.Intent{UpdatedAt: now.Add(-5 * time.Minute)}, time.Minute) { t.Fatalf("expected stale intent") } mode, err := cluster.TestHookNormalizeShutdownMode("cluster-only") if err != nil || mode != "cluster-only" { t.Fatalf("normalize shutdown mode failed: mode=%q err=%v", mode, err) } if _, err := cluster.TestHookNormalizeShutdownMode("poweroff"); err == nil { t.Fatalf("expected poweroff shutdown mode rejection") } if _, err := cluster.TestHookNormalizeShutdownMode("invalid-mode"); err == nil { t.Fatalf("expected invalid shutdown mode rejection") } if got := cluster.TestHookParseFluxKustomizationTimeout("45s"); got != 45*time.Second { t.Fatalf("unexpected flux timeout parse: %s", got) } if got := cluster.TestHookParseFluxKustomizationTimeout("not-a-duration"); got != 0 { t.Fatalf("expected zero timeout for invalid duration, got %s", got) } if !cluster.TestHookLooksLikeImmutableJobError("Job update failed: field is immutable") { t.Fatalf("expected immutable job detector match") } if cluster.TestHookLooksLikeImmutableJobError("all good") { t.Fatalf("unexpected immutable job detector match") } if !cluster.TestHookJobLooksFluxManaged("flux-system", "reconcile-services", map[string]string{"kustomize.toolkit.fluxcd.io/name": "services"}, nil) { t.Fatalf("expected flux-managed job by label") } if cluster.TestHookJobLooksFluxManaged("default", "user-job", nil, []string{"CronJob"}) { t.Fatalf("expected cronjob-owned job not treated as flux-managed") } if cluster.TestHookJobFailed(0, 1, []string{"Failed"}, []string{"True"}) { t.Fatalf("expected succeeded job not failed") } if !cluster.TestHookJobFailed(1, 0, []string{"Failed"}, []string{"True"}) { t.Fatalf("expected failed job detection") } if !cluster.TestHookIsTimeSynced("yes") || !cluster.TestHookIsTimeSynced("1") || cluster.TestHookIsTimeSynced("no") { t.Fatalf("unexpected time sync parser behavior") } if got := cluster.TestHookSanitizeReportFileName("startup drill #1"); got == "" { t.Fatalf("expected sanitized report filename") } snapshot := cluster.TestHookParseSnapshotPathFromEtcdSnapshotList("Name Size Created Location\npre-shutdown 4.2M now \"file:///var/lib/rancher/k3s/server/db/snapshots/pre-shutdown\"\n") if snapshot == "" { t.Fatalf("expected snapshot parser to extract path") } snapshotDirect := cluster.TestHookParseSnapshotPathFromEtcdSnapshotList("\n \npre-shutdown 4.2M now /var/lib/rancher/k3s/server/db/snapshots/direct-path,\n") if snapshotDirect == "" { t.Fatalf("expected direct-path snapshot parser extraction") } if empty := cluster.TestHookParseSnapshotPathFromEtcdSnapshotList("no rows"); empty != "" { t.Fatalf("expected empty snapshot parse for non-matching input, got %q", empty) } cfg := lifecycleConfig(t) orch := cluster.New(cfg, &execx.Runner{DryRun: true}, state.New(cfg.State.RunHistoryPath), log.New(io.Discard, "", 0)) peers := orch.TestHookCoordinationPeers() if len(peers) != 0 { t.Fatalf("expected no coordination peers in baseline fixture, got %v", peers) } _ = orch.TestHookEstimatedShutdownSeconds() _ = orch.TestHookEstimatedEmergencyShutdownSeconds() } // TestHookWorkloadHelperCoverage runs one orchestration or CLI step. // Signature: TestHookWorkloadHelperCoverage(t *testing.T). // Why: covers workload helper wrappers so ignored-node, readiness, and failure summarization paths remain deterministic. func TestHookWorkloadHelperCoverage(t *testing.T) { if desired, ready, ok := cluster.TestHookDesiredReady("deployment", true, 3, 2, 0, 0); !ok || desired != 3 || ready != 2 { t.Fatalf("unexpected desiredReady deployment result: desired=%d ready=%d ok=%v", desired, ready, ok) } if desired, ready, ok := cluster.TestHookDesiredReady("daemonset", false, 0, 0, 2, 1); !ok || desired != 2 || ready != 1 { t.Fatalf("unexpected desiredReady daemonset result: desired=%d ready=%d ok=%v", desired, ready, ok) } if _, _, ok := cluster.TestHookDesiredReady("job", false, 0, 0, 0, 0); ok { t.Fatalf("expected unsupported kind to return ok=false") } if !cluster.TestHookPodControllerOwned([]string{"ReplicaSet"}) { t.Fatalf("expected controller-owned pod") } if cluster.TestHookPodControllerOwned([]string{"CronJob"}) { t.Fatalf("expected non-controller pod for this helper") } reason := cluster.TestHookStuckContainerReason( []string{"ImagePullBackOff"}, []string{}, []string{"ImagePullBackOff", "ErrImagePull"}, ) if reason != "ImagePullBackOff" { t.Fatalf("unexpected stuck container reason: %q", reason) } if reason := cluster.TestHookStuckContainerReason(nil, []string{"Running"}, []string{"CrashLoopBackOff"}); reason != "" { t.Fatalf("expected no stuck reason, got %q", reason) } if reason := cluster.TestHookStuckVaultInitReason("Pending", true, 5*time.Minute, 10*time.Second); reason != "VaultInitStuck" { t.Fatalf("expected VaultInitStuck, got %q", reason) } if reason := cluster.TestHookStuckVaultInitReason("Running", true, 5*time.Minute, 10*time.Second); reason != "" { t.Fatalf("expected no vault-init stuck reason when phase is not Pending, got %q", reason) } if !cluster.TestHookPodTargetsIgnoredNode("titan-22", []string{"titan-22"}) { t.Fatalf("expected ignored node match") } if cluster.TestHookPodTargetsIgnoredNode("titan-23", []string{"titan-22"}) { t.Fatalf("expected ignored node mismatch") } if !cluster.TestHookWorkloadTargetsIgnoredNodes("titan-22", nil, []string{"titan-22"}) { t.Fatalf("expected node selector ignore match") } if !cluster.TestHookWorkloadTargetsIgnoredNodes("", []string{"titan-22"}, []string{"titan-22"}) { t.Fatalf("expected affinity-only ignored-node match") } if cluster.TestHookWorkloadTargetsIgnoredNodes("", []string{"titan-23"}, []string{"titan-22"}) { t.Fatalf("expected affinity mismatch to be false") } if count := cluster.TestHookParseWorkloadIgnoreRulesCount([]string{"monitoring/grafana", "monitoring/deployment/kube-state-metrics"}); count != 2 { t.Fatalf("unexpected workload ignore rule count: %d", count) } if !cluster.TestHookWorkloadIgnoredEntries([]string{"monitoring/grafana"}, "monitoring", "deployment", "grafana") { t.Fatalf("expected workload ignored by rules") } if cluster.TestHookWorkloadIgnoredEntries([]string{"monitoring/grafana"}, "monitoring", "deployment", "loki") { t.Fatalf("unexpected workload ignored by rules") } if idx := cluster.TestHookReadyConditionIndex([]string{"Progressing", "Ready"}); idx != 1 { t.Fatalf("unexpected ready condition index: %d", idx) } if idx := cluster.TestHookReadyConditionIndex([]string{"Progressing"}); idx != -1 { t.Fatalf("expected missing ready condition index=-1, got %d", idx) } if got := cluster.TestHookJoinLimited([]string{"a", "b", "c"}, 2); !strings.Contains(got, "+1 more") { t.Fatalf("expected joinLimited truncation marker, got %q", got) } if got := cluster.TestHookJoinLimited([]string{"a", "b"}, 5); got != "a; b" { t.Fatalf("unexpected joinLimited output: %q", got) } candidates := cluster.TestHookNamespaceCandidatesFromIgnoreKustomizations([]string{"flux-system/monitoring", "flux-system/gitea", "invalid"}) if len(candidates) != 2 || candidates[0] != "gitea" || candidates[1] != "monitoring" { t.Fatalf("unexpected namespace candidates: %v", candidates) } }