package qualification import ( "context" "encoding/json" "errors" "io" "net/http" "net/http/httptest" "strings" "sync/atomic" "testing" "time" ) func TestTriggerAndWaitAssociatesExactNewScopedRun(t *testing.T) { var triggered atomic.Bool server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") switch r.URL.Path { case "/api/ai/patrol/run": triggered.Store(true) w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(`{"success":true}`)) case "/api/ai/patrol/runs": if !triggered.Load() { _, _ = w.Write([]byte(`[{"id":"old","started_at":"2026-01-01T00:00:00Z","completed_at":"2026-01-01T00:00:01Z"}]`)) return } _, _ = w.Write([]byte(`[{"id":"new","started_at":"2099-01-01T00:00:00Z","completed_at":"2099-01-01T00:00:01Z","scope_resource_ids":["r1"]},{"id":"old","started_at":"2026-01-01T00:00:00Z","completed_at":"2026-01-01T00:00:01Z"}]`)) case "/api/ai/patrol/runs/new": _, _ = w.Write([]byte(`{"id":"new","started_at":"2099-01-01T00:00:00Z","completed_at":"2099-01-01T00:00:01Z","scope_resource_ids":["r1"],"tool_calls":[]}`)) default: http.NotFound(w, r) } })) defer server.Close() client, err := NewPulseClient(ClientConfig{BaseURL: server.URL, Timeout: 5 * time.Second}) if err != nil { t.Fatal(err) } run, err := client.TriggerAndWait(context.Background(), []string{"r1"}, "test", 3*time.Second) if err != nil { t.Fatal(err) } if run.ID != "new" { t.Fatalf("run id = %q", run.ID) } } func TestValidateCollectedScenarioProjectionUsesFaultOracle(t *testing.T) { manifest := validTestManifest() manifest.Faults = []FaultSpec{ {ID: "stopped", Target: "dependency", Oracle: []Predicate{{Probe: "docker.running", Target: "dependency", Operator: "eq", Value: json.RawMessage("false")}}}, {ID: "unhealthy", Target: "client", Oracle: []Predicate{{Probe: "docker.health", Target: "client", Operator: "eq", Value: json.RawMessage(`"unhealthy"`)}}}, {ID: "restart-loop", Target: "worker", Oracle: []Predicate{{Probe: "docker.restart_count", Target: "worker", Operator: "gte", Value: json.RawMessage("4")}}}, } resources := map[string]Resource{ "dependency": {Docker: &DockerResource{ContainerState: "running"}}, "client": {Docker: &DockerResource{Health: "healthy"}}, "worker": {Docker: &DockerResource{RestartCount: 1}}, } if err := validateCollectedScenarioProjection(manifest, resources); err == nil { t.Fatal("expected pre-fault collected projection to be rejected") } resources["dependency"] = Resource{Docker: &DockerResource{ContainerState: "exited"}} resources["client"] = Resource{Docker: &DockerResource{Health: "unhealthy"}} resources["worker"] = Resource{Docker: &DockerResource{RestartCount: 4}} if err := validateCollectedScenarioProjection(manifest, resources); err != nil { t.Fatalf("expected collected projection to satisfy scenario-owned oracles: %v", err) } } func TestValidateCollectedScenarioProjectionRequiresNegativeControlBaseline(t *testing.T) { manifest := validTestManifest() manifest.Faults = nil manifest.NegativeControls = []NegativeControl{{Resource: "healthy", Reason: "healthy control"}} manifest.Baseline = []Predicate{{Probe: "docker.health", Target: "healthy", Operator: "eq", Value: json.RawMessage(`"healthy"`)}} resources := map[string]Resource{"healthy": {Docker: &DockerResource{ContainerState: "running", Health: "starting"}}} if err := validateCollectedScenarioProjection(manifest, resources); err == nil { t.Fatal("expected stale starting projection to be rejected for a healthy negative control") } resources["healthy"] = Resource{Docker: &DockerResource{ContainerState: "running", Health: "healthy"}} if err := validateCollectedScenarioProjection(manifest, resources); err != nil { t.Fatalf("expected collected negative control to satisfy scenario-owned baseline: %v", err) } } func TestWaitForResourcesMatchingPollsPastStaleState(t *testing.T) { var calls atomic.Int32 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") call := calls.Add(1) health := "healthy" if call >= 2 { health = "unhealthy" } _ = json.NewEncoder(w).Encode(map[string]any{"data": []map[string]any{{ "id": "r1", "type": "app-container", "name": "fixture", "docker": map[string]any{"health": health}, }}}) })) defer server.Close() client, err := NewPulseClient(ClientConfig{BaseURL: server.URL}) if err != nil { t.Fatal(err) } resources, err := client.WaitForResourcesMatching(context.Background(), map[string]string{"target": "fixture"}, time.Second, time.Millisecond, func(resources map[string]Resource) error { if resources["target"].Docker == nil || resources["target"].Docker.Health != "unhealthy" { return errors.New("fault state is not collected yet") } return nil }) if err != nil { t.Fatal(err) } if calls.Load() < 2 || resources["target"].Docker.Health != "unhealthy" { t.Fatalf("collection returned before fault projection converged: calls=%d resources=%+v", calls.Load(), resources) } } func TestNewPulseClientRejectsCredentialsInReportableBaseURL(t *testing.T) { if _, err := NewPulseClient(ClientConfig{BaseURL: "https://admin:secret@example.test"}); err == nil { t.Fatal("base URL credentials must be rejected rather than persisted in reports") } } func TestOverridePatrolModelTemporarilyEnablesAndRestoresSubscriptionRoute(t *testing.T) { requests := make([]map[string]any, 0, 2) server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") switch { case r.Method == http.MethodGet && r.URL.Path == "/api/settings/ai": _, _ = w.Write([]byte(`{"enabled":true,"model":"ollama:qwen3:8b","patrol_model":"ollama:qwen3:8b","codex_subscription_enabled":false}`)) case r.Method == http.MethodPut && r.URL.Path == "/api/settings/ai/update": var body map[string]any if err := json.NewDecoder(r.Body).Decode(&body); err != nil { t.Fatal(err) } requests = append(requests, body) _, _ = w.Write([]byte(`{}`)) default: http.NotFound(w, r) } })) defer server.Close() client, err := NewPulseClient(ClientConfig{BaseURL: server.URL}) if err != nil { t.Fatal(err) } restore, err := client.OverridePatrolModel(context.Background(), "codex-subscription:gpt-5.6-luna") if err != nil { t.Fatal(err) } if err := restore(context.Background()); err != nil { t.Fatal(err) } if len(requests) != 2 || requests[0]["codex_subscription_enabled"] != true || requests[0]["patrol_model"] != "codex-subscription:gpt-5.6-luna" { t.Fatalf("override request = %#v", requests) } if requests[1]["codex_subscription_enabled"] != false || requests[1]["patrol_model"] != "ollama:qwen3:8b" { t.Fatalf("restore request = %#v", requests[1]) } } func TestAcquirePatrolModelSuitePinsOneRouteAndUsesFreshAsyncPreflight(t *testing.T) { currentModel := "ollama:qwen3:8b" codexEnabled := false preflight := map[string]any{ "success": true, "provider": "ollama", "model": "qwen3:8b", "tool_call_observed": true, "summary": "old preflight", "recorded_at_unix": int64(100), } var updates []map[string]any preflightPosts := 0 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") switch { case r.Method == http.MethodGet && r.URL.Path == "/api/settings/ai": _ = json.NewEncoder(w).Encode(map[string]any{ "enabled": true, "model": "ollama:qwen3:8b", "patrol_model": currentModel, "patrol_enabled": true, "codex_subscription_enabled": codexEnabled, "patrol_preflight": preflight, }) case r.Method == http.MethodPut && r.URL.Path == "/api/settings/ai/update": var body map[string]any if err := json.NewDecoder(r.Body).Decode(&body); err != nil { t.Fatal(err) } updates = append(updates, body) if value, ok := body["patrol_model"].(string); ok { currentModel = value } if value, ok := body["codex_subscription_enabled"].(bool); ok { codexEnabled = value } if currentModel == "codex-subscription:gpt-5.6-sol" { preflight = map[string]any{ "success": true, "provider": "codex-subscription", "model": "gpt-5.6-sol", "tool_call_observed": true, "summary": "tool calling verified", "recorded_at_unix": int64(200), } } _, _ = w.Write([]byte(`{}`)) case r.Method == http.MethodPost && r.URL.Path == "/api/ai/patrol/preflight": preflightPosts++ _, _ = w.Write([]byte(`{"success":true,"provider":"codex-subscription","model":"gpt-5.6-sol","tool_call_observed":true}`)) case r.Method == http.MethodGet && r.URL.Path == "/api/ai/patrol/status": _ = json.NewEncoder(w).Encode(map[string]any{"readiness": map[string]any{ "ready": true, "provider": "codex-subscription", "model": "codex-subscription:gpt-5.6-sol", }}) default: http.NotFound(w, r) } })) defer server.Close() client, err := NewPulseClient(ClientConfig{BaseURL: server.URL}) if err != nil { t.Fatal(err) } lease, err := client.AcquirePatrolModelSuite(context.Background(), "codex-subscription:gpt-5.6-sol", time.Second, time.Millisecond) if err != nil { t.Fatal(err) } if lease.Model != "codex-subscription:gpt-5.6-sol" || preflightPosts != 0 { t.Fatalf("lease = %+v, synchronous preflight posts = %d", lease, preflightPosts) } if len(updates) != 1 { t.Fatalf("route updates before close = %#v", updates) } if err := lease.Close(context.Background()); err != nil { t.Fatal(err) } if len(updates) != 2 || updates[1]["patrol_model"] != "ollama:qwen3:8b" || updates[1]["codex_subscription_enabled"] != false { t.Fatalf("route updates after close = %#v", updates) } } func TestPatrolModelSuiteAcquisitionTimeoutIsRouteAware(t *testing.T) { requested := 45 * time.Second if got := patrolModelSuiteAcquisitionTimeout("openai", requested); got != requested { t.Fatalf("API route acquisition timeout = %s, want %s", got, requested) } wantSubscription := 2*time.Minute + subscriptionAgentPreflightEvidenceGrace for _, provider := range []string{"codex-subscription", "claude-subscription"} { if got := patrolModelSuiteAcquisitionTimeout(provider, requested); got != wantSubscription { t.Fatalf("%s acquisition timeout = %s, want %s", provider, got, wantSubscription) } } longer := 4 * time.Minute if got := patrolModelSuiteAcquisitionTimeout("claude-subscription", longer); got != longer { t.Fatalf("long configured acquisition timeout = %s, want %s", got, longer) } } func TestAcquirePatrolModelSuitePreflightsConfiguredRouteOnceWithoutSettingsWrite(t *testing.T) { updates := 0 preflightPosts := 0 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") switch { case r.Method == http.MethodGet && r.URL.Path == "/api/settings/ai": _, _ = w.Write([]byte(`{"enabled":true,"model":"codex-subscription:gpt-5.6-sol","patrol_model":"codex-subscription:gpt-5.6-sol","patrol_enabled":true,"codex_subscription_enabled":true}`)) case r.Method == http.MethodPut && r.URL.Path == "/api/settings/ai/update": updates++ _, _ = w.Write([]byte(`{}`)) case r.Method == http.MethodPost && r.URL.Path == "/api/ai/patrol/preflight": preflightPosts++ _, _ = w.Write([]byte(`{"success":true,"provider":"codex-subscription","model":"gpt-5.6-sol","tool_call_observed":true,"message":"verified"}`)) case r.Method == http.MethodGet && r.URL.Path == "/api/ai/patrol/status": _, _ = w.Write([]byte(`{"readiness":{"ready":true,"provider":"codex-subscription","model":"codex-subscription:gpt-5.6-sol"}}`)) default: http.NotFound(w, r) } })) defer server.Close() client, err := NewPulseClient(ClientConfig{BaseURL: server.URL}) if err != nil { t.Fatal(err) } lease, err := client.AcquirePatrolModelSuite(context.Background(), "", time.Second, time.Millisecond) if err != nil { t.Fatal(err) } if updates != 0 || preflightPosts != 1 { t.Fatalf("settings updates = %d, preflight posts = %d", updates, preflightPosts) } if err := lease.Close(context.Background()); err != nil { t.Fatal(err) } if updates != 0 { t.Fatalf("no-op lease close wrote settings %d time(s)", updates) } } func TestAcquirePatrolModelSuiteRequiresFreshEvidenceWhenEnablingSameSubscriptionRoute(t *testing.T) { codexEnabled := false preflightSuccess := false updates := 0 oldRecordedAt := time.Now().Add(-time.Second).Unix() server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") switch { case r.Method == http.MethodGet && r.URL.Path == "/api/settings/ai": _ = json.NewEncoder(w).Encode(map[string]any{ "enabled": true, "model": "codex-subscription:gpt-5.6-sol", "patrol_model": "codex-subscription:gpt-5.6-sol", "patrol_enabled": true, "codex_subscription_enabled": codexEnabled, "patrol_preflight": map[string]any{ "success": preflightSuccess, "provider": "codex-subscription", "model": "gpt-5.6-sol", "tool_call_observed": preflightSuccess, "summary": "fresh result", "recorded_at_unix": map[bool]int64{false: oldRecordedAt, true: time.Now().Unix()}[preflightSuccess], }, }) case r.Method == http.MethodPut && r.URL.Path == "/api/settings/ai/update": updates++ var body map[string]any if err := json.NewDecoder(r.Body).Decode(&body); err != nil { t.Fatal(err) } if value, ok := body["codex_subscription_enabled"].(bool); ok { codexEnabled = value preflightSuccess = value } _, _ = w.Write([]byte(`{}`)) case r.Method == http.MethodGet && r.URL.Path == "/api/ai/patrol/status": _, _ = w.Write([]byte(`{"readiness":{"ready":true,"provider":"codex-subscription","model":"codex-subscription:gpt-5.6-sol"}}`)) default: http.NotFound(w, r) } })) defer server.Close() client, err := NewPulseClient(ClientConfig{BaseURL: server.URL}) if err != nil { t.Fatal(err) } lease, err := client.AcquirePatrolModelSuite(context.Background(), "codex-subscription:gpt-5.6-sol", time.Second, time.Millisecond) if err != nil { t.Fatal(err) } if !lease.Preflight.Success || updates != 1 { t.Fatalf("lease preflight = %+v, updates = %d", lease.Preflight, updates) } if err := lease.Close(context.Background()); err != nil { t.Fatal(err) } if updates != 2 || codexEnabled { t.Fatalf("restored updates = %d, codex enabled = %t", updates, codexEnabled) } } func TestAcquirePatrolModelSuiteReturnsOneFreshFailureAndRestoresRoute(t *testing.T) { currentModel := "ollama:qwen3:8b" updates := 0 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") switch { case r.Method == http.MethodGet && r.URL.Path == "/api/settings/ai": response := map[string]any{ "enabled": true, "model": "ollama:qwen3:8b", "patrol_model": currentModel, "patrol_enabled": true, "codex_subscription_enabled": currentModel == "codex-subscription:gpt-5.6-sol", } if currentModel == "codex-subscription:gpt-5.6-sol" { response["patrol_preflight"] = map[string]any{ "success": false, "provider": "codex-subscription", "model": "gpt-5.6-sol", "cause": "provider_connection", "summary": "Provider connection issue", "recorded_at_unix": int64(200), } } _ = json.NewEncoder(w).Encode(response) case r.Method == http.MethodPut && r.URL.Path == "/api/settings/ai/update": updates++ var body map[string]any if err := json.NewDecoder(r.Body).Decode(&body); err != nil { t.Fatal(err) } if value, ok := body["patrol_model"].(string); ok { currentModel = value } _, _ = w.Write([]byte(`{}`)) default: http.NotFound(w, r) } })) defer server.Close() client, err := NewPulseClient(ClientConfig{BaseURL: server.URL}) if err != nil { t.Fatal(err) } _, err = client.AcquirePatrolModelSuite(context.Background(), "codex-subscription:gpt-5.6-sol", time.Second, time.Millisecond) if err == nil || !strings.Contains(err.Error(), "Provider connection issue") { t.Fatalf("acquire error = %v", err) } if updates != 2 || currentModel != "ollama:qwen3:8b" { t.Fatalf("updates = %d, current model = %q", updates, currentModel) } } func TestDecideActionBindsExactPlanHash(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/api/actions/action-1/decision" || r.Method != http.MethodPost { http.NotFound(w, r) return } payload, _ := io.ReadAll(r.Body) var body map[string]any if err := json.Unmarshal(payload, &body); err != nil { t.Fatal(err) } if body["outcome"] != "rejected" || body["planHash"] != "sha256:exact" { t.Fatalf("decision body = %s", payload) } w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(`{"actionId":"action-1","state":"rejected","audit":{"id":"action-1","state":"rejected","plan":{"planHash":"sha256:exact"}}}`)) })) defer server.Close() client, err := NewPulseClient(ClientConfig{BaseURL: server.URL}) if err != nil { t.Fatal(err) } audit, err := client.DecideAction(context.Background(), "action-1", "rejected", "operator rejected", "sha256:exact") if err != nil { t.Fatal(err) } if audit.ID != "action-1" || audit.Plan.PlanHash != "sha256:exact" { t.Fatalf("audit = %+v", audit) } } func TestFilterRunFindingsUsesRunIDsOrFreshExactResources(t *testing.T) { now := time.Now().UTC() before := []Finding{{ID: "updated", ResourceID: "r1", LastSeenAt: now.Add(-time.Minute)}} after := []Finding{ {ID: "run-owned", ResourceID: "other", LastSeenAt: now}, {ID: "updated", ResourceID: "r1", LastSeenAt: now}, {ID: "stale", ResourceID: "r1", LastSeenAt: now.Add(-2 * time.Minute)}, } got := filterRunFindings(before, after, PatrolRun{FindingIDs: []string{"run-owned"}}, map[string]Resource{"target": {ID: "r1"}}, now.Add(-time.Second)) encoded, _ := json.Marshal(got) if len(got) != 2 || got[0].ID != "run-owned" || got[1].ID != "updated" { t.Fatalf("findings = %s", encoded) } }