package cluster import ( "context" "errors" "io" "log" "path/filepath" "strings" "testing" "time" "scm.bstein.dev/bstein/ananke/internal/config" "scm.bstein.dev/bstein/ananke/internal/execx" "scm.bstein.dev/bstein/ananke/internal/state" ) // TestCriticalEndpointHelpers runs one orchestration or CLI step. // Signature: TestCriticalEndpointHelpers(t *testing.T). // Why: covers critical endpoint parsing and readiness checks that gate startup completion. func TestCriticalEndpointHelpers(t *testing.T) { cfg := config.Config{ Startup: config.Startup{ CriticalServiceEndpoints: []string{"monitoring/victoria-metrics-single-server"}, }, } orch := buildOrchestratorWithStubs(t, cfg, []commandStub{ {match: matchContains("kubectl", "get endpoints victoria-metrics-single-server"), out: "10.42.0.10\n10.42.0.11\n"}, }) ok, detail, ns, svc, err := orch.criticalServiceEndpointsReady(context.Background()) if err != nil || !ok { t.Fatalf("expected criticalServiceEndpointsReady success, got ok=%v detail=%q ns=%q svc=%q err=%v", ok, detail, ns, svc, err) } if detail != "services=1" { t.Fatalf("unexpected readiness detail: %q", detail) } gotNS, gotSvc, err := parseCriticalServiceEndpoint("monitoring/victoria-metrics-single-server") if err != nil || gotNS != "monitoring" || gotSvc != "victoria-metrics-single-server" { t.Fatalf("unexpected parse result ns=%q svc=%q err=%v", gotNS, gotSvc, err) } if _, _, err := parseCriticalServiceEndpoint("invalid"); err == nil { t.Fatalf("expected parseCriticalServiceEndpoint error") } } // TestCriticalEndpointAutoHealWorkflow runs one orchestration or CLI step. // Signature: TestCriticalEndpointAutoHealWorkflow(t *testing.T). // Why: covers endpoint-zero recovery where startup heals workload replicas before succeeding. func TestCriticalEndpointAutoHealWorkflow(t *testing.T) { cfg := config.Config{ Startup: config.Startup{ CriticalServiceEndpointWaitSec: 2, CriticalServiceEndpointPollSec: 1, CriticalServiceEndpoints: []string{"monitoring/victoria-metrics-single-server"}, }, State: config.State{ Dir: t.TempDir(), ReportsDir: filepath.Join(t.TempDir(), "reports"), RunHistoryPath: filepath.Join(t.TempDir(), "runs.json"), }, } orch := &Orchestrator{ cfg: cfg, runner: &execx.Runner{}, store: state.New(cfg.State.RunHistoryPath), log: log.New(io.Discard, "", 0), } endpointChecks := 0 dispatch := func(_ context.Context, _ time.Duration, name string, args ...string) (string, error) { joined := name + " " + strings.Join(args, " ") if strings.Contains(joined, "get endpoints victoria-metrics-single-server") { endpointChecks++ if endpointChecks == 1 { return "", nil } return "10.42.0.10\n", nil } if strings.Contains(joined, "scale deployment victoria-metrics-single-server") { return "", errors.New(`Error from server (NotFound): deployments.apps "victoria-metrics-single-server" not found`) } if strings.Contains(joined, "scale statefulset victoria-metrics-single-server") { return "", nil } if strings.Contains(joined, "rollout status statefulset/victoria-metrics-single-server") { return "statefulset rolled out", nil } return "", nil } orch.runOverride = dispatch orch.runSensitiveOverride = dispatch if err := orch.waitForCriticalServiceEndpoints(context.Background()); err != nil { t.Fatalf("waitForCriticalServiceEndpoints failed: %v", err) } if endpointChecks < 2 { t.Fatalf("expected repeated endpoint checks, got %d", endpointChecks) } }