Files
pulse/internal/ai/qualification/client_test.go
T
rcourtman 3853124a39 Keep Patrol action history consistent with recorded outcomes
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.
2026-09-06 21:23:01 +01:00

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)
}
}