mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-24 20:22:53 +00:00
441 lines
18 KiB
Go
441 lines
18 KiB
Go
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)
|
|
}
|
|
}
|