mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-27 14:07:31 +00:00
3853124a39
Refresh durable investigation lifecycle from authoritative actions while preserving completed evidence. Keep resolved history reviewable and label recorded plan facts separately from action outcomes. Follow all resource pages during qualification and record live approval, rejection and storage semantic-review results. Integrate current main and preserve its alert ordering correction.
560 lines
23 KiB
Go
560 lines
23 KiB
Go
package qualification
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestWaitForResourcesMatchingIncludesLaterPages(t *testing.T) {
|
|
var pages []string
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path != "/api/resources" || r.URL.Query().Get("limit") != "100" {
|
|
t.Errorf("unexpected resource request: %s", r.URL)
|
|
}
|
|
page := r.URL.Query().Get("page")
|
|
pages = append(pages, page)
|
|
resources := make([]Resource, 0, 100)
|
|
switch page {
|
|
case "1":
|
|
for i := 0; i < 100; i++ {
|
|
resources = append(resources, Resource{ID: fmt.Sprintf("control-%d", i), Name: fmt.Sprintf("control-%d", i)})
|
|
}
|
|
case "2":
|
|
resources = append(resources, Resource{ID: "storage", Name: "worker", Docker: &DockerResource{Health: "unhealthy"}})
|
|
default:
|
|
t.Errorf("unexpected page: %q", page)
|
|
}
|
|
_ = json.NewEncoder(w).Encode(map[string]any{"data": resources, "meta": map[string]int{"totalPages": 2}})
|
|
}))
|
|
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{"service": "worker"}, time.Second, time.Millisecond, func(resources map[string]Resource) error {
|
|
if resources["service"].Docker.Health != "unhealthy" {
|
|
return errors.New("fault not collected")
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil || resources["service"].ID != "storage" || strings.Join(pages, ",") != "1,2" {
|
|
t.Fatalf("resources=%v pages=%v err=%v", resources, pages, err)
|
|
}
|
|
}
|
|
|
|
func TestResourcesDoesNotReturnPartialInventoryWhenLaterPageFails(t *testing.T) {
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Query().Get("page") == "1" {
|
|
_, _ = w.Write([]byte(`{"data":[{"id":"first"}],"meta":{"totalPages":2}}`))
|
|
return
|
|
}
|
|
http.Error(w, "resource inventory unavailable", http.StatusServiceUnavailable)
|
|
}))
|
|
defer server.Close()
|
|
client, err := NewPulseClient(ClientConfig{BaseURL: server.URL})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
resources, err := client.Resources(context.Background())
|
|
if err == nil || resources != nil {
|
|
t.Fatalf("incomplete inventory returned: resources=%v err=%v", resources, err)
|
|
}
|
|
}
|
|
|
|
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 TestTriggerAndWaitRetriesHonestBusyAdmission(t *testing.T) {
|
|
var attempts atomic.Int32
|
|
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":
|
|
if attempts.Add(1) < 3 {
|
|
w.WriteHeader(http.StatusConflict)
|
|
_, _ = w.Write([]byte(`{"code":"patrol_already_running"}`))
|
|
return
|
|
}
|
|
_, _ = w.Write([]byte(`{"success":true,"run_id":"new"}`))
|
|
case "/api/ai/patrol/runs":
|
|
if attempts.Load() < 3 {
|
|
_, _ = w.Write([]byte(`[]`))
|
|
return
|
|
}
|
|
_, _ = w.Write([]byte(`[{"id":"new","started_at":"2099-01-01T00:00:00Z","completed_at":"2099-01-01T00:00:01Z","scope_resource_ids":["r1"]}]`))
|
|
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: 10 * time.Second})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
run, err := client.TriggerAndWait(context.Background(), []string{"r1"}, "", 8*time.Second)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if run.ID != "new" || attempts.Load() != 3 {
|
|
t.Fatalf("run=%q attempts=%d, want new after 3 admission attempts", run.ID, attempts.Load())
|
|
}
|
|
}
|
|
|
|
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":false,"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]["enabled"] != true || 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]["enabled"] != false || 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
|
|
assistantEnabled := 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": assistantEnabled, "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 value, ok := body["enabled"].(bool); ok {
|
|
assistantEnabled = value
|
|
}
|
|
if assistantEnabled && 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]["enabled"] != false || updates[1]["patrol_model"] != "ollama:qwen3:8b" || updates[1]["codex_subscription_enabled"] != false {
|
|
t.Fatalf("route updates after close = %#v", updates)
|
|
}
|
|
if assistantEnabled {
|
|
t.Fatal("suite close did not restore disabled Pulse Intelligence state")
|
|
}
|
|
}
|
|
|
|
func TestPatrolModelSuiteAcquisitionTimeoutIsRouteAware(t *testing.T) {
|
|
requested := 45 * time.Second
|
|
if got := patrolModelSuiteAcquisitionTimeout("openai", requested, AISettings{}); 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, AISettings{}); got != wantSubscription {
|
|
t.Fatalf("%s acquisition timeout = %s, want %s", provider, got, wantSubscription)
|
|
}
|
|
}
|
|
wantLocal := 5*time.Minute + localProviderPreflightEvidenceGrace
|
|
localSettings := AISettings{RequestTimeoutSeconds: 300}
|
|
if got := patrolModelSuiteAcquisitionTimeout("ollama", requested, localSettings); got != wantLocal {
|
|
t.Fatalf("Ollama acquisition timeout = %s, want %s", got, wantLocal)
|
|
}
|
|
if got := patrolModelSuiteAcquisitionTimeout("ollama", requested, AISettings{}); got != wantLocal {
|
|
t.Fatalf("default Ollama acquisition timeout = %s, want %s", got, wantLocal)
|
|
}
|
|
customOpenAI := localSettings
|
|
customOpenAI.OpenAIBaseURL = "http://llama.internal:8080/v1"
|
|
if got := patrolModelSuiteAcquisitionTimeout("openai", requested, customOpenAI); got != wantLocal {
|
|
t.Fatalf("custom OpenAI acquisition timeout = %s, want %s", got, wantLocal)
|
|
}
|
|
longer := 4 * time.Minute
|
|
if got := patrolModelSuiteAcquisitionTimeout("claude-subscription", longer, AISettings{}); got != longer {
|
|
t.Fatalf("long configured acquisition timeout = %s, want %s", got, longer)
|
|
}
|
|
longerLocal := 6 * time.Minute
|
|
if got := patrolModelSuiteAcquisitionTimeout("ollama", longerLocal, localSettings); got != longerLocal {
|
|
t.Fatalf("long local acquisition timeout = %s, want %s", got, longerLocal)
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|