Files
pulse/internal/api/agent_activity_telemetry_test.go
T
2026-07-11 19:49:17 +01:00

219 lines
9.9 KiB
Go

package api
import (
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"github.com/rcourtman/pulse-go-rewrite/internal/agentcapabilities"
"github.com/rcourtman/pulse-go-rewrite/internal/config"
)
func TestRecordExternalAgentCapabilityActivityUsesCapabilityScope(t *testing.T) {
persistence := config.NewConfigPersistence(t.TempDir())
router := &Router{persistence: persistence}
readOnlyReq := httptest.NewRequest(http.MethodGet, "/api/agent/fleet-context", nil)
attachAPITokenRecord(readOnlyReq, &config.APITokenRecord{Scopes: []string{config.ScopeMonitoringRead}})
router.recordExternalAgentCapabilityActivity(readOnlyReq, agentcapabilities.FleetContextCapabilityName)
history, err := persistence.LoadExternalAgentActivityHistory()
if err != nil {
t.Fatalf("LoadExternalAgentActivityHistory: %v", err)
}
if len(history.Events) != 1 {
t.Fatalf("read-only fleet context should record external-agent activity, got %d: %+v", len(history.Events), history.Events)
}
if history.Events[0].Surface != config.ExternalAgentActivitySurfaceAgentAPI ||
history.Events[0].Activity != config.ExternalAgentActivityFleetContext {
t.Fatalf("unexpected read-only external-agent activity event: %+v", history.Events[0])
}
router.recordExternalAgentCapabilityActivity(readOnlyReq, agentcapabilities.PlanActionCapabilityName)
history, err = persistence.LoadExternalAgentActivityHistory()
if err != nil {
t.Fatalf("LoadExternalAgentActivityHistory: %v", err)
}
if len(history.Events) != 1 {
t.Fatalf("read-only token should not record action capability activity, got %d: %+v", len(history.Events), history.Events)
}
actionReq := httptest.NewRequest(http.MethodPost, "/api/actions/plan", nil)
attachAPITokenRecord(actionReq, &config.APITokenRecord{Scopes: []string{config.ScopeActionsPlan}})
router.recordExternalAgentCapabilityActivity(actionReq, agentcapabilities.PlanActionCapabilityName)
history, err = persistence.LoadExternalAgentActivityHistory()
if err != nil {
t.Fatalf("LoadExternalAgentActivityHistory: %v", err)
}
if len(history.Events) != 2 {
t.Fatalf("expected two external-agent activity events, got %d: %+v", len(history.Events), history.Events)
}
if history.Events[1].Activity != config.ExternalAgentActivityActionPlan {
t.Fatalf("action capability activity = %q, want %q", history.Events[1].Activity, config.ExternalAgentActivityActionPlan)
}
}
func TestRecordExternalAgentCapabilityActivityPreservesMCPAdapterSurface(t *testing.T) {
persistence := config.NewConfigPersistence(t.TempDir())
router := &Router{persistence: persistence}
mcpReq := httptest.NewRequest(http.MethodGet, "/api/agent/fleet-context", nil)
mcpReq.Header.Set(agentcapabilities.AgentSurfaceHeader, agentcapabilities.AgentSurfacePulseMCP)
attachAPITokenRecord(mcpReq, &config.APITokenRecord{Scopes: []string{config.ScopeMonitoringRead}})
router.recordExternalAgentCapabilityActivity(mcpReq, agentcapabilities.FleetContextCapabilityName)
plainReq := httptest.NewRequest(http.MethodGet, "/api/agent/fleet-context", nil)
plainReq.Header.Set(agentcapabilities.AgentSurfaceHeader, "unknown-client")
attachAPITokenRecord(plainReq, &config.APITokenRecord{Scopes: []string{config.ScopeMonitoringRead}})
router.recordExternalAgentCapabilityActivity(plainReq, agentcapabilities.FleetContextCapabilityName)
history, err := persistence.LoadExternalAgentActivityHistory()
if err != nil {
t.Fatalf("LoadExternalAgentActivityHistory: %v", err)
}
if len(history.Events) != 2 {
t.Fatalf("expected two external-agent activity events, got %d: %+v", len(history.Events), history.Events)
}
if history.Events[0].Surface != config.ExternalAgentActivitySurfacePulseMCP {
t.Fatalf("MCP adapter event surface = %q, want %q", history.Events[0].Surface, config.ExternalAgentActivitySurfacePulseMCP)
}
if history.Events[1].Surface != config.ExternalAgentActivitySurfaceAgentAPI {
t.Fatalf("unknown adapter event surface = %q, want %q", history.Events[1].Surface, config.ExternalAgentActivitySurfaceAgentAPI)
}
}
func TestHandleAgentWorkflowPromptActivityRecordsMCPAdapterSurface(t *testing.T) {
persistence := config.NewConfigPersistence(t.TempDir())
router := &Router{persistence: persistence}
body := `{"name":"` + agentcapabilities.PulseWorkflowPromptOperationsLoop + `"}`
req := httptest.NewRequest(http.MethodPost, agentcapabilities.AgentWorkflowPromptActivityPath, strings.NewReader(body))
req.Header.Set(agentcapabilities.AgentSurfaceHeader, agentcapabilities.AgentSurfacePulseMCP)
attachAPITokenRecord(req, &config.APITokenRecord{Scopes: []string{config.ScopeMonitoringRead}})
rec := httptest.NewRecorder()
router.HandleAgentWorkflowPromptActivity(rec, req)
if rec.Code != http.StatusNoContent {
t.Fatalf("HandleAgentWorkflowPromptActivity status = %d body=%q, want 204", rec.Code, rec.Body.String())
}
history, err := persistence.LoadWorkflowPromptActivityHistory()
if err != nil {
t.Fatalf("LoadWorkflowPromptActivityHistory: %v", err)
}
if len(history.Events) != 1 {
t.Fatalf("expected one workflow prompt activity event, got %d: %+v", len(history.Events), history.Events)
}
if history.Events[0].Surface != config.WorkflowPromptActivitySurfacePulseMCP ||
history.Events[0].PromptName != agentcapabilities.PulseWorkflowPromptOperationsLoop {
t.Fatalf("unexpected workflow prompt activity event: %+v", history.Events[0])
}
}
func TestHandleAgentWorkflowPromptActivityRejectsUnknownPrompt(t *testing.T) {
persistence := config.NewConfigPersistence(t.TempDir())
router := &Router{persistence: persistence}
req := httptest.NewRequest(http.MethodPost, agentcapabilities.AgentWorkflowPromptActivityPath, strings.NewReader(`{"name":"unknown"}`))
attachAPITokenRecord(req, &config.APITokenRecord{Scopes: []string{config.ScopeMonitoringRead}})
rec := httptest.NewRecorder()
router.HandleAgentWorkflowPromptActivity(rec, req)
if rec.Code != http.StatusBadRequest {
t.Fatalf("unknown prompt status = %d, want 400", rec.Code)
}
history, err := persistence.LoadWorkflowPromptActivityHistory()
if err != nil {
t.Fatalf("LoadWorkflowPromptActivityHistory: %v", err)
}
if len(history.Events) != 0 {
t.Fatalf("unknown prompt should not record activity, got %+v", history.Events)
}
}
func TestAPITokenCoversExternalAgentSurfaceRejectsExpiredToken(t *testing.T) {
now := time.Date(2026, 6, 20, 9, 30, 0, 0, time.UTC)
expiredAt := now.Add(-time.Minute)
token := &config.APITokenRecord{
Scopes: []string{config.ScopeWildcard},
ExpiresAt: &expiredAt,
}
if apiTokenCoversExternalAgentSurface(token, now, config.ScopeMonitoringRead) {
t.Fatal("expired token should not cover the external-agent surface")
}
}
func TestRecordExternalAgentCapabilityActivityKeepsProvisioningScopesRouteSpecific(t *testing.T) {
persistence := config.NewConfigPersistence(t.TempDir())
router := &Router{persistence: persistence}
readReq := httptest.NewRequest(http.MethodGet, "/api/config/nodes", nil)
attachAPITokenRecord(readReq, &config.APITokenRecord{Scopes: []string{config.ScopeSettingsRead}})
router.recordExternalAgentCapabilityActivity(readReq, agentcapabilities.ListNodesCapabilityName)
router.recordExternalAgentCapabilityActivity(readReq, agentcapabilities.AddNodeCapabilityName)
writeReq := httptest.NewRequest(http.MethodPost, "/api/config/nodes", nil)
attachAPITokenRecord(writeReq, &config.APITokenRecord{Scopes: []string{config.ScopeSettingsWrite}})
router.recordExternalAgentCapabilityActivity(writeReq, agentcapabilities.AddNodeCapabilityName)
history, err := persistence.LoadExternalAgentActivityHistory()
if err != nil {
t.Fatalf("LoadExternalAgentActivityHistory: %v", err)
}
if len(history.Events) != 2 {
t.Fatalf("expected read list_nodes and write add_node activity only, got %d: %+v", len(history.Events), history.Events)
}
for _, event := range history.Events {
if event.Activity != config.ExternalAgentActivityProvisioning {
t.Fatalf("provisioning activity = %q, want %q", event.Activity, config.ExternalAgentActivityProvisioning)
}
}
}
func TestExternalAgentActivityForCanonicalPulseMCPSurfaceTools(t *testing.T) {
manifest := agentcapabilities.CanonicalManifest()
capabilities := agentcapabilities.ManifestSurfaceToolCapabilities(manifest, agentcapabilities.SurfaceIDPulseMCP)
if len(capabilities) == 0 {
t.Fatal("Pulse MCP surface must publish request/response tools")
}
for _, capability := range capabilities {
activity, ok := externalAgentActivityForCapability(capability.Name)
if !ok {
t.Fatalf("MCP-published capability %q has no external-agent activity class", capability.Name)
}
if activity == "" {
t.Fatalf("MCP-published capability %q maps to an empty external-agent activity class", capability.Name)
}
}
if activity, ok := externalAgentActivityForCapability(agentcapabilities.EventSubscriptionCapabilityName); !ok || activity != config.ExternalAgentActivityEventStream {
t.Fatalf("%s activity = %q, %v; want %s", agentcapabilities.EventSubscriptionCapabilityName, activity, ok, config.ExternalAgentActivityEventStream)
}
}
func TestRecordExternalAgentCapabilityActivityUsesCapabilityClass(t *testing.T) {
persistence := config.NewConfigPersistence(t.TempDir())
router := &Router{persistence: persistence}
req := httptest.NewRequest(http.MethodPut, "/api/resources/vm:101/operator-state", nil)
attachAPITokenRecord(req, &config.APITokenRecord{Scopes: []string{config.ScopeWildcard}})
router.recordExternalAgentCapabilityActivity(req, agentcapabilities.SetOperatorStateCapabilityName)
router.recordExternalAgentCapabilityActivity(req, "unknown_capability")
history, err := persistence.LoadExternalAgentActivityHistory()
if err != nil {
t.Fatalf("LoadExternalAgentActivityHistory: %v", err)
}
if len(history.Events) != 1 {
t.Fatalf("expected one external-agent activity event, got %d: %+v", len(history.Events), history.Events)
}
if history.Events[0].Activity != config.ExternalAgentActivityOperatorState {
t.Fatalf("operator-state capability activity = %q, want %q", history.Events[0].Activity, config.ExternalAgentActivityOperatorState)
}
}