mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 10:35:51 +00:00
219 lines
9.9 KiB
Go
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)
|
|
}
|
|
}
|