Files
pulse/cmd/agent-probe/main_test.go
T
rcourtman ee8a24e14a backend and governance: MCP contract, agent capabilities, API, and release-control
Manifest-backed MCP tools, prompts, and resources with surface affordance contracts; agent capability manifest and governance projection; API contract tests and capability route projection; operations-loop and intelligence-funnel telemetry; release-control subsystem documentation, registry, and tooling; licensing and configuration.
2026-06-23 17:26:15 +01:00

184 lines
5.2 KiB
Go

package main
import (
"context"
"io"
"net/http"
"net/http/httptest"
"os"
"strings"
"testing"
"github.com/rcourtman/pulse-go-rewrite/internal/agentcapabilities"
)
// TestPickFocus_PrefersCriticalThenWarningThenInfo pins the focus
// rule the probe uses for "where do I look first?". The unit test
// is here rather than against the substrate because the rule is a
// probe-side triage convention, not a contract Pulse owns; agents
// building on the substrate can implement their own ordering.
func TestPickFocus_PrefersCriticalThenWarningThenInfo(t *testing.T) {
mk := func(id string, c, w, i, p int) fleetResource {
r := fleetResource{CanonicalID: id, PendingApprovalCount: p}
r.Findings.Critical = c
r.Findings.Warning = w
r.Findings.Info = i
r.Findings.Total = c + w + i
return r
}
cases := []struct {
name string
resources []fleetResource
want string
}{
{
name: "single critical beats many warnings",
resources: []fleetResource{
mk("vm:noisy", 0, 50, 0, 0),
mk("vm:critical", 1, 0, 0, 0),
mk("vm:quiet", 0, 0, 0, 0),
},
want: "vm:critical",
},
{
name: "tie on severity broken by count",
resources: []fleetResource{
mk("vm:warm", 0, 1, 0, 0),
mk("vm:warmer", 0, 3, 0, 0),
},
want: "vm:warmer",
},
{
name: "no findings — pending approvals as tiebreaker",
resources: []fleetResource{
mk("vm:idle", 0, 0, 0, 0),
mk("vm:waiting", 0, 0, 0, 2),
},
want: "vm:waiting",
},
{
name: "no findings or approvals — first wins so depth step still runs",
resources: []fleetResource{
mk("vm:first", 0, 0, 0, 0),
mk("vm:second", 0, 0, 0, 0),
},
want: "vm:first",
},
{
name: "empty fleet returns nil",
resources: nil,
want: "",
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got := pickFocus(tc.resources)
if tc.want == "" {
if got != nil {
t.Fatalf("expected nil for empty fleet; got %+v", got)
}
return
}
if got == nil {
t.Fatalf("expected %q; got nil", tc.want)
}
if got.CanonicalID != tc.want {
t.Errorf("focus = %q; want %q", got.CanonicalID, tc.want)
}
})
}
}
func TestFetchFleetUsesSharedCapabilityBodyExecutor(t *testing.T) {
var got struct {
Method string
Path string
Token string
}
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
got.Method = r.Method
got.Path = r.URL.EscapedPath()
got.Token = r.Header.Get(agentcapabilities.AgentAPITokenHeader)
w.WriteHeader(http.StatusAccepted)
_, _ = w.Write([]byte(`{
"generatedAt": "2026-06-18T07:00:00Z",
"resources": [
{
"canonicalId": "vm:101",
"resourceType": "vm",
"resourceName": "web",
"findings": {"total": 1, "critical": 1, "warning": 0, "info": 0},
"pendingApprovalCount": 2
}
]
}`))
}))
defer server.Close()
fleet, err := fetchFleet(context.Background(), server.Client(), server.URL, []agentcapabilities.Capability{{
Name: agentcapabilities.FleetContextCapabilityName,
Method: http.MethodGet,
Path: "/api/agent/fleet-context",
}}, "probe-token")
if err != nil {
t.Fatalf("fetchFleet: %v", err)
}
if len(fleet.Resources) != 1 || fleet.Resources[0].CanonicalID != "vm:101" || fleet.Resources[0].Findings.Critical != 1 {
t.Fatalf("fleet = %+v", fleet)
}
if got.Method != http.MethodGet || got.Path != "/api/agent/fleet-context" {
t.Fatalf("request method/path = %s %s", got.Method, got.Path)
}
if got.Token != "probe-token" {
t.Fatalf("token = %q", got.Token)
}
}
func TestReadOneSSEEventUsesSharedActionableFilter(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("Accept") != agentcapabilities.AgentSSEAccept {
t.Errorf("Accept header = %q, want %q", r.Header.Get("Accept"), agentcapabilities.AgentSSEAccept)
}
if r.Header.Get(agentcapabilities.AgentAPITokenHeader) != "probe-token" {
t.Errorf("%s header = %q", agentcapabilities.AgentAPITokenHeader, r.Header.Get(agentcapabilities.AgentAPITokenHeader))
}
w.Header().Set("Content-Type", agentcapabilities.AgentSSEAccept)
_, _ = w.Write([]byte(strings.Join([]string{
"event: " + string(agentcapabilities.EventKindStreamConnected),
"data: {}",
"",
"event: " + string(agentcapabilities.EventKindHeartbeat),
"",
"event: " + string(agentcapabilities.EventKindFindingCreated),
"data: {\"findingId\":\"f1\"}",
"",
}, "\n")))
}))
defer server.Close()
oldStdout := os.Stdout
r, w, err := os.Pipe()
if err != nil {
t.Fatalf("pipe stdout: %v", err)
}
os.Stdout = w
err = readOneSSEEvent(context.Background(), server.URL, agentcapabilities.AgentEventsPath, "probe-token")
_ = w.Close()
os.Stdout = oldStdout
output, readErr := io.ReadAll(r)
if readErr != nil {
t.Fatalf("read stdout: %v", readErr)
}
if err != nil {
t.Fatalf("readOneSSEEvent: %v", err)
}
text := string(output)
if !strings.Contains(text, "event: "+string(agentcapabilities.EventKindFindingCreated)) {
t.Fatalf("stdout missing finding event: %s", text)
}
if strings.Contains(text, string(agentcapabilities.EventKindStreamConnected)) || strings.Contains(text, string(agentcapabilities.EventKindHeartbeat)) {
t.Fatalf("stdout included transport event: %s", text)
}
}