Files
pulse/internal/api/contract_test.go
T
rcourtman 5016cbc2ba Add vSphere network inventory
Project vCenter network inventory through canonical resources and add the vSphere Networks table backed by vCenter network topology. Align resource presentation coalescing so state and resource APIs share the same host contract.
2026-05-22 20:26:56 +01:00

14704 lines
527 KiB
Go

package api
import (
"bytes"
"context"
"crypto/ed25519"
"crypto/tls"
"encoding/base64"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"os"
"path/filepath"
"reflect"
"regexp"
"sort"
"strings"
"testing"
"time"
"github.com/golang-jwt/jwt/v5"
"github.com/gorilla/websocket"
"github.com/rcourtman/pulse-go-rewrite/internal/actionplanner"
"github.com/rcourtman/pulse-go-rewrite/internal/agentexec"
"github.com/rcourtman/pulse-go-rewrite/internal/ai"
"github.com/rcourtman/pulse-go-rewrite/internal/ai/approval"
"github.com/rcourtman/pulse-go-rewrite/internal/ai/chat"
"github.com/rcourtman/pulse-go-rewrite/internal/ai/correlation"
"github.com/rcourtman/pulse-go-rewrite/internal/ai/memory"
"github.com/rcourtman/pulse-go-rewrite/internal/ai/providers"
"github.com/rcourtman/pulse-go-rewrite/internal/ai/unified"
"github.com/rcourtman/pulse-go-rewrite/internal/alerts"
"github.com/rcourtman/pulse-go-rewrite/internal/config"
"github.com/rcourtman/pulse-go-rewrite/internal/license/entitlements"
"github.com/rcourtman/pulse-go-rewrite/internal/mock"
"github.com/rcourtman/pulse-go-rewrite/internal/models"
"github.com/rcourtman/pulse-go-rewrite/internal/monitoring"
"github.com/rcourtman/pulse-go-rewrite/internal/recovery"
"github.com/rcourtman/pulse-go-rewrite/internal/relay"
"github.com/rcourtman/pulse-go-rewrite/internal/truenas"
"github.com/rcourtman/pulse-go-rewrite/internal/unifiedresources"
"github.com/rcourtman/pulse-go-rewrite/internal/updates"
"github.com/rcourtman/pulse-go-rewrite/internal/vmware"
agentshost "github.com/rcourtman/pulse-go-rewrite/pkg/agents/host"
"github.com/rcourtman/pulse-go-rewrite/pkg/aicontracts"
authpkg "github.com/rcourtman/pulse-go-rewrite/pkg/auth"
"github.com/rcourtman/pulse-go-rewrite/pkg/cloudauth"
pkglicensing "github.com/rcourtman/pulse-go-rewrite/pkg/licensing"
licensetestsupport "github.com/rcourtman/pulse-go-rewrite/pkg/licensing/testsupport"
"github.com/rcourtman/pulse-go-rewrite/pkg/metrics"
"github.com/rcourtman/pulse-go-rewrite/pkg/reporting"
"github.com/rs/zerolog"
)
type resourceContractSnapshot struct {
ID string
Name string
Type string
}
func TestContract_PMGInstancesEndpointUsesUnifiedReadStatePayload(t *testing.T) {
now := time.Now().UTC()
source := models.PMGInstance{
ID: "pmg-contract-1",
Name: "gateway-contract",
Host: "https://pmg-contract.example.com:8006",
GuestURL: "https://pmg-contract.example.com/quarantine",
Status: "online",
Version: "8.2",
Nodes: []models.PMGNodeStatus{{
Name: "pmg-node-contract",
Status: "online",
QueueStatus: &models.PMGQueueStatus{
Incoming: 4,
Total: 10,
OldestAge: 600,
UpdatedAt: now,
},
}},
MailStats: &models.PMGMailStats{
CountTotal: 1000,
JunkIn: 10,
PregreetRejects: 13,
UpdatedAt: now,
},
MailCount: []models.PMGMailCountPoint{{Timestamp: now, Count: 1000, Timeframe: "hour", Index: 1}},
DomainStats: []models.PMGDomainStat{{Domain: "example.com", MailCount: 100, Bytes: 2048}},
DomainStatsAsOf: now,
ConnectionHealth: "connected",
LastSeen: now,
LastUpdated: now,
}
registry := unifiedresources.NewRegistry(nil)
registry.IngestSnapshot(models.StateSnapshot{PMGInstances: []models.PMGInstance{source}})
monitor := &monitoring.Monitor{}
setUnexportedField(t, monitor, "resourceStore", unifiedresources.NewMonitorAdapter(registry))
router := &Router{monitor: monitor}
req := httptest.NewRequest(http.MethodGet, "/api/pmg/instances?id=pmg-contract-1", nil)
rec := httptest.NewRecorder()
router.handleListPMGInstances(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status = %d, body=%s", rec.Code, rec.Body.String())
}
var resp PMGInstancesResponse
if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil {
t.Fatalf("decode PMG contract response: %v", err)
}
if resp.Meta.Total != 1 || len(resp.Data) != 1 {
t.Fatalf("expected one PMG instance, got total=%d len=%d", resp.Meta.Total, len(resp.Data))
}
got := resp.Data[0]
if got.ID != source.ID || got.Host != source.Host || got.GuestURL != source.GuestURL {
t.Fatalf("PMG identity and URL contract not preserved: %+v", got)
}
if len(got.Nodes) != 1 || got.Nodes[0].QueueStatus == nil || got.Nodes[0].QueueStatus.OldestAge != 600 {
t.Fatalf("PMG queue contract not preserved: %+v", got.Nodes)
}
if got.MailStats == nil || got.MailStats.CountTotal != 1000 || got.MailStats.JunkIn != 10 || got.MailStats.PregreetRejects != 13 {
t.Fatalf("PMG mail stats contract not preserved: %+v", got.MailStats)
}
if len(got.MailCount) != 1 || got.MailCount[0].Index != 1 || len(got.DomainStats) != 1 || got.DomainStats[0].Bytes != 2048 {
t.Fatalf("PMG historical/domain contract not preserved: mail=%+v domains=%+v", got.MailCount, got.DomainStats)
}
}
func TestContract_MockAvailabilityTargetsUseSavedTargetPayloads(t *testing.T) {
previousMock := mock.IsMockEnabled()
if err := mock.SetEnabled(true); err != nil {
t.Fatalf("enable mock mode: %v", err)
}
t.Cleanup(func() { _ = mock.SetEnabled(previousMock) })
targets := mockAvailabilityTargetResponses()
if len(targets) < 4 {
t.Fatalf("expected mock availability target payloads, got %d", len(targets))
}
seen := map[string]availabilityTargetResponse{}
for _, target := range targets {
if target.ID == "" {
t.Fatal("mock availability target payload must keep a saved target id")
}
if target.Status == nil {
t.Fatalf("mock availability target %q must include probe status", target.ID)
}
if target.Status.TargetID != target.ID {
t.Fatalf("mock availability target %q status target id drifted to %q", target.ID, target.Status.TargetID)
}
seen[target.ID] = target
}
mqtt, ok := seen["mock-availability-mqtt-meter"]
if !ok {
t.Fatal("mock MQTT availability target missing from API payload contract")
}
if mqtt.Protocol != config.AvailabilityProbeTCP || mqtt.Port != 1883 {
t.Fatalf("mock MQTT availability target must remain a saved TCP:1883 target, got protocol=%q port=%v", mqtt.Protocol, mqtt.Port)
}
response, ok := mockAvailabilityTestResponse("mock-availability-mqtt-meter")
if !ok {
t.Fatal("saved mock availability test response missing")
}
if !response.Success || response.LatencyMillis <= 0 || response.Error != "" {
t.Fatalf("mock MQTT saved-test response must be successful with latency and no error: %+v", response)
}
}
func TestContract_MockDiscoveryEndpointsUseCanonicalPayloads(t *testing.T) {
previousMock := mock.IsMockEnabled()
if err := mock.SetEnabled(true); err != nil {
t.Fatalf("enable mock mode: %v", err)
}
t.Cleanup(func() { _ = mock.SetEnabled(previousMock) })
handlers := NewDiscoveryHandlers(nil, nil)
listReq := httptest.NewRequest(http.MethodGet, "/api/discovery", nil)
listRec := httptest.NewRecorder()
handlers.HandleListDiscoveries(listRec, listReq)
if listRec.Code != http.StatusOK {
t.Fatalf("/api/discovery status = %d, body=%s", listRec.Code, listRec.Body.String())
}
var listBody struct {
Discoveries []map[string]any `json:"discoveries"`
Total int `json:"total"`
}
if err := json.NewDecoder(listRec.Body).Decode(&listBody); err != nil {
t.Fatalf("decode discovery list: %v", err)
}
if listBody.Total == 0 || len(listBody.Discoveries) == 0 {
t.Fatal("expected mock discovery list to expose canonical discovery summaries")
}
var dockerDetail map[string]any
for _, summary := range listBody.Discoveries {
if summary["resource_type"] != "docker" {
continue
}
resourceType, _ := summary["resource_type"].(string)
targetID, _ := summary["target_id"].(string)
resourceID, _ := summary["resource_id"].(string)
if resourceType == "" || targetID == "" || resourceID == "" {
t.Fatalf("mock discovery summary lost canonical identity: %#v", summary)
}
detailReq := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/api/discovery/%s/%s/%s", resourceType, targetID, resourceID), nil)
detailRec := httptest.NewRecorder()
handlers.HandleGetDiscovery(detailRec, detailReq)
if detailRec.Code != http.StatusOK {
t.Fatalf("discovery detail status = %d, body=%s", detailRec.Code, detailRec.Body.String())
}
var detail map[string]any
if err := json.NewDecoder(detailRec.Body).Decode(&detail); err != nil {
t.Fatalf("decode discovery detail: %v", err)
}
if !discoveryContractValueEmpty(detail["suggested_url"]) {
dockerDetail = detail
break
}
}
if dockerDetail == nil {
t.Fatalf("expected mock discovery summaries to include Docker workload URL context: %#v", listBody.Discoveries)
}
for _, key := range []string{"service_name", "service_version", "config_paths", "ports", "suggested_url", "cli_access_version"} {
value, ok := dockerDetail[key]
if !ok || discoveryContractValueEmpty(value) {
t.Fatalf("mock discovery detail missing %s: %#v", key, dockerDetail)
}
}
if _, ok := dockerDetail["raw_command_output"]; ok {
t.Fatalf("mock discovery detail exposed raw command output: %#v", dockerDetail)
}
}
func discoveryContractValueEmpty(value any) bool {
switch typed := value.(type) {
case nil:
return true
case string:
return strings.TrimSpace(typed) == ""
case []any:
return len(typed) == 0
case float64:
return typed == 0
default:
return false
}
}
func TestPatrolRemediationCommercialCopyUsesSafeRemediationWording(t *testing.T) {
files := []string{"ai_handlers.go", "router_routes_ai_relay.go"}
for _, file := range files {
source, err := os.ReadFile(file)
if err != nil {
t.Fatalf("read %s: %v", file, err)
}
text := string(source)
if strings.Contains(text, "Pulse Patrol Auto-Fix requires Pulse Pro") ||
strings.Contains(text, "Auto-Fix requires Pulse Pro") {
t.Fatalf("%s must not expose legacy Auto-Fix license copy", file)
}
}
}
func TestContract_PatrolAutonomyCommunityMonitorUpdatePayload(t *testing.T) {
rawToken := "patrol-autonomy-contract-token-123.12345678"
record := newTokenRecord(t, rawToken, []string{config.ScopeSettingsWrite}, nil)
cfg := newTestConfigWithTokens(t, record)
router := NewRouter(cfg, nil, nil, nil, nil, "1.0.0")
body := `{"autonomy_level":"monitor","full_mode_unlocked":true,"investigation_budget":2,"investigation_timeout_sec":30}`
req := httptest.NewRequest(http.MethodPut, "/api/ai/patrol/autonomy", strings.NewReader(body))
req.Header.Set("X-API-Token", rawToken)
rec := httptest.NewRecorder()
router.Handler().ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("expected monitor autonomy update to be allowed, got %d: %s", rec.Code, rec.Body.String())
}
var resp struct {
Success bool `json:"success"`
Settings PatrolAutonomyResponse `json:"settings"`
}
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
t.Fatalf("decode monitor update response: %v", err)
}
if !resp.Success {
t.Fatalf("expected success response, got %+v", resp)
}
if resp.Settings.AutonomyLevel != config.PatrolAutonomyMonitor ||
resp.Settings.FullModeUnlocked ||
resp.Settings.InvestigationBudget != 5 ||
resp.Settings.InvestigationTimeoutSec != 60 {
t.Fatalf("unexpected monitor update settings: %+v", resp.Settings)
}
premiumBody := `{"autonomy_level":"approval","investigation_budget":10,"investigation_timeout_sec":120}`
premiumReq := httptest.NewRequest(http.MethodPut, "/api/ai/patrol/autonomy", strings.NewReader(premiumBody))
premiumReq.Header.Set("X-API-Token", rawToken)
premiumRec := httptest.NewRecorder()
router.Handler().ServeHTTP(premiumRec, premiumReq)
if premiumRec.Code != http.StatusPaymentRequired {
t.Fatalf("expected premium autonomy to require license, got %d: %s", premiumRec.Code, premiumRec.Body.String())
}
if !strings.Contains(premiumRec.Body.String(), "limited to Monitor") {
t.Fatalf("expected premium autonomy payload to explain Community monitor limit, got %s", premiumRec.Body.String())
}
}
func TestContract_AssistantFindingContextUsesModelOnlyHandoff(t *testing.T) {
handlerSource, err := os.ReadFile(filepath.Clean("ai_handler.go"))
if err != nil {
t.Fatalf("read ai_handler.go: %v", err)
}
settingsHandlerSource, err := os.ReadFile(filepath.Clean("ai_handlers.go"))
if err != nil {
t.Fatalf("read ai_handlers.go: %v", err)
}
chatServiceSource, err := os.ReadFile(filepath.Clean("../ai/chat/service.go"))
if err != nil {
t.Fatalf("read chat service: %v", err)
}
chatSessionSource, err := os.ReadFile(filepath.Clean("../ai/chat/session.go"))
if err != nil {
t.Fatalf("read chat session store: %v", err)
}
chatTypesSource, err := os.ReadFile(filepath.Clean("../ai/chat/types.go"))
if err != nil {
t.Fatalf("read chat types: %v", err)
}
patrolHandoffSource, err := os.ReadFile(filepath.Clean("../ai/patrol_assistant_handoff.go"))
if err != nil {
t.Fatalf("read Patrol Assistant handoff model: %v", err)
}
toolsQuerySource, err := os.ReadFile(filepath.Clean("../ai/tools/tools_query.go"))
if err != nil {
t.Fatalf("read AI tools query runtime: %v", err)
}
handlerText := string(handlerSource)
for _, required := range []string{
`svc.GetModelHandoffFindingID(ctx, req.SessionID)`,
`svc.GetModelHandoffMetadata(ctx, req.SessionID)`,
`svc.ClearModelHandoffContext(ctx, sessionID)`,
`HandoffActions []chat.HandoffAction ` + "`json:\"handoff_actions,omitempty\"`",
`HandoffMetadata chat.HandoffMetadata ` + "`json:\"handoff_metadata,omitempty\"`",
"handoffActions := normalizeChatRequestHandoffActions(req.HandoffActions)",
"handoffMetadata := chat.NormalizeHandoffMetadata(req.HandoffMetadata)",
"h.getPatrolRunForHandoff(ctx, handoffMetadata.RunID)",
"airuntime.BuildPatrolRunAssistantHandoff(run)",
"chatRequestHandoffActionLimit",
"requestHandoffContext := handoffContext",
"requestHandoffResources := handoffResources",
"requestHandoffActions := handoffActions",
"handoffActions = mergeUnifiedFindingRequestHandoffActions(f, buildUnifiedFindingHandoffActions(f, orgID), requestHandoffActions)",
"handoffContext = mergeUnifiedFindingRequestHandoffContext(buildUnifiedFindingChatContext(f, store, handoffActions), requestHandoffContext, f.ID)",
"handoffResources = mergeUnifiedFindingRequestHandoffResources(f, buildUnifiedFindingHandoffResources(f, store), requestHandoffResources)",
"safePatrolFindingRequestHandoffContext",
"Product Handoff Boundary",
"livePatrolApprovalForFinding(f.ID, orgID)",
"chat.HydrateHandoffActionFromApproval(&action, liveApproval)",
"ApprovalRequestedAt",
"ApprovalExpiresAt",
"func chatAutonomousModeForFindingHandoff(requested *bool, findingID, handoffContext string, handoffResources []chat.HandoffResource, handoffActions []chat.HandoffAction, handoffMetadata chat.HandoffMetadata) *bool",
`if strings.TrimSpace(findingID) != ""`,
"chatApprovalRequiredAutonomousMode()",
"AutonomousMode: chatAutonomousModeForFindingHandoff(req.AutonomousMode, findingID, handoffContext, handoffResources, handoffActions, handoffMetadata)",
`appendChatContextLine(&b, "Finding Status", unifiedFindingChatStatus(f, time.Now()))`,
`appendChatContextLine(&b, "Finding Detected At", f.DetectedAt.Format(time.RFC3339))`,
`appendChatContextLine(&b, "Finding Last Seen At", f.LastSeenAt.Format(time.RFC3339))`,
`appendChatContextLine(&b, "Finding Resolved At", f.ResolvedAt.Format(time.RFC3339))`,
`appendChatContextLine(&b, "Finding Snoozed Until", f.SnoozedUntil.Format(time.RFC3339))`,
`appendChatContextLine(&b, "Finding Dismissed Reason", f.DismissedReason)`,
`appendChatContextLine(&b, "Finding Suppressed", "true")`,
`appendChatContextLine(&b, "Finding Times Raised", strconv.Itoa(f.TimesRaised))`,
`appendChatContextLine(&b, "AI Context", f.AIContext)`,
`appendChatContextLine(&b, "Root Cause ID", f.RootCauseID)`,
`appendStringListChatContext(&b, "Correlated Finding", f.CorrelatedIDs)`,
"appendUnifiedFindingRelatedContext(&b, f, lookup)",
"[Related Finding Context]",
"Related Finding Boundary",
"formatUnifiedFindingBriefingRecency(f)",
"latest lifecycle",
`appendChatContextLine(&b, "Remediation ID", f.RemediationID)`,
`appendChatContextLine(&b, "Last Investigated At", f.LastInvestigatedAt.Format(time.RFC3339))`,
`appendChatContextLine(&b, "Investigation Attempts", strconv.Itoa(f.InvestigationAttempts))`,
`appendChatContextLine(&b, "Loop State", f.LoopState)`,
`appendChatContextLine(&b, "Regression Count", strconv.Itoa(f.RegressionCount))`,
`appendChatContextLine(&b, "Last Regression At", f.LastRegressionAt.Format(time.RFC3339))`,
"appendUnifiedFindingLifecycleEventContext(&b, f.Lifecycle)",
"formatUnifiedFindingLatestLifecycleEvent(f.Lifecycle)",
"[Finding Lifecycle Context]",
"Lifecycle Boundary",
"appendUnifiedFindingModelBriefingContext(&b, f, handoffActions)",
"[Finding Briefing]",
`appendChatContextLine(b, "Recency", formatUnifiedFindingBriefingRecencyFacts(f))`,
"formatUnifiedFindingBriefingRecencyFacts(f)",
`appendChatContextLine(b, "Evidence Snapshot", evidence)`,
`appendChatContextLine(b, "Verification", verification)`,
"formatInvestigationRecordEvidenceBriefing",
"formatBriefingStringList",
`appendChatContextLine(b, "Governed Action Context", actionContext)`,
"unifiedFindingBriefingActionContext(f, handoffActions)",
"unifiedFindingPrimaryHandoffAction(f, handoffActions)",
"Latest Lifecycle Event",
"Model Boundary",
`return "suppressed"`,
"Prompt:",
"HandoffContext:",
"HandoffResources: handoffResources",
"HandoffActions: handoffActions",
"HandoffMetadata: handoffMetadata",
} {
if !strings.Contains(handlerText, required) {
t.Fatalf("ai_handler.go must preserve model-only finding handoff contract: missing %q", required)
}
}
if strings.Contains(handlerText, "prompt = findingCtx +") {
t.Fatal("ai_handler.go must not prepend finding context into the persisted prompt")
}
for _, forbidden := range []string{
"Recommended Next Step",
"unifiedFindingBriefingNextStep",
} {
if strings.Contains(handlerText, forbidden) {
t.Fatalf("ai_handler.go must not synthesize product-authored handoff recommendations: found %q", forbidden)
}
}
settingsHandlerText := string(settingsHandlerSource)
for _, required := range []string{
"actionAuditStore: chatService.GetActionAuditStore()",
"tools.AttachApprovalActionPlan(req, time.Now().UTC())",
"tools.RecordPendingApprovalAction(a.actionAuditStore, req)",
} {
if !strings.Contains(settingsHandlerText, required) {
t.Fatalf("ai_handlers.go must preserve Patrol queued-fix action-audit seeding: missing %q", required)
}
}
patrolHandoffText := string(patrolHandoffSource)
for _, required := range []string{
"func BuildPatrolRunAssistantHandoff(run PatrolRunRecord) PatrolRunAssistantHandoff",
"[Patrol Run Context]",
"Source: Pulse Patrol run history",
"patrolRunAssistantHandoffResources",
"sanitizePatrolRunAnalysis",
"Model Boundary",
} {
if !strings.Contains(patrolHandoffText, required) {
t.Fatalf("Patrol runtime must own safe Assistant run handoff context: missing %q", required)
}
}
chatServiceText := string(chatServiceSource)
for _, required := range []string{
"handoffContext := strings.TrimSpace(req.HandoffContext)",
"handoffFindingID := strings.TrimSpace(req.FindingID)",
"handoffMetadata := NormalizeHandoffMetadata(req.HandoffMetadata)",
"hasRequestHandoffEnvelope",
"sessions.SetModelHandoffEnvelope(session.ID, handoffFindingID, handoffContext, handoffResources, handoffActions, handoffMetadata)",
"ClearModelHandoffContext",
"handoffResources := normalizeHandoffResources(req.HandoffResources)",
"GetModelHandoffFindingID",
"GetModelHandoffMetadata",
"sessions.GetModelHandoffContext(session.ID)",
"sessions.GetModelHandoffResources(session.ID)",
"sessions.SetModelHandoffActions(session.ID, handoffActions)",
"sessions.GetModelHandoffActions(session.ID)",
"GetActionAuditStore",
"refreshHandoffActionStatus(handoffActions, s.orgID, s.actionAuditStore)",
"HydrateHandoffActionFromApproval(&actions[idx], req)",
"func HydrateHandoffActionFromApproval(action *HandoffAction, req *approval.ApprovalRequest)",
"approval.RequesterForRequest(req)",
"action.ActionRequestedBy = requestedBy",
"mergeHandoffResourcePolicyContext(handoffContext, handoffResources, handoffResourceProvider)",
"mergeHandoffResourceStateContext(handoffContext, handoffResources, handoffResourceProvider)",
"mergeHandoffResourceRelationshipContext(handoffContext, handoffResources, handoffResourceProvider)",
"mergeHandoffResourceTimelineContext(handoffContext, handoffResources, handoffResourceProvider, s.actionAuditStore, time.Now())",
"handoffContext = mergeHandoffActionContext(handoffContext, handoffActions)",
"sanitizeHandoffContextForResourcePolicy(handoffContext, handoffResources, handoffResourceProvider)",
"Action Reference",
"Action State",
"Action Plan Expires At",
"Action Dry Run Summary",
"Action Result",
"s.hydrateHandoffResources(session.ID, handoffResources, sessions, unifiedResourceProvider)",
"injectHandoffContextIntoLatestUserMessage(messages, handoffContext)",
"Resource Policy Context",
"Policy Boundary",
"Resource State Context",
"labelPrefix+\" Status\"",
"labelPrefix+\" Capabilities\"",
"Resource State Boundary",
"Resource Relationship Context",
"Relationship Boundary",
"Timeline Boundary",
"Approval Status",
"Action Boundary",
"User message: ",
} {
if !strings.Contains(chatServiceText, required) {
t.Fatalf("chat service must inject handoff context into model-only turn: missing %q", required)
}
}
chatSessionText := string(chatSessionSource)
for _, required := range []string{
"ModelContext *sessionModelContext",
"SetModelHandoffFindingID",
"SetModelHandoffEnvelope",
"GetModelHandoffFindingID",
"GetModelHandoffMetadata",
"ClearModelHandoffContext",
"HandoffFindingID string",
"SetModelHandoffContext",
"GetModelHandoffContext",
"SetModelHandoffResources",
"GetModelHandoffResources",
"HandoffResources []HandoffResource",
"SetModelHandoffActions",
"GetModelHandoffActions",
"HandoffActions []HandoffAction",
"SetModelHandoffMetadata",
"HandoffMetadata HandoffMetadata",
} {
if !strings.Contains(chatSessionText, required) {
t.Fatalf("chat session store must persist model-only handoff metadata outside messages: missing %q", required)
}
}
chatTypesText := string(chatTypesSource)
for _, required := range []string{
"type HandoffResource struct",
"HandoffResources []HandoffResource",
"type HandoffAction struct",
"HandoffActions []HandoffAction",
"type HandoffMetadata struct",
"HandoffMetadata HandoffMetadata",
"ApprovalStatus",
"ActionID",
"ActionState",
"ActionRequestedBy",
"ActionPlanExpiresAt",
"ActionDryRunSummary",
"ActionResult",
} {
if !strings.Contains(chatTypesText, required) {
t.Fatalf("chat request must carry structured handoff resource scope outside messages: missing %q", required)
}
}
toolsQueryText := string(toolsQuerySource)
if !strings.Contains(toolsQueryText, "func CanonicalHandoffResourceRegistration(provider UnifiedResourceProvider") {
t.Fatal("AI tools runtime must own canonical handoff resource registration")
}
if !strings.Contains(toolsQueryText, "func CanonicalHandoffUnifiedResource(provider UnifiedResourceProvider") {
t.Fatal("AI tools runtime must own canonical handoff unified resource resolution")
}
}
func TestContract_InvestigateAlertIsApprovalBound(t *testing.T) {
source, err := os.ReadFile(filepath.Clean("ai_handlers.go"))
if err != nil {
t.Fatalf("read ai_handlers.go: %v", err)
}
text := string(source)
if !strings.Contains(text, "autonomousMode := false") ||
!strings.Contains(text, "AutonomousMode: &autonomousMode") ||
!strings.Contains(text, "RequireCommandApproval: true") {
t.Fatal("investigate-alert API handoff must force request-scoped approval and command approval")
}
}
func TestContract_ProxmoxSetupScriptUsesPrivilegeSeparatedTokenACLs(t *testing.T) {
storagePerms := `
pveum aclmod /storage -user pulse-monitor@pve -role PVEDatastoreAdmin
if [ "$TOKEN_CREATED" = true ]; then
pveum aclmod /storage -token "$PULSE_TOKEN_ID" -role PVEDatastoreAdmin
fi`
pveArtifact := buildSetupScriptInstallArtifact(
"https://pulse.example",
"pve",
"https://pve.example:8006",
"https://pulse.example",
true,
"setup-token-123",
0,
)
pveScript := renderSetupScript("pve", setupScriptRenderContext{
ServerName: "pve-example",
PulseURL: "https://pulse.example",
ServerHost: "https://pve.example:8006",
SetupToken: "setup-token-123",
TokenName: "pulse-example",
TokenMatchPrefix: "pulse-example",
StoragePerms: storagePerms,
SensorsPublicKey: "ssh-ed25519 AAAATEST pulse@test",
Artifact: pveArtifact,
})
for _, required := range []string{
`TOKEN_OUTPUT=$(pveum user token add pulse-monitor@pve "$TOKEN_NAME" --privsep 1 2>&1)`,
`pveum aclmod / -user pulse-monitor@pve -role PVEAuditor`,
`pveum aclmod / -token "$PULSE_TOKEN_ID" -role PVEAuditor`,
`pveum aclmod / -user pulse-monitor@pve -role PulseMonitor`,
`pveum aclmod / -token "$PULSE_TOKEN_ID" -role PulseMonitor`,
`pveum aclmod /storage -user pulse-monitor@pve -role PVEDatastoreAdmin`,
`pveum aclmod /storage -token "$PULSE_TOKEN_ID" -role PVEDatastoreAdmin`,
} {
if !strings.Contains(pveScript, required) {
t.Fatalf("PVE setup script must contain %q", required)
}
}
if strings.Contains(pveScript, "--privsep 0") {
t.Fatal("PVE setup script must not create full-privilege API tokens")
}
pbsArtifact := buildSetupScriptInstallArtifact(
"https://pulse.example",
"pbs",
"https://pbs.example:8007",
"https://pulse.example",
false,
"setup-token-123",
0,
)
pbsScript := renderSetupScript("pbs", setupScriptRenderContext{
ServerName: "pbs-example",
PulseURL: "https://pulse.example",
ServerHost: "https://pbs.example:8007",
SetupToken: "setup-token-123",
TokenName: "pulse-example",
TokenMatchPrefix: "pulse-example",
Artifact: pbsArtifact,
})
for _, required := range []string{
`proxmox-backup-manager acl update / Audit --auth-id pulse-monitor@pbs`,
`proxmox-backup-manager acl update / Audit --auth-id "$PULSE_TOKEN_ID"`,
} {
if !strings.Contains(pbsScript, required) {
t.Fatalf("PBS setup script must contain %q", required)
}
}
for _, forbidden := range []string{
`Admin --auth-id pulse-monitor@pbs`,
`Admin --auth-id "$PULSE_TOKEN_ID"`,
} {
if strings.Contains(pbsScript, forbidden) {
t.Fatalf("PBS setup script must not grant admin ACL %q", forbidden)
}
}
}
func TestContract_HostedMagicLinkStablePrincipalProof(t *testing.T) {
source, err := os.ReadFile(filepath.Clean("magic_link_handlers.go"))
if err != nil {
t.Fatalf("read magic_link_handlers.go: %v", err)
}
text := string(source)
for _, required := range []string{
"resolveMagicLinkPrincipal",
"CreateSession(sessionToken, sessionDuration, userAgent, clientIP, principal.UserID)",
"TrackUserSession(principal.UserID, sessionToken)",
} {
if !strings.Contains(text, required) {
t.Fatalf("magic_link_handlers.go must contain %q", required)
}
}
for _, forbidden := range []string{
"CreateSession(sessionToken, sessionDuration, userAgent, clientIP, token.Email)",
"TrackUserSession(token.Email, sessionToken)",
} {
if strings.Contains(text, forbidden) {
t.Fatalf("magic_link_handlers.go must not contain legacy email-principal pattern %q", forbidden)
}
}
invariantDoc, err := os.ReadFile(filepath.Clean("../../docs/release-control/v6/internal/IDENTITY_INVARIANTS.md"))
if err != nil {
t.Fatalf("read identity invariant contract: %v", err)
}
if !strings.Contains(string(invariantDoc), "Email is contact metadata") {
t.Fatal("identity invariant contract must define email as contact metadata")
}
modelSource, err := os.ReadFile(filepath.Clean("../models/organization.go"))
if err != nil {
t.Fatalf("read organization model: %v", err)
}
if !strings.Contains(string(modelSource), "ResolvePrincipalByEmail") {
t.Fatal("organization model must expose email-to-stable-principal resolution")
}
if strings.Contains(string(modelSource), "userID = email") {
t.Fatal("organization model must not synthesize magic-link principals from email")
}
}
func TestContract_CheckoutMagicLinkDeliveryRequiresStoredPrincipalProof(t *testing.T) {
source, err := os.ReadFile(filepath.Clean("payments_webhook_handlers.go"))
if err != nil {
t.Fatalf("read payments_webhook_handlers.go: %v", err)
}
text := string(source)
for _, required := range []string{
"ResolvePrincipalByEmail(email)",
"models.IsValidOrganizationRole(role)",
"GenerateToken(email, orgID)",
"SendMagicLink(email, orgID, token, baseURL)",
} {
if !strings.Contains(text, required) {
t.Fatalf("payments_webhook_handlers.go must contain %q", required)
}
}
for _, forbidden := range []string{
"strings.EqualFold(org.OwnerEmail, email)",
"strings.EqualFold(org.OwnerUserID, email)",
"strings.EqualFold(m.Email, email)",
"strings.EqualFold(m.UserID, email)",
} {
if strings.Contains(text, forbidden) {
t.Fatalf("payments_webhook_handlers.go must not contain legacy contact-email membership pattern %q", forbidden)
}
}
identityDoc, err := os.ReadFile(filepath.Clean("../../docs/release-control/v6/internal/IDENTITY_INVARIANTS.md"))
if err != nil {
t.Fatalf("read identity invariant contract: %v", err)
}
if !strings.Contains(string(identityDoc), "Post-checkout magic-link delivery") {
t.Fatal("identity invariant contract must cover checkout magic-link delivery")
}
}
func TestContract_OrganizationRuntimeAccessUsesStrictUserIDProof(t *testing.T) {
modelSource, err := os.ReadFile(filepath.Clean("../models/organization.go"))
if err != nil {
t.Fatalf("read organization model: %v", err)
}
modelText := string(modelSource)
for _, required := range []string{
"HasMemberUserID",
"GetMemberRoleByUserID",
"IsOwnerUserID",
"CanUserIDAccess",
"CanUserIDManage",
} {
if !strings.Contains(modelText, required) {
t.Fatalf("organization model must contain strict user ID helper %q", required)
}
}
requiredByFile := map[string][]string{
"authorization.go": {
"org.CanUserIDAccess(userID)",
},
"org_handlers.go": {
"org.CanUserIDAccess(username)",
"org.CanUserIDManage(username)",
"org.IsOwnerUserID(username)",
"org.GetMemberRoleByUserID(username)",
},
"security_setup_fix.go": {
"org.CanUserIDManage(sessionUser)",
},
"cloud_org_admin_auth.go": {
"org.IsOwnerUserID(userID)",
},
}
for file, required := range requiredByFile {
source, err := os.ReadFile(filepath.Clean(file))
if err != nil {
t.Fatalf("read %s: %v", file, err)
}
text := string(source)
for _, needle := range required {
if !strings.Contains(text, needle) {
t.Fatalf("%s must contain strict runtime authorization proof %q", file, needle)
}
}
for _, forbidden := range []string{
".CanUserAccess(username)",
".CanUserAccess(userID)",
".CanUserManage(username)",
".CanUserManage(sessionUser)",
".IsOwner(username)",
".GetMemberRole(username)",
} {
if strings.Contains(text, forbidden) {
t.Fatalf("%s must not use legacy email-aware runtime accessor %q", file, forbidden)
}
}
}
identityDoc, err := os.ReadFile(filepath.Clean("../../docs/release-control/v6/internal/IDENTITY_INVARIANTS.md"))
if err != nil {
t.Fatalf("read identity invariant contract: %v", err)
}
identityText := string(identityDoc)
if !strings.Contains(identityText, "strict user-ID") || !strings.Contains(identityText, "OwnerUserID") {
t.Fatal("identity invariant contract must require strict user-ID membership checks for live authorization")
}
}
func TestContract_HostedHandoffRequiresStableSubjectProof(t *testing.T) {
directSource, err := os.ReadFile(filepath.Clean("cloud_handoff.go"))
if err != nil {
t.Fatalf("read cloud_handoff.go: %v", err)
}
exchangeSource, err := os.ReadFile(filepath.Clean("cloud_handoff_handlers.go"))
if err != nil {
t.Fatalf("read cloud_handoff_handlers.go: %v", err)
}
for file, source := range map[string]string{
"cloud_handoff.go": string(directSource),
"cloud_handoff_handlers.go": string(exchangeSource),
} {
for _, required := range []string{
"isEmailShapedHandoffUserID(userID)",
"CreateSession(sessionToken, sessionDuration, userAgent, clientIP, authz.UserID)",
"TrackUserSession(authz.UserID, sessionToken)",
} {
if !strings.Contains(source, required) {
t.Fatalf("%s must contain %q", file, required)
}
}
for _, forbidden := range []string{
"userID = email",
"email = normalizeHandoffEmail(userID)",
"CreateSession(sessionToken, sessionDuration, userAgent, clientIP, claims.Email)",
"TrackUserSession(claims.Email, sessionToken)",
} {
if strings.Contains(source, forbidden) {
t.Fatalf("%s must not contain legacy email-principal pattern %q", file, forbidden)
}
}
}
}
func TestContract_SSOStablePrincipalProof(t *testing.T) {
ssoIdentity, err := os.ReadFile(filepath.Clean("auth_principal_identity.go"))
if err != nil {
t.Fatalf("read auth_principal_identity.go: %v", err)
}
oidcSource, err := os.ReadFile(filepath.Clean("oidc_handlers.go"))
if err != nil {
t.Fatalf("read oidc_handlers.go: %v", err)
}
samlSource, err := os.ReadFile(filepath.Clean("saml_handlers.go"))
if err != nil {
t.Fatalf("read saml_handlers.go: %v", err)
}
for file, text := range map[string]string{
"auth_principal_identity.go": string(ssoIdentity),
"oidc_handlers.go": string(oidcSource),
"saml_handlers.go": string(samlSource),
} {
for _, required := range []string{
"stableSSOPrincipal",
"applySSORoleAssignments",
} {
if !strings.Contains(text, required) {
t.Fatalf("%s must contain %q", file, required)
}
}
}
for _, forbidden := range []string{
"establishOIDCSession(w, req, username, oidcTokens)",
"UpdateUserRoles(username, rolesToAssign)",
} {
if strings.Contains(string(oidcSource), forbidden) {
t.Fatalf("oidc_handlers.go must not contain legacy SSO principal pattern %q", forbidden)
}
}
for _, forbidden := range []string{
"establishSAMLSession(w, req, username, samlSession)",
"UpdateUserRoles(result.Username, rolesToAssign)",
} {
if strings.Contains(string(samlSource), forbidden) {
t.Fatalf("saml_handlers.go must not contain legacy SSO principal pattern %q", forbidden)
}
}
}
func TestContract_APITokenOwnerBindingCoversSharedMintingPaths(t *testing.T) {
files := map[string][]string{
"security_tokens.go": {
"setAPITokenOwnerUserID",
"apiTokenOwnerUserIDForRequest",
"reserved token metadata key",
},
"agent_install_command_shared.go": {
"OwnerUserID string",
"setAPITokenOwnerUserID(record, opts.OwnerUserID)",
"mergeAPITokenMetadata(record, opts.Metadata)",
},
"deploy_handlers.go": {
"setAPITokenOwnerUserID(record, ownerUserID)",
"setAPITokenOwnerUserID(runtimeRecord, apiTokenOwnerUserID(*bootstrapToken))",
"apiTokenOwnerUserIDForRequest(h.config, r)",
},
"router.go": {
"setAPITokenOwnerUserID(record, apiTokenOwnerUserIDForRequest(r.config, req))",
},
"security_setup_fix.go": {
"setAPITokenOwnerUserID(tokenRecord, setupRequest.Username)",
"setAPITokenOwnerUserID(tokenRecord, apiTokenOwnerUserIDForRequest(r.config, rq))",
},
}
for file, needles := range files {
source, err := os.ReadFile(filepath.Clean(file))
if err != nil {
t.Fatalf("read %s: %v", file, err)
}
text := string(source)
for _, needle := range needles {
if !strings.Contains(text, needle) {
t.Fatalf("%s must contain %q", file, needle)
}
}
}
}
func TestContractAISettingsClampsPaidRuntimeControlsToEntitlements(t *testing.T) {
t.Parallel()
tmp := t.TempDir()
cfg := &config.Config{DataPath: tmp}
persistence := config.NewConfigPersistence(tmp)
aiCfg := config.NewDefaultAIConfig()
aiCfg.Enabled = true
aiCfg.Model = "ollama:llama3"
aiCfg.OllamaBaseURL = "http://127.0.0.1:11434"
aiCfg.ControlLevel = config.ControlLevelAutonomous
aiCfg.PatrolAutoFix = true
aiCfg.AlertTriggeredAnalysis = true
if err := persistence.SaveAIConfig(*aiCfg); err != nil {
t.Fatalf("save ai config: %v", err)
}
handler := newTestAISettingsHandler(cfg, persistence, nil)
handler.defaultAIService.SetLicenseChecker(stubLicenseChecker{allow: false})
req := newLoopbackRequest(http.MethodGet, "/api/settings/ai", nil)
rec := httptest.NewRecorder()
handler.HandleGetAISettings(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("GET /api/settings/ai status = %d, body %s", rec.Code, rec.Body.String())
}
var resp AISettingsResponse
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
t.Fatalf("decode response: %v", err)
}
if resp.ControlLevel != config.ControlLevelControlled {
t.Fatalf("control level = %q, want %q", resp.ControlLevel, config.ControlLevelControlled)
}
if resp.PatrolAutoFix {
t.Fatal("patrol auto-remediation must be entitlement-clamped")
}
if resp.AlertTriggeredAnalysis {
t.Fatal("alert-triggered analysis must be entitlement-clamped")
}
}
func sortedVMChartKeys(values map[string]VMChartData) []string {
keys := make([]string, 0, len(values))
for key := range values {
keys = append(keys, key)
}
sort.Strings(keys)
return keys
}
type contractCapturingStreamingProvider struct {
lastRequest providers.ChatRequest
}
type contractSupplementalUsageProvider struct {
settled bool
readyAt time.Time
records []unifiedresources.IngestRecord
}
func (p *contractSupplementalUsageProvider) SupplementalRecords(*monitoring.Monitor, string) []unifiedresources.IngestRecord {
out := make([]unifiedresources.IngestRecord, len(p.records))
copy(out, p.records)
return out
}
func (p *contractSupplementalUsageProvider) SnapshotOwnedSources() []unifiedresources.DataSource {
return []unifiedresources.DataSource{unifiedresources.SourceTrueNAS}
}
func (p *contractSupplementalUsageProvider) SupplementalInventoryReadyAt(*monitoring.Monitor, string) (time.Time, bool) {
return p.readyAt, p.settled
}
func (p *contractSupplementalUsageProvider) settle(count int) {
now := time.Now().UTC()
p.readyAt = now
p.settled = true
p.records = make([]unifiedresources.IngestRecord, 0, count)
for i := 0; i < count; i++ {
host := fmt.Sprintf("contract-truenas-%02d.lab.local", i+1)
p.records = append(p.records, unifiedresources.IngestRecord{
SourceID: fmt.Sprintf("system:contract-truenas-%02d", i+1),
Resource: unifiedresources.Resource{
ID: fmt.Sprintf("contract-truenas-%02d", i+1),
Type: unifiedresources.ResourceTypeAgent,
Name: host,
Status: unifiedresources.StatusOnline,
LastSeen: now,
UpdatedAt: now,
Identity: unifiedresources.ResourceIdentity{
Hostnames: []string{host},
},
TrueNAS: &unifiedresources.TrueNASData{
Hostname: host,
},
},
})
}
}
func (p *contractCapturingStreamingProvider) Chat(ctx context.Context, req providers.ChatRequest) (*providers.ChatResponse, error) {
p.lastRequest = req
return &providers.ChatResponse{Content: "ok", Model: req.Model}, nil
}
func (p *contractCapturingStreamingProvider) ChatStream(ctx context.Context, req providers.ChatRequest, callback providers.StreamCallback) error {
p.lastRequest = req
callback(providers.StreamEvent{Type: "content", Data: providers.ContentEvent{Text: "hello"}})
callback(providers.StreamEvent{Type: "done", Data: providers.DoneEvent{StopReason: "end_turn"}})
return nil
}
func (p *contractCapturingStreamingProvider) TestConnection(ctx context.Context) error { return nil }
func (p *contractCapturingStreamingProvider) Name() string { return "contract-capture" }
func (p *contractCapturingStreamingProvider) ListModels(ctx context.Context) ([]providers.ModelInfo, error) {
return nil, nil
}
func (p *contractCapturingStreamingProvider) SupportsThinking(model string) bool { return false }
func TestContract_WebSocketTrustedProxyHostedOrigin(t *testing.T) {
t.Setenv("PULSE_TRUSTED_PROXY_CIDRS", "127.0.0.1/32")
resetTrustedProxyCIDRsForTests()
rawToken := "contract-ws-origin-forwarded-123.12345678"
record := newTokenRecord(t, rawToken, []string{config.ScopeMonitoringRead}, nil)
server, cleanup := newWebSocketRouter(t, []string{}, record)
defer cleanup()
wsURL := "ws" + strings.TrimPrefix(server.URL, "http") + "/ws?org_id=default"
headers := http.Header{}
headers.Set("X-API-Token", rawToken)
headers.Set("Origin", "https://tenant.example.com")
headers.Set("X-Forwarded-Proto", "https")
headers.Set("X-Forwarded-Host", "tenant.example.com")
conn, resp, err := websocket.DefaultDialer.Dial(wsURL, headers)
if err != nil {
t.Fatalf("expected websocket connection behind trusted proxy, got %v", err)
}
if resp == nil || resp.StatusCode != http.StatusSwitchingProtocols {
conn.Close()
t.Fatalf("expected 101 switching protocols, got %v", resp)
}
conn.Close()
}
func TestContract_AIRelayTargetHostnameEquivalence(t *testing.T) {
server := agentexec.NewServer(func(string, string, string) bool { return true })
ts := newIPv4HTTPServer(t, http.HandlerFunc(server.HandleWebSocket))
defer ts.Close()
conn := registerAgent(t, ts.URL, "agent-1", "prox97.seftic.local")
defer conn.Close()
adapter := &agentCommandAdapter{handler: &AISettingsHandler{agentServer: server}}
if got := adapter.FindAgentForTarget("prox97"); got != "agent-1" {
t.Fatalf("FindAgentForTarget(%q) = %q, want agent-1", "prox97", got)
}
if got := adapter.FindAgentForTarget("prox97.seftic.local"); got != "agent-1" {
t.Fatalf("FindAgentForTarget(%q) = %q, want agent-1", "prox97.seftic.local", got)
}
if got := adapter.FindAgentForTarget("prox97.other.local"); got != "" {
t.Fatalf("FindAgentForTarget(%q) = %q, want empty for distinct FQDN", "prox97.other.local", got)
}
}
func TestContract_WireAIChatDependencies_WiresTrueNASAppActionProvider(t *testing.T) {
router := &Router{
trueNASPoller: monitoring.NewTrueNASPoller(nil, 0, nil),
}
service := &capturingAIService{}
router.wireAIChatDependenciesForService(context.Background(), service)
if service.appContainerActionProvider == nil {
t.Fatal("expected TrueNAS app action provider to be wired into AI chat dependencies")
}
if service.appContainerReadProvider == nil {
t.Fatal("expected TrueNAS app read provider to be wired into AI chat dependencies")
}
if service.appContainerConfigProvider == nil {
t.Fatal("expected TrueNAS app config provider to be wired into AI chat dependencies")
}
}
func TestContract_ChatServiceAdapterPatrolForwardsExecutionID(t *testing.T) {
cfg := chat.Config{
DataDir: t.TempDir(),
AIConfig: &config.AIConfig{
Enabled: true,
ChatModel: "stub:model",
PatrolModel: "stub:model",
},
}
service := chat.NewService(cfg)
provider := &contractCapturingStreamingProvider{}
setUnexportedField(t, service, "providerFactory", func(string) (providers.StreamingProvider, error) {
return provider, nil
})
if err := service.Start(context.Background()); err != nil {
t.Fatalf("Start: %v", err)
}
t.Cleanup(func() { _ = service.Stop(context.Background()) })
adapter := &chatServiceAdapter{svc: service}
resp, err := adapter.ExecutePatrolStream(context.Background(), ai.PatrolExecuteRequest{
Prompt: "patrol",
ExecutionID: "patrol-run-contract",
}, func(ai.ChatStreamEvent) {})
if err != nil {
t.Fatalf("ExecutePatrolStream: %v", err)
}
if resp == nil || resp.Content == "" {
t.Fatalf("expected patrol response content, got %#v", resp)
}
if provider.lastRequest.ExecutionID != "patrol-run-contract" {
t.Fatalf("execution_id=%q want patrol-run-contract", provider.lastRequest.ExecutionID)
}
}
func TestContract_AIQuickstartPayloadFieldsAreRetired(t *testing.T) {
settingsBody, err := json.Marshal(AISettingsResponse{
Enabled: true,
Configured: false,
})
if err != nil {
t.Fatalf("marshal AI settings response: %v", err)
}
if bytes.Contains(settingsBody, []byte(`quickstart`)) {
t.Fatalf("expected AI settings payload not to expose retired quickstart fields, got %s", settingsBody)
}
statusBody, err := json.Marshal(PatrolStatusResponse{
RuntimeState: ai.PatrolRuntimeStateActive,
Healthy: true,
})
if err != nil {
t.Fatalf("marshal patrol status response: %v", err)
}
if bytes.Contains(statusBody, []byte(`quickstart`)) {
t.Fatalf("expected patrol status payload not to expose retired quickstart fields, got %s", statusBody)
}
}
func TestContract_AISettingsUpdateRequiresProviderBeforeEnable(t *testing.T) {
tmp := t.TempDir()
cfg := &config.Config{DataPath: tmp}
persistence := config.NewConfigPersistence(tmp)
handler := newTestAISettingsHandler(cfg, persistence, nil)
req := newLoopbackRequest(http.MethodPut, "/api/settings/ai/update", strings.NewReader(`{
"enabled": true
}`))
req.Header.Set("Content-Type", "application/json")
rec := httptest.NewRecorder()
handler.HandleUpdateAISettings(rec, req)
if rec.Code != http.StatusBadRequest {
t.Fatalf("status=%d, want %d: %s", rec.Code, http.StatusBadRequest, rec.Body.String())
}
if strings.Contains(strings.ToLower(rec.Body.String()), "quickstart") {
t.Fatalf("provider-required response must not mention retired quickstart, got %s", rec.Body.String())
}
if !strings.Contains(rec.Body.String(), "Please configure a provider") {
t.Fatalf("expected provider-required response, got %s", rec.Body.String())
}
}
func TestContract_AISettingsUpdateProviderResolutionJSONSnapshot(t *testing.T) {
ollama := newIPv4HTTPServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/api/tags":
_ = json.NewEncoder(w).Encode(map[string]any{
"models": []map[string]any{
{"name": "llama3:latest"},
{"name": "mistral:latest"},
},
})
default:
http.NotFound(w, r)
}
}))
defer ollama.Close()
tmp := t.TempDir()
cfg := &config.Config{DataPath: tmp}
persistence := config.NewConfigPersistence(tmp)
handler := newTestAISettingsHandler(cfg, persistence, nil)
req := newLoopbackRequest(http.MethodPut, "/api/settings/ai/update", strings.NewReader(fmt.Sprintf(`{
"enabled": true,
"ollama_base_url": %q
}`, ollama.URL)))
req.Header.Set("Content-Type", "application/json")
rec := httptest.NewRecorder()
handler.HandleUpdateAISettings(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status=%d, want %d: %s", rec.Code, http.StatusOK, rec.Body.String())
}
want := fmt.Sprintf(`{
"enabled":true,
"model":"ollama:llama3:latest",
"configured":true,
"custom_context":"",
"auth_method":"api_key",
"oauth_connected":false,
"patrol_interval_minutes":360,
"patrol_enabled":true,
"patrol_auto_fix":false,
"alert_triggered_analysis":true,
"patrol_event_triggers_enabled":true,
"patrol_alert_triggers_enabled":true,
"patrol_anomaly_triggers_enabled":true,
"use_proactive_thresholds":false,
"available_models":[],
"anthropic_configured":false,
"openai_configured":false,
"openrouter_configured":false,
"deepseek_configured":false,
"gemini_configured":false,
"ollama_configured":true,
"ollama_base_url":%q,
"ollama_password_set":false,
"configured_providers":["ollama"],
"control_level":"read_only",
"protected_guests":[],
"discovery_enabled":false,
"patrol_readiness":{
"status":"warning",
"ready":true,
"cause":"model_tool_support_unverified",
"summary":"Ollama connectivity alone does not prove tool support. Use an Ollama model that returns tool_calls for Patrol verification.",
"provider":"ollama",
"model":"ollama:llama3:latest",
"checks":[{
"id":"configuration",
"status":"warning",
"cause":"model_tool_support_unverified",
"label":"Patrol configuration",
"message":"Ollama connectivity alone does not prove tool support. Use an Ollama model that returns tool_calls for Patrol verification."
}]
}
}`, ollama.URL)
// patrol_preflight is now emitted on update responses with dynamic
// timestamps and durations that vary per run; strip it from the
// snapshot comparison and rely on dedicated preflight tests for its
// shape.
assertJSONSnapshot(t, rec.Body.Bytes(), want, "patrol_preflight")
}
func TestContract_PatrolStatusDoesNotSurfaceQuickstartActivation(t *testing.T) {
handler, patrol, _, _ := setupAIHandlerWithPatrol(t)
persistence := handler.defaultPersistence
aiCfg := config.NewDefaultAIConfig()
aiCfg.Enabled = true
if err := persistence.SaveAIConfig(*aiCfg); err != nil {
t.Fatalf("SaveAIConfig: %v", err)
}
if err := handler.defaultAIService.LoadConfig(); err != nil {
t.Fatalf("LoadConfig: %v", err)
}
setUnexportedField(t, patrol, "lastBlockedReason", "Quickstart credits exhausted. Connect your API key to continue using AI Patrol.")
setUnexportedField(t, patrol, "lastBlockedAt", time.Now().UTC())
req := httptest.NewRequest(http.MethodGet, "/api/ai/patrol/status", nil)
rec := httptest.NewRecorder()
handler.HandleGetPatrolStatus(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status=%d, want %d: %s", rec.Code, http.StatusOK, rec.Body.String())
}
var resp PatrolStatusResponse
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
t.Fatalf("decode patrol status: %v", err)
}
if resp.RuntimeState == ai.PatrolRuntimeStateBlocked {
t.Fatalf("runtime_state=%q, want non-blocked for retired quickstart", resp.RuntimeState)
}
if !resp.Enabled {
t.Fatal("expected patrol status to remain enabled while activation is required")
}
if !resp.Healthy {
t.Fatal("expected retired quickstart activation state to stay healthy on the public status surface")
}
if strings.Contains(strings.ToLower(resp.BlockedReason), "quickstart") {
t.Fatalf("blocked_reason must not expose retired quickstart copy, got %q", resp.BlockedReason)
}
}
func TestContract_AISettingsDoesNotExposeQuickstartActivation(t *testing.T) {
tmp := t.TempDir()
cfg := &config.Config{DataPath: tmp}
persistence := config.NewConfigPersistence(tmp)
handler := newTestAISettingsHandler(cfg, persistence, nil)
req := httptest.NewRequest(http.MethodGet, "/api/settings/ai", nil)
rec := httptest.NewRecorder()
handler.HandleGetAISettings(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status=%d, want %d: %s", rec.Code, http.StatusOK, rec.Body.String())
}
var resp AISettingsResponse
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
t.Fatalf("decode ai settings: %v", err)
}
if resp.Configured {
t.Fatal("expected AI settings to remain unconfigured without BYOK/local provider")
}
if bytes.Contains(bytes.ToLower(rec.Body.Bytes()), []byte("quickstart")) {
t.Fatalf("AI settings payload must not expose retired quickstart fields, got %s", rec.Body.Bytes())
}
}
func TestContract_AISettingsBYOKOverrideDoesNotExposeQuickstartInventoryJSONSnapshot(t *testing.T) {
tmp := t.TempDir()
cfg := &config.Config{DataPath: tmp}
persistence := config.NewConfigPersistence(tmp)
aiCfg := config.NewDefaultAIConfig()
aiCfg.Enabled = true
aiCfg.Model = "openai:gpt-4o"
aiCfg.OpenAIAPIKey = "sk-openai-test"
if err := persistence.SaveAIConfig(*aiCfg); err != nil {
t.Fatalf("SaveAIConfig: %v", err)
}
handler := newTestAISettingsHandler(cfg, persistence, nil)
req := httptest.NewRequest(http.MethodGet, "/api/settings/ai", nil)
rec := httptest.NewRecorder()
handler.HandleGetAISettings(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status=%d, want %d: %s", rec.Code, http.StatusOK, rec.Body.String())
}
const want = `{
"enabled":true,
"model":"openai:gpt-4o",
"configured":true,
"custom_context":"",
"auth_method":"api_key",
"oauth_connected":false,
"patrol_interval_minutes":360,
"patrol_enabled":true,
"patrol_auto_fix":false,
"alert_triggered_analysis":true,
"patrol_event_triggers_enabled":true,
"patrol_alert_triggers_enabled":true,
"patrol_anomaly_triggers_enabled":true,
"use_proactive_thresholds":false,
"available_models":[],
"anthropic_configured":false,
"openai_configured":true,
"openrouter_configured":false,
"deepseek_configured":false,
"gemini_configured":false,
"ollama_configured":false,
"ollama_base_url":"http://localhost:11434",
"ollama_password_set":false,
"configured_providers":["openai"],
"control_level":"read_only",
"protected_guests":[],
"discovery_enabled":false,
"patrol_readiness":{"status":"not_ready","ready":false,"cause":"service_unavailable","summary":"Pulse Patrol service is not available.","checks":[{"id":"service","status":"not_ready","cause":"service_unavailable","label":"Patrol service","message":"Pulse Patrol service is not available.","action":"restart_service"}]}
}`
assertJSONSnapshot(t, rec.Body.Bytes(), want)
}
func TestContract_ChartMetricPointsPreserveMillisecondPrecision(t *testing.T) {
pointTime := time.Date(2026, time.March, 31, 12, 0, 0, 987_000_000, time.UTC)
converted := monitorPointsToAPI([]monitoring.MetricPoint{{
Timestamp: pointTime,
Value: 42,
}})
if len(converted) != 1 {
t.Fatalf("expected one converted point, got %d", len(converted))
}
if converted[0].Timestamp != pointTime.UnixMilli() {
t.Fatalf("expected millisecond timestamp %d, got %d", pointTime.UnixMilli(), converted[0].Timestamp)
}
}
func TestContract_StorageChartsUseCanonicalMetricsTargetIDs(t *testing.T) {
monitor, state, metricsHistory := newTestMonitor(t)
now := time.Now().UTC().Add(-5 * time.Minute).Truncate(time.Millisecond)
metricsHistory.AddStorageMetric("vc-1:datastore:datastore-202", "usage", 0.25, now)
metricsHistory.AddStorageMetric("vc-1:datastore:datastore-202", "used", 3.57*1024*1024*1024*1024, now)
metricsHistory.AddStorageMetric("vc-1:datastore:datastore-202", "avail", 11.03*1024*1024*1024*1024, now)
metricsHistory.AddStorageMetric("pool:archive", "usage", 0.36, now)
metricsHistory.AddStorageMetric("pool:archive", "used", 5.49*1024*1024*1024*1024, now)
metricsHistory.AddStorageMetric("pool:archive", "avail", 4.51*1024*1024*1024*1024, now)
adapter := unifiedresources.NewMonitorAdapter(nil)
adapter.PopulateSnapshotAndSupplemental(state.GetSnapshot(), map[unifiedresources.DataSource][]unifiedresources.IngestRecord{
unifiedresources.SourceVMware: {
{
SourceID: "vc-1:datastore:datastore-202",
Resource: unifiedresources.Resource{
ID: "storage-vmware-1",
Type: unifiedresources.ResourceTypeStorage,
Name: "archive-tier",
Status: unifiedresources.StatusOnline,
LastSeen: now,
Storage: &unifiedresources.StorageMeta{
Type: "datastore",
Platform: "vmware",
Nodes: []string{"esxi-01.lab.local"},
},
VMware: &unifiedresources.VMwareData{
ConnectionID: "vc-1",
EntityType: "datastore",
ManagedObjectID: "datastore-202",
RuntimeHostName: "esxi-01.lab.local",
},
},
},
},
unifiedresources.SourceTrueNAS: {
{
SourceID: "pool:archive",
Resource: unifiedresources.Resource{
ID: "storage-truenas-1",
Type: unifiedresources.ResourceTypeStorage,
Name: "archive",
Status: unifiedresources.StatusOnline,
LastSeen: now,
Storage: &unifiedresources.StorageMeta{
Type: "zfs-pool",
Platform: "truenas",
},
TrueNAS: &unifiedresources.TrueNASData{
Hostname: "truenas-main",
},
},
},
},
})
setTestUnexportedField(t, monitor, "resourceStore", monitoring.ResourceStoreInterface(adapter))
router := &Router{monitor: monitor}
req := httptest.NewRequest(http.MethodGet, "/api/storage-charts?range=60", nil)
rec := httptest.NewRecorder()
router.handleStorageCharts(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("expected 200, got %d: %s", rec.Code, rec.Body.String())
}
var decoded StorageChartsResponse
if err := json.Unmarshal(rec.Body.Bytes(), &decoded); err != nil {
t.Fatalf("unmarshal storage charts response: %v", err)
}
if _, ok := decoded.Pools["vc-1:datastore:datastore-202"]; !ok {
t.Fatalf("expected VMware datastore chart keyed by canonical metrics target, got %v", decoded.Pools)
}
if _, ok := decoded.Pools["pool:archive"]; !ok {
t.Fatalf("expected TrueNAS pool chart keyed by canonical metrics target, got %v", decoded.Pools)
}
if _, ok := decoded.Pools["storage-vmware-1"]; ok {
t.Fatalf("expected raw VMware resource id to stay out of chart payload, got %v", decoded.Pools)
}
if _, ok := decoded.Pools["storage-truenas-1"]; ok {
t.Fatalf("expected raw TrueNAS resource id to stay out of chart payload, got %v", decoded.Pools)
}
vmwarePool := decoded.Pools["vc-1:datastore:datastore-202"]
if len(vmwarePool.Used) != 1 || len(vmwarePool.Avail) != 1 {
t.Fatalf("expected canonical storage summary payload to preserve used/avail series, got %+v", vmwarePool)
}
if vmwarePool.Used[0].Timestamp != now.UnixMilli() || vmwarePool.Avail[0].Timestamp != now.UnixMilli() {
t.Fatalf(
"expected storage summary series to preserve millisecond timestamps, got used=%v avail=%v",
vmwarePool.Used,
vmwarePool.Avail,
)
}
}
func TestContract_StorageSummaryChartsStayAggregateOnly(t *testing.T) {
monitor, state, metricsHistory := newTestMonitor(t)
now := time.Now().UTC().Truncate(time.Second)
state.Storage = []models.Storage{
{ID: "store-1", Name: "Store One"},
{ID: "store-2", Name: "Store Two"},
}
metricsHistory.AddStorageMetric("store-1", "used", 400, now)
metricsHistory.AddStorageMetric("store-1", "avail", 600, now)
metricsHistory.AddStorageMetric("store-1", "total", 1000, now)
metricsHistory.AddStorageMetric("store-2", "used", 100, now)
metricsHistory.AddStorageMetric("store-2", "avail", 900, now)
metricsHistory.AddStorageMetric("store-2", "total", 1000, now)
syncTestResourceStore(t, monitor, state)
router := &Router{monitor: monitor}
req := httptest.NewRequest(http.MethodGet, "/api/charts/storage-summary?range=24h", nil)
rec := httptest.NewRecorder()
router.handleStorageSummaryCharts(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("expected 200, got %d: %s", rec.Code, rec.Body.String())
}
var decoded StorageSummaryTrendResponse
if err := json.Unmarshal(rec.Body.Bytes(), &decoded); err != nil {
t.Fatalf("unmarshal storage summary response: %v", err)
}
if len(decoded.Capacity) != 1 {
t.Fatalf("expected one aggregate capacity point, got %+v", decoded.Capacity)
}
if decoded.Capacity[0].Value != 25 {
t.Fatalf("expected aggregate capacity of 25%%, got %+v", decoded.Capacity[0])
}
if decoded.Stats.PointCounts.Total != 1 || decoded.Stats.PointCounts.Storage != 1 {
t.Fatalf("expected aggregate-only point counts, got %+v", decoded.Stats.PointCounts)
}
}
func TestContract_TrueNASConnectionsDisabledMessageIsExplicit(t *testing.T) {
setTrueNASFeatureForTest(t, false)
handler, _, _ := newTrueNASHandlersForTest(t, nil)
req := httptest.NewRequest(http.MethodGet, "/api/truenas/connections", nil)
rec := httptest.NewRecorder()
handler.HandleList(rec, req)
if rec.Code != http.StatusNotFound {
t.Fatalf("expected 404 when TrueNAS integration is disabled, got %d: %s", rec.Code, rec.Body.String())
}
if !strings.Contains(rec.Body.String(), "explicitly disabled") {
t.Fatalf("expected explicit disable message, got %s", rec.Body.String())
}
}
func TestContract_TrueNASSavedConnectionTestsUpdateRuntimeSummary(t *testing.T) {
setTrueNASFeatureForTest(t, true)
connection := config.TrueNASInstance{
ID: "conn-1",
Name: "tower",
Host: "truenas.local",
Port: 443,
APIKey: "super-secret",
UseHTTPS: true,
InsecureSkipVerify: false,
Enabled: true,
}
handler, persistence, _ := newTrueNASHandlersForTest(t, nil)
if err := persistence.SaveTrueNASConfig([]config.TrueNASInstance{connection}); err != nil {
t.Fatalf("seed truenas config: %v", err)
}
poller := monitoring.NewTrueNASPoller(nil, time.Minute, nil)
handler.getPoller = func(context.Context) *monitoring.TrueNASPoller { return poller }
handler.newClient = func(cfg truenas.ClientConfig) (trueNASClient, error) {
return &fakeTrueNASClient{}, nil
}
req := httptest.NewRequest(http.MethodPost, "/api/truenas/connections/conn-1/test", nil)
rec := httptest.NewRecorder()
handler.HandleTestSavedConnection(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("expected 200, got %d: %s", rec.Code, rec.Body.String())
}
summary := poller.ConnectionSummaries("default", []config.TrueNASInstance{connection})[connection.ID]
if summary.Poll == nil || summary.Poll.LastSuccessAt == nil {
t.Fatalf("expected saved manual test to refresh poll summary, got %+v", summary.Poll)
}
}
func TestContract_VMwareConnectionsDisabledMessageIsExplicit(t *testing.T) {
setVMwareFeatureForTest(t, false)
handler, _ := newVMwareHandlersForTest(t)
req := httptest.NewRequest(http.MethodGet, "/api/vmware/connections", nil)
rec := httptest.NewRecorder()
handler.HandleList(rec, req)
if rec.Code != http.StatusNotFound {
t.Fatalf("expected 404 when VMware integration is disabled, got %d: %s", rec.Code, rec.Body.String())
}
if !strings.Contains(rec.Body.String(), "explicitly disabled") {
t.Fatalf("expected explicit disable message, got %s", rec.Body.String())
}
}
func TestContract_VMwareSavedConnectionTestsUpdateRuntimeSummary(t *testing.T) {
setVMwareFeatureForTest(t, true)
connection := config.VMwareVCenterInstance{
ID: "conn-1",
Name: "lab-vcenter",
Host: "vcsa.lab.local",
Port: 443,
Username: "administrator@vsphere.local",
Password: "super-secret",
Enabled: true,
}
handler, persistence := newVMwareHandlersForTest(t)
poller := monitoring.NewVMwarePoller(nil, time.Minute)
handler.getPoller = func(context.Context) *monitoring.VMwarePoller { return poller }
if err := persistence.SaveVMwareConfig([]config.VMwareVCenterInstance{connection}); err != nil {
t.Fatalf("seed vmware config: %v", err)
}
handler.newClient = func(cfg vmware.ClientConfig) (vmwareClient, error) {
return &fakeVMwareClient{
testConnection: func(context.Context) (*vmware.InventorySummary, error) {
return &vmware.InventorySummary{Hosts: 3, VMs: 20, Datastores: 4, Networks: 6, VIRelease: "8.0.3"}, nil
},
}, nil
}
req := httptest.NewRequest(http.MethodPost, "/api/vmware/connections/conn-1/test", nil)
rec := httptest.NewRecorder()
handler.HandleTestSavedConnection(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("expected 200, got %d: %s", rec.Code, rec.Body.String())
}
summary := poller.ConnectionSummaries("default", []config.VMwareVCenterInstance{connection})[connection.ID]
if summary.Poll == nil || summary.Poll.LastSuccessAt == nil {
t.Fatalf("expected saved manual test to refresh runtime summary, got %+v", summary.Poll)
}
if summary.Observed == nil || summary.Observed.VMs != 20 || summary.Observed.Networks != 6 {
t.Fatalf("expected saved manual test to refresh observed summary, got %+v", summary.Observed)
}
}
func TestContract_VMwareConnectionListCarriesObservedSummary(t *testing.T) {
setVMwareFeatureForTest(t, true)
connection := config.VMwareVCenterInstance{
ID: "conn-1",
Name: "lab-vcenter",
Host: "vcsa.lab.local",
Port: 443,
Username: "administrator@vsphere.local",
Password: "super-secret",
Enabled: true,
}
handler, persistence := newVMwareHandlersForTest(t)
poller := monitoring.NewVMwarePoller(nil, time.Minute)
handler.getPoller = func(context.Context) *monitoring.VMwarePoller { return poller }
if err := persistence.SaveVMwareConfig([]config.VMwareVCenterInstance{connection}); err != nil {
t.Fatalf("seed vmware config: %v", err)
}
collectedAt := time.Date(2026, 3, 30, 18, 0, 0, 0, time.UTC)
poller.RecordConnectionTestSuccess("default", connection.ID, &vmware.InventorySummary{
Hosts: 4,
VMs: 24,
Datastores: 6,
Networks: 8,
VIRelease: "8.0.3",
}, collectedAt)
req := httptest.NewRequest(http.MethodGet, "/api/vmware/connections", nil)
rec := httptest.NewRecorder()
handler.HandleList(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("expected 200, got %d: %s", rec.Code, rec.Body.String())
}
var responses []vmwareConnectionResponse
if err := json.Unmarshal(rec.Body.Bytes(), &responses); err != nil {
t.Fatalf("unmarshal vmware list response: %v", err)
}
if len(responses) != 1 {
t.Fatalf("expected 1 vmware connection response, got %d", len(responses))
}
if responses[0].Password != "********" {
t.Fatalf("expected redacted password in vmware list response, got %q", responses[0].Password)
}
if responses[0].Poll == nil || responses[0].Poll.LastSuccessAt == nil {
t.Fatalf("expected poll summary in vmware list response, got %+v", responses[0])
}
if responses[0].Observed == nil {
t.Fatalf("expected observed summary in vmware list response, got %+v", responses[0])
}
if got := responses[0].Observed.Hosts; got != 4 {
t.Fatalf("observed hosts = %d, want 4", got)
}
if got := responses[0].Observed.VMs; got != 24 {
t.Fatalf("observed vms = %d, want 24", got)
}
if got := responses[0].Observed.Datastores; got != 6 {
t.Fatalf("observed datastores = %d, want 6", got)
}
if got := responses[0].Observed.Networks; got != 8 {
t.Fatalf("observed networks = %d, want 8", got)
}
if got := responses[0].Observed.VIRelease; got != "8.0.3" {
t.Fatalf("observed viRelease = %q, want 8.0.3", got)
}
if responses[0].Observed.CollectedAt == nil || !responses[0].Observed.CollectedAt.Equal(collectedAt) {
t.Fatalf("observed collectedAt = %+v, want %s", responses[0].Observed.CollectedAt, collectedAt.Format(time.RFC3339))
}
}
func TestContract_InfrastructureChartsNormalizeLongRangeMixedCadence(t *testing.T) {
store := newTestMetricsStore(t)
monitor, state, _ := newTestMonitor(t)
setTestUnexportedField(t, monitor, "metricsStore", store)
state.Nodes = []models.Node{{
ID: "node-contract-1",
Name: "node-contract-1",
Instance: "pve1",
Status: "online",
CPU: 0.75,
Memory: models.Memory{Usage: 42.0},
Disk: models.Disk{Usage: 55.0},
}}
syncTestResourceStore(t, monitor, state)
now := time.Now().UTC().Add(-10 * time.Minute).Truncate(time.Minute)
windowStart := now.Add(-7 * 24 * time.Hour)
seed := make([]metrics.WriteMetric, 0, 1200)
appendMetric := func(ts time.Time, value float64) {
for _, metricType := range []string{"cpu", "memory", "disk"} {
seed = append(seed, metrics.WriteMetric{
ResourceType: "node",
ResourceID: "node-contract-1",
MetricType: metricType,
Value: value,
Timestamp: ts,
Tier: metrics.TierMinute,
})
}
}
for ts := windowStart; ts.Before(now.Add(-24 * time.Hour)); ts = ts.Add(65 * time.Minute) {
appendMetric(ts, 20)
}
for ts := now.Add(-24 * time.Hour); ts.Before(now.Add(-2 * time.Hour)); ts = ts.Add(2 * time.Minute) {
appendMetric(ts, 40)
}
for ts := now.Add(-2 * time.Hour); ts.Before(now); ts = ts.Add(time.Minute) {
appendMetric(ts, 60)
}
appendMetric(now, 75)
store.WriteBatchSync(seed)
router := &Router{monitor: monitor}
req := httptest.NewRequest(http.MethodGet, "/api/charts/infrastructure?range=7d", nil)
rec := httptest.NewRecorder()
router.handleInfrastructureCharts(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("expected 200, got %d: %s", rec.Code, rec.Body.String())
}
var decoded InfrastructureChartsResponse
if err := json.Unmarshal(rec.Body.Bytes(), &decoded); err != nil {
t.Fatalf("unmarshal infrastructure charts response: %v", err)
}
cpuSeries := decoded.NodeData["node-contract-1"]["cpu"]
if len(cpuSeries) == 0 {
t.Fatal("expected normalized cpu series")
}
if len(cpuSeries) > infrastructureSummaryMaxSeriesPoints {
t.Fatalf("expected cpu series <= %d points, got %d", infrastructureSummaryMaxSeriesPoints, len(cpuSeries))
}
if cpuSeries[len(cpuSeries)-1].Timestamp != now.UnixMilli() {
t.Fatalf("expected latest cpu timestamp %d, got %d", now.UnixMilli(), cpuSeries[len(cpuSeries)-1].Timestamp)
}
if cpuSeries[len(cpuSeries)-1].Value != 75 {
t.Fatalf("expected latest cpu value 75, got %.2f", cpuSeries[len(cpuSeries)-1].Value)
}
recentWindowStart := now.Add(-24 * time.Hour).UnixMilli()
recentCount := 0
for _, point := range cpuSeries {
if point.Timestamp >= recentWindowStart {
recentCount++
}
}
if recentCount > 20 {
t.Fatalf("expected day-proportional recent summary buckets, got %d recent cpu points", recentCount)
}
}
func TestContract_InfrastructureChartsHonorExplicitMetricFilters(t *testing.T) {
monitor, state, _ := newTestMonitor(t)
state.Nodes = []models.Node{{
ID: "node-contract-1",
Name: "node-contract-1",
Status: "online",
CPU: 0.1,
Memory: models.Memory{Usage: 12.0},
Disk: models.Disk{Usage: 34.0},
}}
state.DockerHosts = []models.DockerHost{{
ID: "docker-host-contract-1",
Runtime: "docker",
CPUUsage: 23.0,
Memory: models.Memory{Usage: 45.0},
Disks: []models.Disk{{Usage: 67.0}},
Status: "online",
}}
state.Hosts = []models.Host{{
ID: "agent-contract-1",
Hostname: "agent-contract-1",
CPUUsage: 11.0,
Memory: models.Memory{Usage: 22.0},
Disks: []models.Disk{{Usage: 33.0}},
Status: "online",
}}
syncTestResourceStore(t, monitor, state)
router := &Router{monitor: monitor}
req := httptest.NewRequest(
http.MethodGet,
"/api/charts/infrastructure?range=5m&metrics=cpu,memory",
nil,
)
rec := httptest.NewRecorder()
router.handleInfrastructureCharts(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("expected 200, got %d: %s", rec.Code, rec.Body.String())
}
var decoded InfrastructureChartsResponse
if err := json.Unmarshal(rec.Body.Bytes(), &decoded); err != nil {
t.Fatalf("unmarshal infrastructure charts response: %v", err)
}
if decoded.Stats.PointCounts.Nodes != 2 {
t.Fatalf("expected stats.pointCounts.nodes=2, got %d", decoded.Stats.PointCounts.Nodes)
}
if decoded.Stats.PointCounts.DockerHosts != 2 {
t.Fatalf(
"expected stats.pointCounts.dockerHosts=2, got %d",
decoded.Stats.PointCounts.DockerHosts,
)
}
if decoded.Stats.PointCounts.Agents != 2 {
t.Fatalf("expected stats.pointCounts.agents=2, got %d", decoded.Stats.PointCounts.Agents)
}
if _, ok := decoded.NodeData["node-contract-1"]["disk"]; ok {
t.Fatal("expected node disk series to be filtered out of infrastructure summary payload")
}
if got := len(decoded.NodeData["node-contract-1"]); got != 2 {
t.Fatalf("expected node payload to contain only requested metrics, got %d entries", got)
}
if _, ok := decoded.DockerHostData["docker-host-contract-1"]["disk"]; ok {
t.Fatal("expected docker-host disk series to be filtered out of infrastructure summary payload")
}
if got := len(decoded.DockerHostData["docker-host-contract-1"]); got != 2 {
t.Fatalf("expected docker-host payload to contain only requested metrics, got %d entries", got)
}
if _, ok := decoded.AgentData["agent-contract-1"]["disk"]; ok {
t.Fatal("expected agent disk series to be filtered out of infrastructure summary payload")
}
if got := len(decoded.AgentData["agent-contract-1"]); got != 2 {
t.Fatalf("expected agent payload to contain only requested metrics, got %d entries", got)
}
}
func TestContract_MockChartRoutesUseCanonicalMockUnifiedReadStateForVMwareHosts(t *testing.T) {
prevMock := mock.IsMockEnabled()
mock.SetEnabled(true)
t.Cleanup(func() { mock.SetEnabled(prevMock) })
fixtures := vmware.DefaultFixtures()
if len(fixtures.Hosts) == 0 {
t.Fatal("expected default VMware fixtures to include at least one host")
}
expectedHostID := vmware.SourceID(fixtures.ConnectionID, "host", fixtures.Hosts[0].Host)
monitor, state, _ := newTestMonitor(t)
state.Hosts = []models.Host{{
ID: "live-store-host-1",
Hostname: "live-store-host-1",
CPUUsage: 11.0,
Memory: models.Memory{Usage: 22.0},
Disks: []models.Disk{{Usage: 33.0}},
Status: "online",
}}
syncTestResourceStore(t, monitor, state)
router := &Router{monitor: monitor}
for _, path := range []string{"/api/charts?range=5m", "/api/charts/infrastructure?range=5m"} {
req := httptest.NewRequest(http.MethodGet, path, nil)
rec := httptest.NewRecorder()
switch path {
case "/api/charts?range=5m":
router.handleCharts(rec, req)
case "/api/charts/infrastructure?range=5m":
router.handleInfrastructureCharts(rec, req)
default:
t.Fatalf("unexpected path %q", path)
}
if rec.Code != http.StatusOK {
t.Fatalf("%s status = %d, body=%s", path, rec.Code, rec.Body.String())
}
agentData := map[string]VMChartData{}
if path == "/api/charts?range=5m" {
var decoded ChartResponse
if err := json.Unmarshal(rec.Body.Bytes(), &decoded); err != nil {
t.Fatalf("decode %s: %v", path, err)
}
agentData = decoded.AgentData
} else {
var decoded InfrastructureChartsResponse
if err := json.Unmarshal(rec.Body.Bytes(), &decoded); err != nil {
t.Fatalf("decode %s: %v", path, err)
}
agentData = decoded.AgentData
}
series, ok := agentData[expectedHostID]
if !ok {
t.Fatalf("%s missing VMware host %q; got keys=%v", path, expectedHostID, sortedVMChartKeys(agentData))
}
if len(series["cpu"]) == 0 {
t.Fatalf("%s expected VMware host %q cpu series", path, expectedHostID)
}
if _, ok := agentData["live-store-host-1"]; ok {
t.Fatalf("%s unexpectedly used live store-only host instead of canonical mock snapshot; keys=%v", path, sortedVMChartKeys(agentData))
}
}
}
func TestContract_WorkloadChartsCapLongRangeMixedCadenceByTime(t *testing.T) {
store := newTestMetricsStore(t)
monitor, state, _ := newTestMonitor(t)
setTestUnexportedField(t, monitor, "metricsStore", store)
state.Nodes = []models.Node{{
ID: "node-contract-1",
Name: "node-contract-1",
Instance: "pve1",
Status: "online",
}}
state.VMs = []models.VM{{
ID: "vm-contract-1",
VMID: 101,
Name: "vm-contract-1",
Node: "node-contract-1",
Instance: "pve1",
Status: "running",
CPU: 0.75,
Memory: models.Memory{Usage: 42.0},
Disk: models.Disk{Usage: 55.0},
NetworkIn: 128,
NetworkOut: 256,
DiskRead: 512,
DiskWrite: 256,
}}
syncTestResourceStore(t, monitor, state)
readState := monitor.GetUnifiedReadStateOrSnapshot()
vms := readState.VMs()
if len(vms) != 1 {
t.Fatalf("expected 1 vm view, got %d", len(vms))
}
sourceID := strings.TrimSpace(vms[0].SourceID())
if sourceID == "" {
t.Fatal("expected vm source ID")
}
now := time.Now().UTC().Add(-10 * time.Minute).Truncate(time.Minute)
windowStart := now.Add(-7 * 24 * time.Hour)
seed := make([]metrics.WriteMetric, 0, 2000)
appendMetric := func(ts time.Time, percentValue, rateValue float64) {
for _, metricType := range []string{"cpu", "memory", "disk"} {
seed = append(seed, metrics.WriteMetric{
ResourceType: "vm",
ResourceID: sourceID,
MetricType: metricType,
Value: percentValue,
Timestamp: ts,
Tier: metrics.TierMinute,
})
}
for _, metricType := range []string{"diskread", "diskwrite", "netin", "netout"} {
seed = append(seed, metrics.WriteMetric{
ResourceType: "vm",
ResourceID: sourceID,
MetricType: metricType,
Value: rateValue,
Timestamp: ts,
Tier: metrics.TierMinute,
})
}
}
for ts := windowStart; ts.Before(now.Add(-24 * time.Hour)); ts = ts.Add(65 * time.Minute) {
appendMetric(ts, 20, 50)
}
for ts := now.Add(-24 * time.Hour); ts.Before(now.Add(-2 * time.Hour)); ts = ts.Add(2 * time.Minute) {
appendMetric(ts, 40, 75)
}
for ts := now.Add(-2 * time.Hour); ts.Before(now); ts = ts.Add(time.Minute) {
appendMetric(ts, 60, 100)
}
appendMetric(now, 75, 125)
store.WriteBatchSync(seed)
router := &Router{monitor: monitor}
req := httptest.NewRequest(http.MethodGet, "/api/charts/workloads?range=7d&maxPoints=30", nil)
rec := httptest.NewRecorder()
router.handleWorkloadCharts(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("expected 200, got %d: %s", rec.Code, rec.Body.String())
}
var decoded WorkloadChartsResponse
if err := json.Unmarshal(rec.Body.Bytes(), &decoded); err != nil {
t.Fatalf("unmarshal workload charts response: %v", err)
}
if len(decoded.ChartData) != 1 {
t.Fatalf("expected 1 workload chart entry, got %d", len(decoded.ChartData))
}
var cpuSeries []MetricPoint
for _, chartData := range decoded.ChartData {
cpuSeries = chartData["cpu"]
break
}
if len(cpuSeries) == 0 {
t.Fatal("expected capped cpu series")
}
if len(cpuSeries) > 30 {
t.Fatalf("expected cpu series <= 30 points, got %d", len(cpuSeries))
}
if cpuSeries[len(cpuSeries)-1].Timestamp != now.UnixMilli() {
t.Fatalf("expected latest cpu timestamp %d, got %d", now.UnixMilli(), cpuSeries[len(cpuSeries)-1].Timestamp)
}
recentWindowStart := now.Add(-24 * time.Hour).UnixMilli()
recentCount := 0
for _, point := range cpuSeries {
if point.Timestamp >= recentWindowStart {
recentCount++
}
}
if recentCount > 8 {
t.Fatalf("expected day-proportional capped workload points, got %d recent cpu points", recentCount)
}
}
func TestContract_WorkloadChartsUseCanonicalWorkloadIDsForProviderBackedVMs(t *testing.T) {
monitor, state, history := newTestMonitor(t)
now := time.Now().UTC().Add(-10 * time.Minute).Truncate(time.Minute)
metricID := "vc-1:vm:vm-201"
history.AddGuestMetric(metricID, "cpu", 51, now.Add(-10*time.Minute))
history.AddGuestMetric(metricID, "memory", 64, now.Add(-5*time.Minute))
history.AddGuestMetric(metricID, "disk", 43, now.Add(-3*time.Minute))
history.AddGuestMetric(metricID, "netin", 1200, now.Add(-2*time.Minute))
history.AddGuestMetric(metricID, "netout", 800, now.Add(-2*time.Minute))
adapter := unifiedresources.NewMonitorAdapter(nil)
adapter.PopulateSnapshotAndSupplemental(state.GetSnapshot(), map[unifiedresources.DataSource][]unifiedresources.IngestRecord{
unifiedresources.SourceVMware: {
{
SourceID: metricID,
Resource: unifiedresources.Resource{
ID: "vm-vmware-contract",
Type: unifiedresources.ResourceTypeVM,
Name: "warehouse-api-01",
Status: unifiedresources.StatusOnline,
LastSeen: now,
MetricsTarget: &unifiedresources.MetricsTarget{
ResourceType: "vm",
ResourceID: metricID,
},
VMware: &unifiedresources.VMwareData{
ConnectionID: "vc-1",
EntityType: "vm",
ManagedObjectID: "vm-201",
},
},
},
},
})
setTestUnexportedField(t, monitor, "resourceStore", monitoring.ResourceStoreInterface(adapter))
readState := monitor.GetUnifiedReadStateOrSnapshot()
if readState == nil || len(readState.VMs()) != 1 || readState.VMs()[0] == nil {
t.Fatalf("expected one provider-backed VM in read state, got %+v", readState)
}
resourceID, _, ok := vmChartRequest(readState.VMs()[0])
if !ok {
t.Fatal("expected canonical vm chart request")
}
router := &Router{monitor: monitor}
workloadReq := httptest.NewRequest(http.MethodGet, "/api/charts/workloads?range=1h", nil)
workloadRec := httptest.NewRecorder()
router.handleWorkloadCharts(workloadRec, workloadReq)
if workloadRec.Code != http.StatusOK {
t.Fatalf("expected workload charts 200, got %d: %s", workloadRec.Code, workloadRec.Body.String())
}
var workloadDecoded WorkloadChartsResponse
if err := json.Unmarshal(workloadRec.Body.Bytes(), &workloadDecoded); err != nil {
t.Fatalf("unmarshal workload charts response: %v", err)
}
if _, ok := workloadDecoded.ChartData[resourceID]; !ok {
t.Fatalf("expected workload charts keyed by canonical workload id %q, got %v", resourceID, workloadDecoded.ChartData)
}
if _, ok := workloadDecoded.ChartData[metricID]; ok {
t.Fatalf("expected provider metrics target %q to stay out of workload chart response keys", metricID)
}
if workloadDecoded.GuestTypes[resourceID] != "vm" {
t.Fatalf("expected vm guest type for %q, got %q", resourceID, workloadDecoded.GuestTypes[resourceID])
}
summaryReq := httptest.NewRequest(http.MethodGet, "/api/charts/workloads-summary?range=1h", nil)
summaryRec := httptest.NewRecorder()
router.handleWorkloadsSummaryCharts(summaryRec, summaryReq)
if summaryRec.Code != http.StatusOK {
t.Fatalf("expected workloads summary 200, got %d: %s", summaryRec.Code, summaryRec.Body.String())
}
var summaryDecoded WorkloadsSummaryChartsResponse
if err := json.Unmarshal(summaryRec.Body.Bytes(), &summaryDecoded); err != nil {
t.Fatalf("unmarshal workloads summary response: %v", err)
}
if summaryDecoded.GuestCounts.Total != 1 || summaryDecoded.GuestCounts.Running != 1 {
t.Fatalf("expected stable provider-backed guest counts, got %+v", summaryDecoded.GuestCounts)
}
if len(summaryDecoded.TopContributors.CPU) == 0 {
t.Fatal("expected provider-backed cpu top contributor")
}
if summaryDecoded.TopContributors.CPU[0].ID != resourceID {
t.Fatalf("expected workloads summary contributor id %q, got %+v", resourceID, summaryDecoded.TopContributors.CPU[0])
}
if summaryDecoded.TopContributors.CPU[0].ID == metricID {
t.Fatalf("expected workloads summary contributor id to avoid raw metrics target %q", metricID)
}
}
func TestContract_WorkloadsSummaryChartsNormalizeLongRangeMixedCadence(t *testing.T) {
store := newTestMetricsStore(t)
monitor, state, _ := newTestMonitor(t)
setTestUnexportedField(t, monitor, "metricsStore", store)
state.Nodes = []models.Node{{
ID: "node-contract-1",
Name: "node-contract-1",
Instance: "pve1",
Status: "online",
}}
state.VMs = []models.VM{{
ID: "vm-contract-1",
VMID: 101,
Name: "vm-contract-1",
Node: "node-contract-1",
Instance: "pve1",
Status: "running",
CPU: 0.75,
Memory: models.Memory{Usage: 42.0},
Disk: models.Disk{Usage: 55.0},
NetworkIn: 128,
NetworkOut: 256,
}}
syncTestResourceStore(t, monitor, state)
readState := monitor.GetUnifiedReadStateOrSnapshot()
vms := readState.VMs()
if len(vms) != 1 {
t.Fatalf("expected 1 vm view, got %d", len(vms))
}
sourceID := strings.TrimSpace(vms[0].SourceID())
if sourceID == "" {
t.Fatal("expected vm source ID")
}
now := time.Now().UTC().Add(-10 * time.Minute).Truncate(time.Minute)
windowStart := now.Add(-7 * 24 * time.Hour)
seed := make([]metrics.WriteMetric, 0, 1500)
appendMetric := func(ts time.Time, percentValue, rateValue float64) {
for _, metricType := range []string{"cpu", "memory", "disk"} {
seed = append(seed, metrics.WriteMetric{
ResourceType: "vm",
ResourceID: sourceID,
MetricType: metricType,
Value: percentValue,
Timestamp: ts,
Tier: metrics.TierMinute,
})
}
for _, metricType := range []string{"netin", "netout"} {
seed = append(seed, metrics.WriteMetric{
ResourceType: "vm",
ResourceID: sourceID,
MetricType: metricType,
Value: rateValue,
Timestamp: ts,
Tier: metrics.TierMinute,
})
}
}
for ts := windowStart; ts.Before(now.Add(-24 * time.Hour)); ts = ts.Add(65 * time.Minute) {
appendMetric(ts, 20, 50)
}
for ts := now.Add(-24 * time.Hour); ts.Before(now.Add(-2 * time.Hour)); ts = ts.Add(2 * time.Minute) {
appendMetric(ts, 40, 75)
}
for ts := now.Add(-2 * time.Hour); ts.Before(now); ts = ts.Add(time.Minute) {
appendMetric(ts, 60, 100)
}
appendMetric(now, 75, 125)
store.WriteBatchSync(seed)
router := &Router{monitor: monitor}
req := httptest.NewRequest(http.MethodGet, "/api/charts/workloads-summary?range=7d", nil)
rec := httptest.NewRecorder()
router.handleWorkloadsSummaryCharts(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("expected 200, got %d: %s", rec.Code, rec.Body.String())
}
var decoded WorkloadsSummaryChartsResponse
if err := json.Unmarshal(rec.Body.Bytes(), &decoded); err != nil {
t.Fatalf("unmarshal workloads summary charts response: %v", err)
}
if len(decoded.CPU.P50) == 0 {
t.Fatal("expected normalized workload summary p50 series")
}
if len(decoded.CPU.P50) > workloadsSummaryMaxSeriesPoints {
t.Fatalf("expected workload summary p50 <= %d points, got %d", workloadsSummaryMaxSeriesPoints, len(decoded.CPU.P50))
}
if len(decoded.CPU.P95) > workloadsSummaryMaxSeriesPoints {
t.Fatalf("expected workload summary p95 <= %d points, got %d", workloadsSummaryMaxSeriesPoints, len(decoded.CPU.P95))
}
if decoded.CPU.P50[len(decoded.CPU.P50)-1].Timestamp != now.UnixMilli() {
t.Fatalf("expected latest workload summary timestamp %d, got %d", now.UnixMilli(), decoded.CPU.P50[len(decoded.CPU.P50)-1].Timestamp)
}
recentWindowStart := now.Add(-24 * time.Hour).UnixMilli()
recentCount := 0
for _, point := range decoded.CPU.P50 {
if point.Timestamp >= recentWindowStart {
recentCount++
}
}
if recentCount > 20 {
t.Fatalf("expected day-proportional summary buckets, got %d recent p50 points", recentCount)
}
}
func TestContract_WorkloadChartMetricBudgetGuardrailsRemainCanonical(t *testing.T) {
data, err := os.ReadFile("router.go")
if err != nil {
t.Fatalf("failed to read router.go: %v", err)
}
source := string(data)
requiredSnippets := []string{
`var workloadSummaryMetricOrder = []string{`,
`"cpu",`,
`"memory",`,
`"disk",`,
`"netin",`,
`"netout",`,
`var workloadChartsBatchWG sync.WaitGroup`,
`workloadChartsBatchWG.Add(4)`,
`podBatchMetrics = monitor.GetGuestMetricsForChartBatch("k8s", podRequests, duration, workloadSummaryMetricOrder...)`,
`var workloadsSummaryBatchWG sync.WaitGroup`,
`workloadsSummaryBatchWG.Add(4)`,
`vmBatchMetrics = monitor.GetGuestMetricsForChartBatch("vm", vmRequests, duration, workloadSummaryMetricOrder...)`,
`containerBatchMetrics = monitor.GetGuestMetricsForChartBatch("container", containerRequests, duration, workloadSummaryMetricOrder...)`,
`dockerContainerBatchMetrics = monitor.GetGuestMetricsForChartBatch("dockerContainer", dockerContainerRequests, duration, workloadSummaryMetricOrder...)`,
`summaryChartsCacheTTL = 5 * time.Second`,
`infrastructureChartsCacheKey(req *http.Request, timeRange string, requestedMetricNames []string) string`,
`cachedInfrastructureChartsPayload`,
`cacheInfrastructureChartsPayload`,
`workloadsSummaryChartsCacheKey`,
`cachedWorkloadsSummaryChartsPayload`,
`cacheWorkloadsSummaryChartsPayload`,
`type workloadSummaryMetricBucket struct`,
}
for _, snippet := range requiredSnippets {
if !strings.Contains(source, snippet) {
t.Fatalf("router.go must contain %q", snippet)
}
}
}
func TestContract_GenerateStyledMockSeries_UsesTimestampBasedCurve(t *testing.T) {
now := time.Date(2026, time.March, 31, 12, 0, 0, 0, time.UTC).UnixMilli()
coarse := generateStyledMockSeries(
now,
time.Hour,
7,
51.9,
"dockerContainer",
"orion-2-f54579833f9c",
"memory",
)
fine := generateStyledMockSeries(
now,
time.Hour,
13,
51.9,
"dockerContainer",
"orion-2-f54579833f9c",
"memory",
)
if len(coarse) != 7 || len(fine) != 13 {
t.Fatalf("unexpected synthetic series lengths coarse=%d fine=%d", len(coarse), len(fine))
}
for i, point := range coarse {
fineIndex := i * 2
if fine[fineIndex].Timestamp != point.Timestamp {
t.Fatalf(
"expected shared timestamp at coarse[%d]=%d to match fine[%d]=%d",
i,
point.Timestamp,
fineIndex,
fine[fineIndex].Timestamp,
)
}
if fine[fineIndex].Value != point.Value {
t.Fatalf(
"expected shared timestamp value at coarse[%d]=%f to match fine[%d]=%f",
i,
point.Value,
fineIndex,
fine[fineIndex].Value,
)
}
}
}
func TestContract_PlatformMockToggleRebindsRuntimeConnectionsAndResources(t *testing.T) {
t.Setenv("PULSE_MOCK_MODE", "false")
prevMock := mock.IsMockEnabled()
mock.SetEnabled(false)
t.Cleanup(func() {
mock.SetEnabled(prevMock)
})
cfg := &config.Config{DataPath: t.TempDir()}
monitor, err := monitoring.New(cfg)
if err != nil {
t.Fatalf("new monitor: %v", err)
}
monitor.SetMockMode(false)
router := NewRouter(cfg, monitor, nil, nil, nil, "1.0.0")
t.Cleanup(func() {
router.shutdownBackgroundWorkers()
})
toggleReq := httptest.NewRequest(http.MethodPost, "/api/system/mock-mode", strings.NewReader(`{"enabled":true}`))
toggleRec := httptest.NewRecorder()
router.configHandlers.HandleUpdateMockMode(toggleRec, toggleReq)
if toggleRec.Code != http.StatusOK {
t.Fatalf("toggle status = %d, body=%s", toggleRec.Code, toggleRec.Body.String())
}
truenasListRec := httptest.NewRecorder()
truenasListReq := httptest.NewRequest(http.MethodGet, "/api/truenas/connections", nil)
router.trueNASHandlers.HandleList(truenasListRec, truenasListReq)
if truenasListRec.Code != http.StatusOK {
t.Fatalf("truenas connections status = %d, body=%s", truenasListRec.Code, truenasListRec.Body.String())
}
var truenasConnections []trueNASConnectionResponse
if err := json.Unmarshal(truenasListRec.Body.Bytes(), &truenasConnections); err != nil {
t.Fatalf("decode truenas connections: %v", err)
}
if len(truenasConnections) != 1 || truenasConnections[0].ID != "truenas-mock-1" {
t.Fatalf("expected mock truenas connection, got %#v", truenasConnections)
}
vmwareListRec := httptest.NewRecorder()
vmwareListReq := httptest.NewRequest(http.MethodGet, "/api/vmware/connections", nil)
router.vmwareHandlers.HandleList(vmwareListRec, vmwareListReq)
if vmwareListRec.Code != http.StatusOK {
t.Fatalf("vmware connections status = %d, body=%s", vmwareListRec.Code, vmwareListRec.Body.String())
}
var vmwareConnections []vmwareConnectionResponse
if err := json.Unmarshal(vmwareListRec.Body.Bytes(), &vmwareConnections); err != nil {
t.Fatalf("decode vmware connections: %v", err)
}
if len(vmwareConnections) != 1 || vmwareConnections[0].ID != "vc-mock-1" {
t.Fatalf("expected mock vmware connection, got %#v", vmwareConnections)
}
assertResourceSource := func(path string, wantSource unifiedresources.DataSource) {
t.Helper()
rec := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, path, nil)
router.resourceHandlers.HandleListResources(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("%s status = %d, body=%s", path, rec.Code, rec.Body.String())
}
var resp ResourcesResponse
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
t.Fatalf("decode %s response: %v", path, err)
}
for _, resource := range resp.Data {
for _, source := range resource.Sources {
if source == wantSource {
return
}
}
}
t.Fatalf("expected %s response to include source %q, got %#v", path, wantSource, resp.Data)
}
assertResourceSource("/api/resources?source=truenas", unifiedresources.SourceTrueNAS)
assertResourceSource("/api/resources?source=vmware-vsphere", unifiedresources.SourceVMware)
assertResourceCount := func(path string, want int) {
t.Helper()
rec := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, path, nil)
router.resourceHandlers.HandleListResources(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("%s status = %d, body=%s", path, rec.Code, rec.Body.String())
}
var resp ResourcesResponse
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
t.Fatalf("decode %s response: %v", path, err)
}
if len(resp.Data) != want {
t.Fatalf("%s returned %d resources, want %d", path, len(resp.Data), want)
}
}
assertResourceCount("/api/resources?source=truenas&type=app-container", len(truenas.DefaultFixtures().Apps))
assertResourceCount("/api/resources?source=truenas&type=network-share", len(truenas.DefaultFixtures().Shares))
assertResourceCount("/api/resources?source=vmware-vsphere&type=storage", len(vmware.DefaultFixtures().Datastores))
assertResourceCount("/api/resources?source=vmware-vsphere&type=network", len(vmware.DefaultFixtures().Networks))
}
func TestContract_PlatformMockConnectionListsUseSharedFixtureMetadata(t *testing.T) {
setTrueNASFeatureForTest(t, true)
setVMwareFeatureForTest(t, true)
prevMock := mock.IsMockEnabled()
mock.SetEnabled(true)
t.Cleanup(func() {
mock.SetEnabled(prevMock)
})
t.Run("truenas", func(t *testing.T) {
fixture := mock.DefaultTrueNASConnectionFixture()
if fixture.CollectedAt.IsZero() {
t.Fatal("expected canonical truenas mock fixture timestamp")
}
handler, _, _ := newTrueNASHandlersForTest(t, nil)
req := httptest.NewRequest(http.MethodGet, "/api/truenas/connections", nil)
rec := httptest.NewRecorder()
handler.HandleList(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("expected 200, got %d: %s", rec.Code, rec.Body.String())
}
var responses []trueNASConnectionResponse
if err := json.Unmarshal(rec.Body.Bytes(), &responses); err != nil {
t.Fatalf("decode truenas mock list response: %v", err)
}
if len(responses) != 1 {
t.Fatalf("expected 1 mock truenas connection, got %d", len(responses))
}
response := responses[0]
if response.ID != fixture.ID || response.Name != fixture.Name || response.Host != fixture.Host || response.Port != fixture.Port {
t.Fatalf("unexpected truenas mock connection metadata: got %+v want fixture %+v", response.TrueNASInstance, fixture)
}
if response.APIKey != "********" {
t.Fatalf("expected redacted truenas api key, got %q", response.APIKey)
}
if response.Poll == nil || response.Poll.IntervalSeconds != fixture.PollIntervalSeconds {
t.Fatalf("expected truenas poll interval %d, got %+v", fixture.PollIntervalSeconds, response.Poll)
}
if response.Poll.LastSuccessAt == nil || !response.Poll.LastSuccessAt.Equal(fixture.CollectedAt) {
t.Fatalf("expected truenas last success at %s, got %+v", fixture.CollectedAt.Format(time.RFC3339), response.Poll)
}
if response.Observed == nil {
t.Fatal("expected truenas observed summary")
}
if response.Observed.Host != fixture.Host ||
response.Observed.ResourceID != fixture.ResourceID ||
response.Observed.Systems != fixture.Systems ||
response.Observed.StoragePools != fixture.StoragePools ||
response.Observed.Datasets != fixture.Datasets ||
response.Observed.Apps != fixture.Apps ||
response.Observed.Disks != fixture.Disks ||
response.Observed.RecoveryArtifacts != fixture.RecoveryArtifacts {
t.Fatalf("unexpected truenas observed summary: got %+v want fixture %+v", response.Observed, fixture)
}
if response.Observed.CollectedAt == nil || !response.Observed.CollectedAt.Equal(fixture.CollectedAt) {
t.Fatalf("expected truenas observed collectedAt %s, got %+v", fixture.CollectedAt.Format(time.RFC3339), response.Observed)
}
})
t.Run("vmware", func(t *testing.T) {
fixture := mock.DefaultVMwareConnectionFixture()
if fixture.CollectedAt.IsZero() {
t.Fatal("expected canonical vmware mock fixture timestamp")
}
handler, _ := newVMwareHandlersForTest(t)
req := httptest.NewRequest(http.MethodGet, "/api/vmware/connections", nil)
rec := httptest.NewRecorder()
handler.HandleList(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("expected 200, got %d: %s", rec.Code, rec.Body.String())
}
var responses []vmwareConnectionResponse
if err := json.Unmarshal(rec.Body.Bytes(), &responses); err != nil {
t.Fatalf("decode vmware mock list response: %v", err)
}
if len(responses) != 1 {
t.Fatalf("expected 1 mock vmware connection, got %d", len(responses))
}
response := responses[0]
if response.ID != fixture.ID || response.Name != fixture.Name || response.Host != fixture.Host || response.Port != fixture.Port || response.Username != fixture.Username {
t.Fatalf("unexpected vmware mock connection metadata: got %+v want fixture %+v", response.VMwareVCenterInstance, fixture)
}
if response.Password != "********" {
t.Fatalf("expected redacted vmware password, got %q", response.Password)
}
if response.Poll == nil || response.Poll.IntervalSeconds != fixture.PollIntervalSeconds {
t.Fatalf("expected vmware poll interval %d, got %+v", fixture.PollIntervalSeconds, response.Poll)
}
if response.Poll.LastSuccessAt == nil || !response.Poll.LastSuccessAt.Equal(fixture.CollectedAt) {
t.Fatalf("expected vmware last success at %s, got %+v", fixture.CollectedAt.Format(time.RFC3339), response.Poll)
}
if response.Observed == nil {
t.Fatal("expected vmware observed summary")
}
if response.Observed.Hosts != fixture.Hosts ||
response.Observed.VMs != fixture.VMs ||
response.Observed.Datastores != fixture.Datastores ||
response.Observed.VIRelease != fixture.VIRelease {
t.Fatalf("unexpected vmware observed summary: got %+v want fixture %+v", response.Observed, fixture)
}
if response.Observed.CollectedAt == nil || !response.Observed.CollectedAt.Equal(fixture.CollectedAt) {
t.Fatalf("expected vmware observed collectedAt %s, got %+v", fixture.CollectedAt.Format(time.RFC3339), response.Observed)
}
})
}
func TestContract_VMwareConnectionListCarriesDegradedObservedSummary(t *testing.T) {
setVMwareFeatureForTest(t, true)
connection := config.VMwareVCenterInstance{
ID: "conn-1",
Name: "lab-vcenter",
Host: "vcsa.lab.local",
Port: 443,
Username: "administrator@vsphere.local",
Password: "super-secret",
Enabled: true,
}
handler, persistence := newVMwareHandlersForTest(t)
if err := persistence.SaveVMwareConfig([]config.VMwareVCenterInstance{connection}); err != nil {
t.Fatalf("seed vmware config: %v", err)
}
collectedAt := time.Date(2026, 3, 31, 18, 15, 0, 0, time.UTC)
handler.statusMu.Lock()
handler.statuses = map[string]vmwareConnectionRuntimeStatus{
connection.ID: {
Poll: &monitoring.VMwareConnectionPollStatus{
IntervalSeconds: 60,
LastSuccessAt: &collectedAt,
},
Observed: &monitoring.VMwareConnectionObservedSummary{
CollectedAt: &collectedAt,
Hosts: 4,
VMs: 24,
Datastores: 6,
VIRelease: "8.0.3",
Degraded: true,
IssueCount: 3,
Issues: []monitoring.VMwareConnectionObservedIssue{
{
Stage: "signals",
Category: "permission",
Message: "VMware permissions are insufficient for HostSystem overall status",
Occurrences: 2,
},
{
Stage: "topology",
Category: "unavailable",
Message: "VMware vm guest identity is temporarily unavailable",
Occurrences: 1,
},
},
},
},
}
handler.statusMu.Unlock()
req := httptest.NewRequest(http.MethodGet, "/api/vmware/connections", nil)
rec := httptest.NewRecorder()
handler.HandleList(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("expected 200, got %d: %s", rec.Code, rec.Body.String())
}
var responses []vmwareConnectionResponse
if err := json.Unmarshal(rec.Body.Bytes(), &responses); err != nil {
t.Fatalf("unmarshal vmware degraded list response: %v", err)
}
if len(responses) != 1 || responses[0].Observed == nil {
t.Fatalf("expected 1 vmware connection with degraded observed summary, got %+v", responses)
}
if !responses[0].Observed.Degraded {
t.Fatalf("expected degraded observed summary, got %+v", responses[0].Observed)
}
if responses[0].Observed.IssueCount != 3 {
t.Fatalf("observed issueCount = %d, want 3", responses[0].Observed.IssueCount)
}
if len(responses[0].Observed.Issues) != 2 {
t.Fatalf("observed issues len = %d, want 2", len(responses[0].Observed.Issues))
}
if responses[0].Observed.Issues[0].Stage != "signals" || responses[0].Observed.Issues[0].Occurrences != 2 {
t.Fatalf("unexpected first observed issue: %+v", responses[0].Observed.Issues[0])
}
if responses[0].Observed.Issues[1].Stage != "topology" || responses[0].Observed.Issues[1].Occurrences != 1 {
t.Fatalf("unexpected second observed issue: %+v", responses[0].Observed.Issues[1])
}
}
func TestContract_SSOTestRejectsMetadataURLWithUserinfo(t *testing.T) {
called := make(chan struct{}, 1)
metadataServer := newIPv4HTTPServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
select {
case called <- struct{}{}:
default:
}
w.Header().Set("Content-Type", "application/xml")
w.Write([]byte(testSAMLMetadata))
}))
defer metadataServer.Close()
metadataURL, err := url.Parse(metadataServer.URL)
if err != nil {
t.Fatalf("parse metadata server url: %v", err)
}
metadataURL.User = url.UserPassword("user", "pass")
reqBody := SSOTestRequest{
Type: "saml",
SAML: &SAMLTestConfig{
IDPMetadataURL: metadataURL.String(),
},
}
body, err := json.Marshal(reqBody)
if err != nil {
t.Fatalf("marshal request: %v", err)
}
req := httptest.NewRequest(http.MethodPost, "/api/security/sso/providers/test", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
setTestIP(req)
rec := httptest.NewRecorder()
router := &Router{}
router.handleTestSSOProvider(rec, req)
if rec.Code != http.StatusBadRequest {
t.Fatalf("status=%d, want %d: %s", rec.Code, http.StatusBadRequest, rec.Body.String())
}
var resp SSOTestResponse
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
t.Fatalf("unmarshal response: %v", err)
}
if resp.Success {
t.Fatalf("expected failed response, got success: %+v", resp)
}
select {
case <-called:
t.Fatal("expected SAML metadata URL with userinfo to be rejected before outbound fetch")
default:
}
}
func TestContract_SSOTestRejectsManualSLOURLWithUserinfo(t *testing.T) {
reqBody := SSOTestRequest{
Type: "saml",
SAML: &SAMLTestConfig{
IDPSSOURL: "https://idp.example.com/sso",
IDPSLOURL: "https://user:pass@idp.example.com/slo",
},
}
body, err := json.Marshal(reqBody)
if err != nil {
t.Fatalf("marshal request: %v", err)
}
req := httptest.NewRequest(http.MethodPost, "/api/security/sso/providers/test", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
setTestIP(req)
rec := httptest.NewRecorder()
router := &Router{}
router.handleTestSSOProvider(rec, req)
if rec.Code != http.StatusBadRequest {
t.Fatalf("status=%d, want %d: %s", rec.Code, http.StatusBadRequest, rec.Body.String())
}
var resp SSOTestResponse
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
t.Fatalf("unmarshal response: %v", err)
}
if resp.Success {
t.Fatalf("expected failed response, got success: %+v", resp)
}
if resp.Message != "Invalid SLO URL format" {
t.Fatalf("expected invalid SLO URL message, got %+v", resp)
}
}
func TestContract_SSOTestOIDCDiscoveryKeepsIssuerBasePath(t *testing.T) {
var issuerURL string
discoveryServer := newIPv4HTTPServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/auth/realms/pulse/.well-known/openid-configuration" {
http.NotFound(w, r)
return
}
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `{
"issuer": %q,
"authorization_endpoint": %q,
"token_endpoint": %q,
"userinfo_endpoint": %q,
"jwks_uri": %q,
"scopes_supported": ["openid", "profile", "email"]
}`, issuerURL, issuerURL+"/protocol/openid-connect/auth", issuerURL+"/protocol/openid-connect/token", issuerURL+"/protocol/openid-connect/userinfo", issuerURL+"/protocol/openid-connect/certs")
}))
defer discoveryServer.Close()
issuerURL = discoveryServer.URL + "/auth/realms/pulse"
reqBody := SSOTestRequest{
Type: "oidc",
OIDC: &OIDCTestConfig{
IssuerURL: issuerURL,
},
}
body, err := json.Marshal(reqBody)
if err != nil {
t.Fatalf("marshal request: %v", err)
}
req := httptest.NewRequest(http.MethodPost, "/api/security/sso/providers/test", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
setTestIP(req)
rec := httptest.NewRecorder()
router := &Router{}
router.handleTestSSOProvider(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status=%d, want %d: %s", rec.Code, http.StatusOK, rec.Body.String())
}
var resp SSOTestResponse
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
t.Fatalf("unmarshal response: %v", err)
}
if !resp.Success {
t.Fatalf("expected success response, got %+v", resp)
}
if resp.Details == nil {
t.Fatal("expected OIDC details in response")
}
if resp.Details.EntityID != issuerURL {
t.Fatalf("issuer=%q, want %q", resp.Details.EntityID, issuerURL)
}
}
func TestContract_SSOTestRejectsCrossOriginSAMLMetadataRedirect(t *testing.T) {
targetCalled := make(chan struct{}, 1)
target := newIPv4HTTPServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
select {
case targetCalled <- struct{}{}:
default:
}
w.Header().Set("Content-Type", "application/xml")
w.Write([]byte(testSAMLMetadata))
}))
defer target.Close()
origin := newIPv4HTTPServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, target.URL+r.URL.Path, http.StatusFound)
}))
defer origin.Close()
reqBody := SSOTestRequest{
Type: "saml",
SAML: &SAMLTestConfig{
IDPMetadataURL: origin.URL + "/metadata",
},
}
body, err := json.Marshal(reqBody)
if err != nil {
t.Fatalf("marshal request: %v", err)
}
req := httptest.NewRequest(http.MethodPost, "/api/security/sso/providers/test", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
setTestIP(req)
rec := httptest.NewRecorder()
router := &Router{}
router.handleTestSSOProvider(rec, req)
if rec.Code != http.StatusBadRequest {
t.Fatalf("status=%d, want %d: %s", rec.Code, http.StatusBadRequest, rec.Body.String())
}
select {
case <-targetCalled:
t.Fatal("expected cross-origin SAML redirect to be rejected before fetching the target origin")
default:
}
}
func TestContract_SSOTestRejectsCrossOriginOIDCDiscoveryRedirect(t *testing.T) {
targetCalled := make(chan struct{}, 1)
var targetURL string
target := newIPv4HTTPServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/.well-known/openid-configuration" {
http.NotFound(w, r)
return
}
select {
case targetCalled <- struct{}{}:
default:
}
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `{
"issuer": %q,
"authorization_endpoint": %q,
"token_endpoint": %q,
"userinfo_endpoint": %q,
"jwks_uri": %q,
"scopes_supported": ["openid", "profile", "email"]
}`, targetURL, targetURL+"/auth", targetURL+"/token", targetURL+"/userinfo", targetURL+"/jwks")
}))
defer target.Close()
targetURL = target.URL
origin := newIPv4HTTPServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/.well-known/openid-configuration" {
http.NotFound(w, r)
return
}
http.Redirect(w, r, target.URL+r.URL.Path, http.StatusFound)
}))
defer origin.Close()
reqBody := SSOTestRequest{
Type: "oidc",
OIDC: &OIDCTestConfig{
IssuerURL: origin.URL,
},
}
body, err := json.Marshal(reqBody)
if err != nil {
t.Fatalf("marshal request: %v", err)
}
req := httptest.NewRequest(http.MethodPost, "/api/security/sso/providers/test", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
setTestIP(req)
rec := httptest.NewRecorder()
router := &Router{}
router.handleTestSSOProvider(rec, req)
if rec.Code != http.StatusBadRequest {
t.Fatalf("status=%d, want %d: %s", rec.Code, http.StatusBadRequest, rec.Body.String())
}
select {
case <-targetCalled:
t.Fatal("expected cross-origin OIDC redirect to be rejected before fetching the target origin")
default:
}
}
func TestContract_SSOLocalRedirectTargetsStayCanonical(t *testing.T) {
if got := buildLocalRedirectTarget("https://evil.example.com/pwn", map[string]string{
"oidc": "error",
}); got != "/?oidc=error" {
t.Fatalf("absolute redirect target = %q, want %q", got, "/?oidc=error")
}
if got := buildLocalRedirectTarget("/login?foo=bar#section", map[string]string{
"saml": "success",
}); got != "/login?foo=bar&saml=success#section" {
t.Fatalf("local redirect target = %q, want %q", got, "/login?foo=bar&saml=success#section")
}
}
func TestContract_SAMLLoginRejectsUnsupportedMethods(t *testing.T) {
router := newSAMLRouter(t, testSAMLProvider("okta", true))
req := httptest.NewRequest(http.MethodPut, "/api/saml/okta/login", nil)
rec := httptest.NewRecorder()
router.handleSAMLLogin(rec, req)
if rec.Code != http.StatusMethodNotAllowed {
t.Fatalf("status=%d, want %d: %s", rec.Code, http.StatusMethodNotAllowed, rec.Body.String())
}
}
func TestContract_FindingJSONSnapshot(t *testing.T) {
now := time.Date(2026, 2, 8, 13, 14, 15, 0, time.UTC)
lastSeen := now.Add(5 * time.Minute)
resolvedAt := now.Add(10 * time.Minute)
ackAt := now.Add(11 * time.Minute)
snoozedUntil := now.Add(12 * time.Minute)
lastInvestigated := now.Add(15 * time.Minute)
lastRegression := now.Add(30 * time.Minute)
payload := ai.Finding{
ID: "finding-1",
Key: "cpu-high",
Severity: ai.FindingSeverityCritical,
Category: ai.FindingCategoryPerformance,
ResourceID: "vm-100",
ResourceName: "web-server",
ResourceType: "vm",
Node: "pve-1",
Title: "High CPU usage",
Description: "CPU sustained above 95%",
Recommendation: "Investigate processes and load",
Evidence: "cpu=96%",
Source: "ai-analysis",
DetectedAt: now,
LastSeenAt: lastSeen,
ResolvedAt: &resolvedAt,
AutoResolved: true,
ResolveReason: "No longer detected",
AcknowledgedAt: &ackAt,
SnoozedUntil: &snoozedUntil,
AlertIdentifier: "alert-1",
DismissedReason: "expected_behavior",
UserNote: "Runs nightly batch",
TimesRaised: 4,
Suppressed: true,
InvestigationSessionID: "inv-session-1",
InvestigationStatus: "completed",
InvestigationOutcome: "fix_queued",
LastInvestigatedAt: &lastInvestigated,
InvestigationAttempts: 2,
InvestigationRecord: &aicontracts.InvestigationRecord{
ID: "investigation-1",
FindingID: "finding-1",
Subject: aicontracts.InvestigationRecordSubject{
ResourceID: "vm-100",
ResourceName: "web-server",
ResourceType: "vm",
Node: "pve-1",
},
Trigger: aicontracts.InvestigationRecordTrigger{
FindingKey: "cpu-high",
Source: "ai-analysis",
Severity: "critical",
Category: "performance",
Title: "High CPU usage",
DetectedAt: now,
Description: "CPU sustained above 95%",
},
Status: aicontracts.InvestigationStatusCompleted,
Outcome: aicontracts.OutcomeFixQueued,
Confidence: aicontracts.InvestigationRecordConfidenceMedium,
Evidence: []aicontracts.InvestigationRecordEvidence{{Kind: "finding_evidence", Summary: "cpu=96%"}},
Conclusion: "CPU sustained above 95%",
RecommendedAction: "Investigate processes and load",
Verification: []string{},
Rollback: []string{},
ToolsUsed: []string{"ssh.exec"},
StartedAt: now,
ApprovalID: "approval-1",
},
LoopState: "remediation_planned",
Lifecycle: []ai.FindingLifecycleEvent{
{
At: now,
Type: "state_change",
Message: "Moved to investigating",
From: "detected",
To: "investigating",
Metadata: map[string]string{
"from": "detected",
"to": "investigating",
},
},
},
RegressionCount: 1,
LastRegressionAt: &lastRegression,
}
got, err := json.Marshal(payload)
if err != nil {
t.Fatalf("marshal finding: %v", err)
}
const want = `{
"id":"finding-1",
"key":"cpu-high",
"severity":"critical",
"category":"performance",
"resource_id":"vm-100",
"resource_name":"web-server",
"resource_type":"vm",
"node":"pve-1",
"title":"High CPU usage",
"description":"CPU sustained above 95%",
"recommendation":"Investigate processes and load",
"evidence":"cpu=96%",
"source":"ai-analysis",
"detected_at":"2026-02-08T13:14:15Z",
"last_seen_at":"2026-02-08T13:19:15Z",
"resolved_at":"2026-02-08T13:24:15Z",
"auto_resolved":true,
"resolve_reason":"No longer detected",
"acknowledged_at":"2026-02-08T13:25:15Z",
"snoozed_until":"2026-02-08T13:26:15Z",
"alert_identifier":"alert-1",
"dismissed_reason":"expected_behavior",
"user_note":"Runs nightly batch",
"times_raised":4,
"suppressed":true,
"investigation_session_id":"inv-session-1",
"investigation_status":"completed",
"investigation_outcome":"fix_queued",
"last_investigated_at":"2026-02-08T13:29:15Z",
"investigation_attempts":2,
"investigation_record":{"id":"investigation-1","finding_id":"finding-1","subject":{"resource_id":"vm-100","resource_name":"web-server","resource_type":"vm","node":"pve-1"},"trigger":{"finding_key":"cpu-high","source":"ai-analysis","severity":"critical","category":"performance","title":"High CPU usage","detected_at":"2026-02-08T13:14:15Z","description":"CPU sustained above 95%"},"status":"completed","outcome":"fix_queued","confidence":"medium","evidence":[{"kind":"finding_evidence","summary":"cpu=96%"}],"conclusion":"CPU sustained above 95%","recommended_action":"Investigate processes and load","verification":[],"rollback":[],"tools_used":["ssh.exec"],"started_at":"2026-02-08T13:14:15Z","approval_id":"approval-1"},
"loop_state":"remediation_planned",
"lifecycle":[{"at":"2026-02-08T13:14:15Z","type":"state_change","message":"Moved to investigating","from":"detected","to":"investigating","metadata":{"from":"detected","to":"investigating"}}],
"regression_count":1,
"last_regression_at":"2026-02-08T13:44:15Z"
}`
assertJSONSnapshot(t, got, want)
}
// TestContract_PatrolStatusTrustJSONSnapshot pins the canonical JSON shape
// for the trust snapshot block carried on the patrol-status response. The
// PatrolStatusResponse.Trust field surfaces FindingsTrustSummary on the
// operator-facing Patrol page so trust signals (fix verified, dismissed as
// noise, etc.) are scannable without expanding individual findings.
func TestContract_PatrolStatusTrustJSONSnapshot(t *testing.T) {
trust := ai.FindingsTrustSummary{
Tracked: 4,
CurrentlyActive: 1,
Resolved: 2,
AutoResolved: 1,
FixVerified: 1,
FixFailed: 0,
DismissedAsNoise: 1,
DismissedAsExpected: 0,
DismissedAsLater: 0,
Suppressed: 0,
RegressedAtLeastOnce: 1,
}
got, err := json.Marshal(trust)
if err != nil {
t.Fatalf("marshal trust summary: %v", err)
}
want := `{"tracked":4,"currently_active":1,"resolved":2,"auto_resolved":1,"fix_verified":1,"fix_failed":0,"dismissed_as_noise":1,"dismissed_as_expected":0,"dismissed_as_later":0,"suppressed":0,"regressed_at_least_once":1}`
if string(got) != want {
t.Fatalf("trust summary JSON drift:\n got: %s\nwant: %s", got, want)
}
}
// TestContract_UnifiedFindingPreviousResolvedFixSummaryJSONSnapshot pins the
// canonical JSON shape for the operational-memory field that preserves the
// prior successful fix summary across regressions. The Finding to
// UnifiedFinding conversion in the API router must emit this field under
// the canonical "previous_resolved_fix_summary" key so Assistant context
// and operator surfaces can reason about "what worked last time."
func TestContract_UnifiedFindingPreviousResolvedFixSummaryJSONSnapshot(t *testing.T) {
finding := unified.UnifiedFinding{
ID: "uf-regress",
Source: unified.SourceAIPatrol,
Severity: unified.SeverityWarning,
Category: unified.CategoryReliability,
ResourceID: "vm-100",
Title: "Service stalled",
Description: "Service stopped responding again",
PreviousResolvedFixSummary: "Restart the workload service after backup window clears",
DetectedAt: time.Date(2026, 5, 8, 12, 0, 0, 0, time.UTC),
LastSeenAt: time.Date(2026, 5, 8, 12, 5, 0, 0, time.UTC),
}
got, err := json.Marshal(finding)
if err != nil {
t.Fatalf("marshal unified finding: %v", err)
}
if !strings.Contains(string(got), `"previous_resolved_fix_summary":"Restart the workload service after backup window clears"`) {
t.Fatalf("expected previous_resolved_fix_summary field in canonical UnifiedFinding payload, got %s", got)
}
}
// TestContract_UnifiedFindingImpactJSONSnapshot pins the canonical JSON shape
// for UnifiedFinding payloads carrying the operator-facing Impact field. The
// AI engine populates Finding.Impact at detection time and the Finding to
// UnifiedFinding conversion in the API router must preserve that string under
// the canonical "impact" key so the operator-visible card shows
// consequence-if-ignored copy.
func TestContract_UnifiedFindingImpactJSONSnapshot(t *testing.T) {
now := time.Date(2026, 2, 8, 13, 14, 15, 0, time.UTC)
finding := unified.UnifiedFinding{
ID: "uf-impact",
Source: unified.SourceAIPatrol,
Severity: unified.SeverityWarning,
Category: unified.CategoryReliability,
ResourceID: "patrol-runtime",
ResourceName: "Pulse Patrol Service",
ResourceType: "service",
Title: "Pulse Patrol: Provider connection issue",
Description: "Pulse Patrol could not maintain a healthy connection to the configured provider during analysis.",
Impact: "While Patrol cannot analyze, alerts continue to fire without evidence or recommended actions, and AI Intelligence summaries cannot refresh.",
Recommendation: "Check provider reachability, base URL, firewall or proxy rules, and provider availability, then rerun Patrol.",
DetectedAt: now,
LastSeenAt: now,
}
got, err := json.Marshal(finding)
if err != nil {
t.Fatalf("marshal unified finding: %v", err)
}
if !strings.Contains(string(got), `"impact":"While Patrol cannot analyze`) {
t.Fatalf("expected impact field in canonical UnifiedFinding payload, got %s", got)
}
}
func TestContract_ApprovalJSONSnapshot(t *testing.T) {
now := time.Date(2026, 2, 8, 13, 14, 15, 0, time.UTC)
expires := now.Add(5 * time.Minute)
decided := now.Add(2 * time.Minute)
payload := approval.ApprovalRequest{
ID: "approval-1",
ExecutionID: "exec-1",
ToolID: "tool-1",
Command: "rm -rf /tmp/cache",
TargetType: "agent",
TargetID: "host-1",
TargetName: "alpha",
Context: "Cleanup temporary cache",
RequestedBy: approval.RequesterPulsePatrol,
RiskLevel: approval.RiskHigh,
Status: approval.StatusApproved,
RequestedAt: now,
ExpiresAt: expires,
DecidedAt: &decided,
DecidedBy: "admin",
DenyReason: "not needed",
CommandHash: "sha256:abc",
Consumed: true,
}
got, err := json.Marshal(payload)
if err != nil {
t.Fatalf("marshal approval: %v", err)
}
const want = `{
"id":"approval-1",
"executionId":"exec-1",
"toolId":"tool-1",
"command":"rm -rf /tmp/cache",
"targetType":"agent",
"targetId":"host-1",
"targetName":"alpha",
"context":"Cleanup temporary cache",
"requestedBy":"pulse_patrol",
"riskLevel":"high",
"status":"approved",
"requestedAt":"2026-02-08T13:14:15Z",
"expiresAt":"2026-02-08T13:19:15Z",
"decidedAt":"2026-02-08T13:16:15Z",
"decidedBy":"admin",
"denyReason":"not needed",
"commandHash":"sha256:abc",
"consumed":true
}`
assertJSONSnapshot(t, got, want)
}
func TestContract_ApprovalListResponseJSONSnapshot(t *testing.T) {
payload := map[string]any{
"approvals": []approval.ApprovalRequest{},
"stats": map[string]int{
"approved": 0,
"denied": 0,
"executions": 0,
"expired": 0,
"pending": 0,
},
}
got, err := json.Marshal(payload)
if err != nil {
t.Fatalf("marshal approval list response: %v", err)
}
const want = `{
"approvals":[],
"stats":{
"approved":0,
"denied":0,
"executions":0,
"expired":0,
"pending":0
}
}`
assertJSONSnapshot(t, got, want)
}
func TestContract_HostedSignupResponseJSONSnapshot(t *testing.T) {
payload := hostedSignupResponse{
Message: hostedSignupAcceptedMessage,
}
got, err := json.Marshal(payload)
if err != nil {
t.Fatalf("marshal hosted signup response: %v", err)
}
if strings.Contains(string(got), "user_id") || strings.Contains(string(got), "org_id") {
t.Fatalf("hosted signup accepted response must not expose identity fields: %s", got)
}
const want = `{
"message":"If that email can finish signup, you'll receive a Pulse Account sign-in link shortly."
}`
assertJSONSnapshot(t, got, want)
}
func TestContract_BillingStateQuickstartJSONSnapshot(t *testing.T) {
grantedAt := time.Date(2026, 3, 25, 14, 30, 0, 0, time.UTC).Unix()
payload := billingState{
Capabilities: []string{"ai_autofix", "ai_patrol"},
Limits: map[string]int64{"max_monitored_systems": 25},
MetersEnabled: []string{},
PlanVersion: "cloud_starter",
SubscriptionState: subscriptionStateActiveValue,
QuickstartCreditsGranted: true,
QuickstartCreditsUsed: 3,
QuickstartCreditsGrantedAt: &grantedAt,
}
got, err := json.Marshal(payload)
if err != nil {
t.Fatalf("marshal billing state: %v", err)
}
const want = `{
"capabilities":["ai_autofix","ai_patrol"],
"limits":{"max_monitored_systems":25},
"meters_enabled":[],
"plan_version":"cloud_starter",
"subscription_state":"active",
"quickstart_credits_granted":true,
"quickstart_credits_used":3,
"quickstart_credits_granted_at":1774449000
}`
assertJSONSnapshot(t, got, want)
}
func TestContract_HostedAISettingsDoesNotAutoBootstrapQuickstartJSONSnapshot(t *testing.T) {
t.Setenv("PULSE_HOSTED_MODE", "true")
baseDir := t.TempDir()
mtp := config.NewMultiTenantPersistence(baseDir)
persistence, err := mtp.GetPersistence("default")
if err != nil {
t.Fatalf("GetPersistence(default): %v", err)
}
seedHostedAIBillingState(t, mtp, "default")
handler := NewAISettingsHandler(mtp, nil, nil)
handler.defaultConfig = &config.Config{DataPath: baseDir}
req := httptest.NewRequest(http.MethodGet, "/api/settings/ai", nil)
rec := httptest.NewRecorder()
handler.HandleGetAISettings(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status=%d, want %d: %s", rec.Code, http.StatusOK, rec.Body.String())
}
if persistence.HasAIConfig() {
t.Fatal("expected hosted AI settings contract not to persist implicit quickstart bootstrap")
}
const want = `{
"enabled":false,
"model":"",
"configured":false,
"custom_context":"",
"auth_method":"api_key",
"oauth_connected":false,
"patrol_interval_minutes":360,
"patrol_enabled":true,
"patrol_auto_fix":false,
"alert_triggered_analysis":true,
"patrol_event_triggers_enabled":true,
"patrol_alert_triggers_enabled":true,
"patrol_anomaly_triggers_enabled":true,
"use_proactive_thresholds":false,
"available_models":[],
"anthropic_configured":false,
"openai_configured":false,
"openrouter_configured":false,
"deepseek_configured":false,
"gemini_configured":false,
"ollama_configured":false,
"ollama_base_url":"http://localhost:11434",
"ollama_password_set":false,
"configured_providers":[],
"control_level":"read_only",
"protected_guests":[],
"discovery_enabled":false,
"patrol_readiness":{"status":"not_ready","ready":false,"cause":"service_unavailable","summary":"Pulse Patrol service is not available.","checks":[{"id":"service","status":"not_ready","cause":"service_unavailable","label":"Patrol service","message":"Pulse Patrol service is not available.","action":"restart_service"}]}
}`
assertJSONSnapshot(t, rec.Body.Bytes(), want)
}
func TestContract_AISettingsRetiredQuickstartAliasJSONSnapshot(t *testing.T) {
tmp := t.TempDir()
cfg := &config.Config{DataPath: tmp}
persistence := config.NewConfigPersistence(tmp)
aiCfg := config.NewDefaultAIConfig()
aiCfg.Enabled = true
aiCfg.Model = "quickstart:minimax-2.5m"
aiCfg.ChatModel = "quickstart:minimax-2.5m"
aiCfg.PatrolModel = "quickstart:minimax-2.5m"
aiCfg.DiscoveryModel = "quickstart:minimax-2.5m"
aiCfg.AutoFixModel = "quickstart:minimax-2.5m"
if err := persistence.SaveAIConfig(*aiCfg); err != nil {
t.Fatalf("SaveAIConfig: %v", err)
}
handler := newTestAISettingsHandler(cfg, persistence, nil)
req := httptest.NewRequest(http.MethodGet, "/api/settings/ai", nil)
rec := httptest.NewRecorder()
handler.HandleGetAISettings(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status=%d, want %d: %s", rec.Code, http.StatusOK, rec.Body.String())
}
const want = `{
"enabled":true,
"model":"",
"configured":false,
"custom_context":"",
"auth_method":"api_key",
"oauth_connected":false,
"patrol_interval_minutes":360,
"patrol_enabled":true,
"patrol_auto_fix":false,
"alert_triggered_analysis":true,
"patrol_event_triggers_enabled":true,
"patrol_alert_triggers_enabled":true,
"patrol_anomaly_triggers_enabled":true,
"use_proactive_thresholds":false,
"available_models":[],
"anthropic_configured":false,
"openai_configured":false,
"openrouter_configured":false,
"deepseek_configured":false,
"gemini_configured":false,
"ollama_configured":false,
"ollama_base_url":"http://localhost:11434",
"ollama_password_set":false,
"configured_providers":[],
"control_level":"read_only",
"protected_guests":[],
"discovery_enabled":false,
"patrol_readiness":{"status":"not_ready","ready":false,"cause":"service_unavailable","summary":"Pulse Patrol service is not available.","checks":[{"id":"service","status":"not_ready","cause":"service_unavailable","label":"Patrol service","message":"Pulse Patrol service is not available.","action":"restart_service"}]}
}`
assertJSONSnapshot(t, rec.Body.Bytes(), want)
if bytes.Contains(bytes.ToLower(rec.Body.Bytes()), []byte("quickstart")) {
t.Fatalf("expected AI settings payload to suppress legacy hosted quickstart aliases, got %s", rec.Body.Bytes())
}
}
func TestContract_AISettingsOllamaAuthJSONSnapshot(t *testing.T) {
tmp := t.TempDir()
cfg := &config.Config{DataPath: tmp}
persistence := config.NewConfigPersistence(tmp)
aiCfg := config.NewDefaultAIConfig()
aiCfg.Enabled = true
aiCfg.Model = "openai:gpt-4o"
aiCfg.PatrolModel = "ollama:llama3"
aiCfg.OllamaBaseURL = "http://ollama.example:11434"
aiCfg.OllamaUsername = "unai"
aiCfg.OllamaPassword = "secret"
if err := persistence.SaveAIConfig(*aiCfg); err != nil {
t.Fatalf("SaveAIConfig: %v", err)
}
handler := newTestAISettingsHandler(cfg, persistence, nil)
req := httptest.NewRequest(http.MethodGet, "/api/settings/ai", nil)
rec := httptest.NewRecorder()
handler.HandleGetAISettings(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status=%d, want %d: %s", rec.Code, http.StatusOK, rec.Body.String())
}
const want = `{
"enabled":true,
"model":"ollama:llama3",
"patrol_model":"ollama:llama3",
"configured":true,
"custom_context":"",
"auth_method":"api_key",
"oauth_connected":false,
"patrol_interval_minutes":360,
"patrol_enabled":true,
"patrol_auto_fix":false,
"alert_triggered_analysis":true,
"patrol_event_triggers_enabled":true,
"patrol_alert_triggers_enabled":true,
"patrol_anomaly_triggers_enabled":true,
"use_proactive_thresholds":false,
"available_models":[],
"anthropic_configured":false,
"openai_configured":false,
"openrouter_configured":false,
"deepseek_configured":false,
"gemini_configured":false,
"ollama_configured":true,
"ollama_base_url":"http://ollama.example:11434",
"ollama_username":"unai",
"ollama_password_set":true,
"configured_providers":["ollama"],
"control_level":"read_only",
"protected_guests":[],
"discovery_enabled":false,
"patrol_readiness":{"status":"not_ready","ready":false,"cause":"service_unavailable","summary":"Pulse Patrol service is not available.","checks":[{"id":"service","status":"not_ready","cause":"service_unavailable","label":"Patrol service","message":"Pulse Patrol service is not available.","action":"restart_service"}]}
}`
assertJSONSnapshot(t, rec.Body.Bytes(), want)
}
func TestContract_VMInventoryExportCSVHeaders(t *testing.T) {
handler := NewReportingHandlers(nil, nil)
req := httptest.NewRequest(http.MethodGet, "/api/admin/reports/inventory/vms/export?format=csv", nil)
rec := httptest.NewRecorder()
handler.HandleExportVMInventory(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("expected 200, got %d body=%s", rec.Code, rec.Body.String())
}
if got := rec.Header().Get("Content-Type"); got != "text/csv; charset=utf-8" {
t.Fatalf("expected csv content type, got %q", got)
}
if got := rec.Header().Get("Content-Disposition"); !strings.HasPrefix(got, "attachment; filename=\"vm-inventory-") {
t.Fatalf("expected VM inventory attachment filename, got %q", got)
}
const want = "Resource ID,Instance,Node,Pool,VMID,VM Name,Status,CPU Cores,Memory Allocated Bytes,Disk Allocated Bytes,Disk Used Bytes,Disk Status Reason\n"
if got := rec.Body.String(); got != want {
t.Fatalf("unexpected VM inventory CSV header row:\nwant %q\ngot %q", want, got)
}
}
func TestContract_ReportingCatalogJSONSnapshot(t *testing.T) {
handler := NewReportingHandlers(nil, nil)
req := httptest.NewRequest(http.MethodGet, "/api/admin/reports/catalog", nil)
rec := httptest.NewRecorder()
handler.HandleGetReportingCatalog(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("expected 200, got %d body=%s", rec.Code, rec.Body.String())
}
if got := rec.Header().Get("Content-Type"); got != "application/json" {
t.Fatalf("expected json content type, got %q", got)
}
const want = `{
"id":"advanced_reporting",
"title":"Detailed Reporting",
"description":"Generate performance reports and current-state exports across infrastructure and workloads.",
"lockedState":{
"title":"Advanced Reporting",
"description":"AI-narrated performance reports for a single resource or your full fleet, with an executive summary, outlier callouts, and period-over-period changes. PDF and CSV formats, plus current-state VM inventory exports. AI narration uses Pulse Assistant when configured; reports fall back to a deterministic summary otherwise. Available on paid self-hosted and hosted plans."
},
"guidance":{
"title":"Advanced Insights",
"description":"Performance reports include an AI-narrated executive summary, fleet outliers, and period-over-period comparison. They draw from the historical metrics store and Patrol findings within the window. The VM inventory export captures current runtime state for spreadsheet-friendly fleet reviews. Use reports for retrospective trends and the inventory export for current allocation snapshots."
},
"performanceReport":{
"id":"performance_reports",
"title":"Performance Reports",
"description":"Generate PDF summaries or CSV metric exports from historical monitoring data for one or more selected resources.",
"singleResourceEndpoint":"/api/admin/reports/generate",
"multiResourceEndpoint":"/api/admin/reports/generate-multi",
"singleFilenamePrefix":"report",
"singleFilenameSubject":"resource_id",
"multiFilenamePrefix":"fleet-report",
"filenameDateStyle":"utc_yyyymmdd",
"formats":[
{
"value":"pdf",
"label":"PDF Report"
},
{
"value":"csv",
"label":"CSV Data"
}
],
"defaultFormat":"pdf",
"ranges":[
{
"key":"24h",
"label":"Last 24 Hours",
"description":"Current-day operational summary for short-term regressions.",
"windowHours":24
},
{
"key":"7d",
"label":"Last 7 Days",
"description":"Weekly trend window for recent performance changes.",
"windowHours":168
},
{
"key":"30d",
"label":"Last 30 Days",
"description":"Monthly review window for sustained capacity or reliability shifts.",
"windowHours":720
}
],
"defaultRange":"24h",
"multiResourceMax":50,
"supportsMetricFilter":true,
"supportsCustomTitle":true
},
"vmInventoryExport":{
"id":"vm_inventory",
"title":"VM Inventory Export",
"description":"Export the current fleet-wide VM inventory as CSV using the canonical runtime model. Includes VM identity, placement, CPU, memory allocation, disk allocation, and disk usage columns.",
"format":"csv",
"exportEndpoint":"/api/admin/reports/inventory/vms/export",
"filenamePrefix":"vm-inventory",
"filenameDateStyle":"utc_yyyymmdd",
"columns":[
{
"key":"resource_id",
"label":"Resource ID",
"description":"Canonical Pulse resource ID for the VM."
},
{
"key":"instance",
"label":"Instance",
"description":"Configured Proxmox instance or cluster name."
},
{
"key":"node",
"label":"Node",
"description":"Proxmox node currently hosting the VM."
},
{
"key":"pool",
"label":"Pool",
"description":"Canonical Proxmox pool membership when the platform reports one."
},
{
"key":"vmid",
"label":"VMID",
"description":"Numeric Proxmox VM identifier."
},
{
"key":"vm_name",
"label":"VM Name",
"description":"Current VM display name from the runtime model."
},
{
"key":"status",
"label":"Status",
"description":"Canonical runtime status for the VM."
},
{
"key":"cpu_cores",
"label":"CPU Cores",
"description":"Allocated virtual CPU core count."
},
{
"key":"memory_allocated_bytes",
"label":"Memory Allocated Bytes",
"description":"Configured memory allocation in bytes."
},
{
"key":"disk_allocated_bytes",
"label":"Disk Allocated Bytes",
"description":"Total allocated disk capacity in bytes across the VM."
},
{
"key":"disk_used_bytes",
"label":"Disk Used Bytes",
"description":"Current used disk bytes from the canonical runtime disk view."
},
{
"key":"disk_status_reason",
"label":"Disk Status Reason",
"description":"Reason disk usage is partial or unavailable when the runtime cannot provide a full guest view."
}
]
}
}`
assertJSONSnapshot(t, rec.Body.Bytes(), want)
}
func TestContract_PerformanceReportTransportUsesCatalogDefinition(t *testing.T) {
engine := &stubReportingEngine{data: []byte("report"), contentType: "application/pdf"}
original := reporting.GetEngine()
reporting.SetEngine(engine)
t.Cleanup(func() { reporting.SetEngine(original) })
handler := NewReportingHandlers(nil, nil)
req := httptest.NewRequest(
http.MethodGet,
"/api/reporting?resourceType=node&resourceId=node-1&metricType=+cpu+&title=+Node+report+",
nil,
)
rec := httptest.NewRecorder()
handler.HandleGenerateReport(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("expected 200, got %d body=%s", rec.Code, rec.Body.String())
}
definition := reporting.DescribePerformanceReport()
if got := rec.Header().Get("Content-Disposition"); !strings.HasPrefix(got, fmt.Sprintf("attachment; filename=\"%s-node-1-", definition.SingleFilenamePrefix)) {
t.Fatalf("expected canonical performance-report attachment filename, got %q", got)
}
if engine.lastReq.Format != definition.DefaultFormat {
t.Fatalf("expected default format %q, got %q", definition.DefaultFormat, engine.lastReq.Format)
}
if engine.lastReq.MetricType != "cpu" || engine.lastReq.Title != "Node report" {
t.Fatalf("expected trimmed canonical optional fields, got %+v", engine.lastReq)
}
}
func TestContract_ReportingCatalogRouteAccessibleWithoutReportingFeature(t *testing.T) {
rawToken := "reporting-catalog-contract-token-123.12345678"
record := newTokenRecord(t, rawToken, []string{config.ScopeSettingsRead}, nil)
cfg := newTestConfigWithTokens(t, record)
router := NewRouter(cfg, nil, nil, nil, nil, "1.0.0")
req := httptest.NewRequest(http.MethodGet, "/api/admin/reports/catalog", nil)
req.Header.Set("X-API-Token", rawToken)
rec := httptest.NewRecorder()
router.Handler().ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("expected 200 for ungated reporting catalog route, got %d body=%s", rec.Code, rec.Body.String())
}
if got := rec.Header().Get("Content-Type"); !strings.Contains(got, "application/json") {
t.Fatalf("expected json content type, got %q", got)
}
}
func TestContract_LocalCommercialAnalyticsRoutesAreNotRegistered(t *testing.T) {
rawToken := "retired-commercial-analytics-contract-token-123.12345678"
record := newTokenRecord(t, rawToken, []string{config.ScopeSettingsRead}, nil)
cfg := newTestConfigWithTokens(t, record)
router := NewRouter(cfg, nil, nil, nil, nil, "1.0.0")
for _, tc := range []struct {
method string
path string
}{
{method: http.MethodPost, path: "/api/upgrade-metrics/events"},
{method: http.MethodGet, path: "/api/upgrade-metrics/stats"},
{method: http.MethodGet, path: "/api/upgrade-metrics/health"},
{method: http.MethodGet, path: "/api/upgrade-metrics/config"},
{method: http.MethodPut, path: "/api/upgrade-metrics/config"},
{method: http.MethodGet, path: "/api/admin/upgrade-metrics-funnel"},
} {
t.Run(tc.method+" "+tc.path, func(t *testing.T) {
req := httptest.NewRequest(tc.method, tc.path, nil)
req.Header.Set("X-API-Token", rawToken)
rec := httptest.NewRecorder()
router.Handler().ServeHTTP(rec, req)
if rec.Code != http.StatusNotFound {
t.Fatalf("%s %s status=%d, want %d body=%s", tc.method, tc.path, rec.Code, http.StatusNotFound, rec.Body.String())
}
})
}
}
func TestContract_PerformanceReportTransportUsesCatalogDefaultRange(t *testing.T) {
engine := &stubReportingEngine{data: []byte("report"), contentType: "application/pdf"}
original := reporting.GetEngine()
reporting.SetEngine(engine)
t.Cleanup(func() { reporting.SetEngine(original) })
handler := NewReportingHandlers(nil, nil)
end := time.Date(2026, 3, 25, 15, 0, 0, 0, time.UTC)
req := httptest.NewRequest(
http.MethodGet,
"/api/reporting?resourceType=node&resourceId=node-1&end="+url.QueryEscape(end.Format(time.RFC3339)),
nil,
)
rec := httptest.NewRecorder()
handler.HandleGenerateReport(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("expected 200, got %d body=%s", rec.Code, rec.Body.String())
}
definition := reporting.DescribePerformanceReport()
if got := engine.lastReq.Start; !got.Equal(end.Add(-definition.DefaultRangeDuration())) {
t.Fatalf("expected canonical default start time, got %s", got)
}
if !engine.lastReq.End.Equal(end) {
t.Fatalf("expected canonical end time, got %s", engine.lastReq.End)
}
}
func TestContract_PerformanceReportTransportRejectsInvalidTimeRange(t *testing.T) {
engine := &stubReportingEngine{data: []byte("report"), contentType: "application/pdf"}
original := reporting.GetEngine()
reporting.SetEngine(engine)
t.Cleanup(func() { reporting.SetEngine(original) })
handler := NewReportingHandlers(nil, nil)
req := httptest.NewRequest(
http.MethodGet,
"/api/reporting?resourceType=node&resourceId=node-1&start=2026-03-25T12:00:00Z&end=2026-03-25T11:00:00Z",
nil,
)
rec := httptest.NewRecorder()
handler.HandleGenerateReport(rec, req)
if rec.Code != http.StatusBadRequest {
t.Fatalf("expected 400, got %d body=%s", rec.Code, rec.Body.String())
}
var payload struct {
Code string `json:"code"`
Error string `json:"error"`
}
if err := json.NewDecoder(rec.Body).Decode(&payload); err != nil {
t.Fatalf("decode invalid-time-range response: %v", err)
}
if payload.Code != "invalid_time_range" {
t.Fatalf("expected invalid_time_range code, got %q", payload.Code)
}
if payload.Error != "end must be after start" {
t.Fatalf("expected canonical invalid_time_range message, got %q", payload.Error)
}
}
func TestContract_PerformanceReportTransportRejectsOversizedMultiBody(t *testing.T) {
engine := &stubReportingEngine{data: []byte("report"), contentType: "application/pdf"}
original := reporting.GetEngine()
reporting.SetEngine(engine)
t.Cleanup(func() { reporting.SetEngine(original) })
handler := NewReportingHandlers(nil, nil)
padding := strings.Repeat("x", reportingMultiReportBodyMax)
req := httptest.NewRequest(
http.MethodPost,
"/api/reporting/generate-multi",
strings.NewReader(fmt.Sprintf(`{"resources":[{"resourceType":"vm","resourceId":"vm-1"}],"format":"pdf","title":"%s"}`, padding)),
)
rec := httptest.NewRecorder()
handler.HandleGenerateMultiReport(rec, req)
if rec.Code != http.StatusBadRequest {
t.Fatalf("expected 400, got %d body=%s", rec.Code, rec.Body.String())
}
var payload struct {
Code string `json:"code"`
Error string `json:"error"`
}
if err := json.NewDecoder(rec.Body).Decode(&payload); err != nil {
t.Fatalf("decode oversized-body response: %v", err)
}
if payload.Code != "body_too_large" {
t.Fatalf("expected body_too_large code, got %q", payload.Code)
}
if payload.Error != "Request body must be 1MB or less" {
t.Fatalf("expected canonical oversized-body message, got %q", payload.Error)
}
}
func TestContract_PerformanceReportTransportRejectsInvalidOptionalFieldWithStableCode(t *testing.T) {
engine := &stubReportingEngine{data: []byte("report"), contentType: "application/pdf"}
original := reporting.GetEngine()
reporting.SetEngine(engine)
t.Cleanup(func() { reporting.SetEngine(original) })
handler := NewReportingHandlers(nil, nil)
req := httptest.NewRequest(
http.MethodGet,
"/api/reporting?resourceType=node&resourceId=node-1&metricType="+url.QueryEscape("cpu usage"),
nil,
)
rec := httptest.NewRecorder()
handler.HandleGenerateReport(rec, req)
if rec.Code != http.StatusBadRequest {
t.Fatalf("expected 400, got %d body=%s", rec.Code, rec.Body.String())
}
var payload struct {
Code string `json:"code"`
Error string `json:"error"`
}
if err := json.NewDecoder(rec.Body).Decode(&payload); err != nil {
t.Fatalf("decode invalid optional-field response: %v", err)
}
if payload.Code != "invalid_metric_type" {
t.Fatalf("expected invalid_metric_type code, got %q", payload.Code)
}
if payload.Error != "metricType must match [a-zA-Z0-9._:-]+ and be <= 64 chars" {
t.Fatalf("expected canonical invalid_metric_type message, got %q", payload.Error)
}
}
func TestContract_ReportingInvalidFormatErrorsUseCatalogDefinitions(t *testing.T) {
engine := &stubReportingEngine{data: []byte("report"), contentType: "application/pdf"}
original := reporting.GetEngine()
reporting.SetEngine(engine)
t.Cleanup(func() { reporting.SetEngine(original) })
handler := NewReportingHandlers(nil, nil)
reportReq := httptest.NewRequest(
http.MethodGet,
"/api/reporting?format=xlsx&resourceType=node&resourceId=node-1",
nil,
)
reportRec := httptest.NewRecorder()
handler.HandleGenerateReport(reportRec, reportReq)
if reportRec.Code != http.StatusBadRequest {
t.Fatalf("expected 400 for invalid performance report format, got %d body=%s", reportRec.Code, reportRec.Body.String())
}
var reportPayload struct {
Error string `json:"error"`
}
if err := json.NewDecoder(reportRec.Body).Decode(&reportPayload); err != nil {
t.Fatalf("decode performance invalid-format response: %v", err)
}
if reportPayload.Error != reporting.DescribePerformanceReport().InvalidFormatError() {
t.Fatalf("expected canonical performance invalid-format error, got %q", reportPayload.Error)
}
inventoryReq := httptest.NewRequest(
http.MethodGet,
"/api/admin/reports/inventory/vms/export?format=pdf",
nil,
)
inventoryRec := httptest.NewRecorder()
handler.HandleExportVMInventory(inventoryRec, inventoryReq)
if inventoryRec.Code != http.StatusBadRequest {
t.Fatalf("expected 400 for invalid inventory format, got %d body=%s", inventoryRec.Code, inventoryRec.Body.String())
}
var inventoryPayload struct {
Error string `json:"error"`
}
if err := json.NewDecoder(inventoryRec.Body).Decode(&inventoryPayload); err != nil {
t.Fatalf("decode inventory invalid-format response: %v", err)
}
if inventoryPayload.Error != reporting.DescribeVMInventoryExport().InvalidFormatError() {
t.Fatalf("expected canonical inventory invalid-format error, got %q", inventoryPayload.Error)
}
}
func TestContract_HostedTenantAISettingsDoesNotAutoBootstrapQuickstartJSONSnapshot(t *testing.T) {
t.Setenv("PULSE_HOSTED_MODE", "true")
baseDir := t.TempDir()
mtp := config.NewMultiTenantPersistence(baseDir)
persistence, err := mtp.GetPersistence("t-tenant")
if err != nil {
t.Fatalf("GetPersistence(t-tenant): %v", err)
}
seedHostedAIBillingState(t, mtp, "default")
handler := NewAISettingsHandler(mtp, nil, nil)
handler.defaultConfig = &config.Config{DataPath: baseDir}
req := httptest.NewRequest(http.MethodGet, "/api/settings/ai", nil)
req = req.WithContext(context.WithValue(req.Context(), OrgIDContextKey, "t-tenant"))
rec := httptest.NewRecorder()
handler.HandleGetAISettings(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status=%d, want %d: %s", rec.Code, http.StatusOK, rec.Body.String())
}
if persistence.HasAIConfig() {
t.Fatal("expected hosted tenant AI settings contract not to persist implicit quickstart bootstrap")
}
const want = `{
"enabled":false,
"model":"",
"configured":false,
"custom_context":"",
"auth_method":"api_key",
"oauth_connected":false,
"patrol_interval_minutes":360,
"patrol_enabled":true,
"patrol_auto_fix":false,
"alert_triggered_analysis":true,
"patrol_event_triggers_enabled":true,
"patrol_alert_triggers_enabled":true,
"patrol_anomaly_triggers_enabled":true,
"use_proactive_thresholds":false,
"available_models":[],
"anthropic_configured":false,
"openai_configured":false,
"openrouter_configured":false,
"deepseek_configured":false,
"gemini_configured":false,
"ollama_configured":false,
"ollama_base_url":"http://localhost:11434",
"ollama_password_set":false,
"configured_providers":[],
"control_level":"read_only",
"protected_guests":[],
"discovery_enabled":false,
"patrol_readiness":{"status":"not_ready","ready":false,"cause":"service_unavailable","summary":"Pulse Patrol service is not available.","checks":[{"id":"service","status":"not_ready","cause":"service_unavailable","label":"Patrol service","message":"Pulse Patrol service is not available.","action":"restart_service"}]}
}`
assertJSONSnapshot(t, rec.Body.Bytes(), want)
}
func TestContract_StripeWebhookHandlersUseCanonicalRuntimeDataDir(t *testing.T) {
envDir := t.TempDir()
t.Setenv("PULSE_DATA_DIR", envDir)
persistence := config.NewMultiTenantPersistence(envDir)
billingStore := config.NewFileBillingStore(envDir)
rbacProvider := NewTenantRBACProvider(envDir)
withExplicitDir := NewStripeWebhookHandlers(billingStore, persistence, rbacProvider, nil, nil, true, envDir)
if got := filepath.Dir(withExplicitDir.deduper.dir); got != filepath.Join(envDir, "stripe") {
t.Fatalf("explicit dedupe dir root = %q, want %q", got, filepath.Join(envDir, "stripe"))
}
if got := filepath.Dir(withExplicitDir.index.dir); got != filepath.Join(envDir, "stripe") {
t.Fatalf("explicit customer index dir root = %q, want %q", got, filepath.Join(envDir, "stripe"))
}
withEnvFallback := NewStripeWebhookHandlers(billingStore, persistence, rbacProvider, nil, nil, true, "")
if got := filepath.Dir(withEnvFallback.deduper.dir); got != filepath.Join(envDir, "stripe") {
t.Fatalf("env fallback dedupe dir root = %q, want %q", got, filepath.Join(envDir, "stripe"))
}
if got := filepath.Dir(withEnvFallback.index.dir); got != filepath.Join(envDir, "stripe") {
t.Fatalf("env fallback customer index dir root = %q, want %q", got, filepath.Join(envDir, "stripe"))
}
}
func TestContract_NotificationWebhookTestResponseJSONSnapshot(t *testing.T) {
payload := map[string]interface{}{
"success": true,
"status": 200,
"response": "OK",
}
got, err := json.Marshal(payload)
if err != nil {
t.Fatalf("marshal webhook test response: %v", err)
}
const want = `{
"response":"OK",
"status":200,
"success":true
}`
assertJSONSnapshot(t, got, want)
}
func TestContract_NotificationPushoverWebhookResponseJSONSnapshot(t *testing.T) {
payload := map[string]interface{}{
"id": "hook-1",
"name": "Pushover",
"url": "https://api.pushover.net/1/messages.json",
"service": "pushover",
"customFields": map[string]string{
"token": "app-token",
"user": "user-key",
},
}
got, err := json.Marshal(payload)
if err != nil {
t.Fatalf("marshal pushover webhook response: %v", err)
}
const want = `{
"customFields":{"token":"app-token","user":"user-key"},
"id":"hook-1",
"name":"Pushover",
"service":"pushover",
"url":"https://api.pushover.net/1/messages.json"
}`
assertJSONSnapshot(t, got, want)
}
func TestContract_ResourceIntelligenceIncludesRecentChanges(t *testing.T) {
svc := newEnabledAIService(t)
canonicalStore := unifiedresources.NewMemoryStore()
observedAt := time.Now().Add(-15 * time.Minute)
if err := canonicalStore.RecordChange(unifiedresources.ResourceChange{
ID: "change-contract",
ObservedAt: observedAt,
ResourceID: "vm-100",
Kind: unifiedresources.ChangeRestart,
SourceType: unifiedresources.SourcePlatformEvent,
Reason: "guest restarted",
}); err != nil {
t.Fatalf("record canonical change: %v", err)
}
correlationDetector := correlation.NewDetector(correlation.Config{
MinOccurrences: 1,
CorrelationWindow: 2 * time.Hour,
RetentionWindow: 24 * time.Hour,
})
correlationBase := observedAt.Add(-10 * time.Minute)
correlationDetector.RecordEvent(correlation.Event{
ResourceID: "storage-1",
ResourceName: "storage-1",
ResourceType: "storage",
EventType: correlation.EventDiskFull,
Timestamp: correlationBase,
})
correlationDetector.RecordEvent(correlation.Event{
ResourceID: "vm-100",
ResourceName: "vm-100",
ResourceType: "vm",
EventType: correlation.EventRestart,
Timestamp: correlationBase.Add(1 * time.Minute),
})
correlationDetector.RecordEvent(correlation.Event{
ResourceID: "storage-1",
ResourceName: "storage-1",
ResourceType: "storage",
EventType: correlation.EventDiskFull,
Timestamp: correlationBase.Add(2 * time.Minute),
})
correlationDetector.RecordEvent(correlation.Event{
ResourceID: "vm-100",
ResourceName: "vm-100",
ResourceType: "vm",
EventType: correlation.EventRestart,
Timestamp: correlationBase.Add(3 * time.Minute),
})
svc.SetUnifiedResourceProvider(&stubUnifiedResourceProvider{
resources: []unifiedresources.Resource{
{ID: "public-1", Type: unifiedresources.ResourceTypeVM, Tags: []string{"public"}},
{ID: "internal-1", Type: unifiedresources.ResourceTypeAgent, Agent: &unifiedresources.AgentData{Hostname: "agent-1"}},
},
})
setUnexportedField(t, svc.GetPatrolService(), "correlationDetector", correlationDetector)
setUnexportedField(t, svc, "resourceExportStore", canonicalStore)
setUnexportedField(t, svc.GetPatrolService(), "aiService", svc)
handlers := &AISettingsHandler{defaultAIService: svc}
req := httptest.NewRequest(http.MethodGet, "/api/ai/intelligence?resource_id=vm-100", nil)
rec := httptest.NewRecorder()
handlers.HandleGetIntelligence(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status = %d, want 200: %s", rec.Code, rec.Body.String())
}
var payload map[string]interface{}
if err := json.Unmarshal(rec.Body.Bytes(), &payload); err != nil {
t.Fatalf("decode response: %v", err)
}
recentChanges, ok := payload["recent_changes"].([]interface{})
if !ok {
t.Fatalf("expected recent_changes array in response, got %T", payload["recent_changes"])
}
if len(recentChanges) != 1 {
t.Fatalf("expected 1 recent change, got %d", len(recentChanges))
}
if _, ok := payload["policy_posture"]; ok {
t.Fatal("did not expect policy_posture in resource intelligence response")
}
dependencies, ok := payload["dependencies"].([]interface{})
if !ok {
t.Fatalf("expected dependencies array in response, got %T", payload["dependencies"])
}
if len(dependencies) == 0 {
t.Fatal("expected at least one dependency in response")
}
correlations, ok := payload["correlations"].([]interface{})
if !ok {
t.Fatalf("expected correlations array in response, got %T", payload["correlations"])
}
if len(correlations) == 0 {
t.Fatal("expected at least one correlation in response")
}
firstCorrelation, ok := correlations[0].(map[string]interface{})
if !ok {
t.Fatalf("expected correlation object, got %T", correlations[0])
}
if firstCorrelation["event_pattern"] == "" {
t.Fatal("expected correlation event_pattern in response")
}
if firstCorrelation["avg_delay"] == nil {
t.Fatal("expected correlation avg_delay in response")
}
if firstCorrelation["confidence"] == nil {
t.Fatal("expected correlation confidence in response")
}
}
func TestContract_IntelligenceSummaryIncludesRecentChanges(t *testing.T) {
svc := newEnabledAIService(t)
canonicalStore := unifiedresources.NewMemoryStore()
if err := canonicalStore.RecordChange(unifiedresources.ResourceChange{
ID: "change-summary",
ObservedAt: time.Now().Add(-15 * time.Minute),
ResourceID: "vm-100",
Kind: unifiedresources.ChangeRestart,
SourceType: unifiedresources.SourcePlatformEvent,
Reason: "guest restarted",
}); err != nil {
t.Fatalf("record canonical change: %v", err)
}
svc.SetUnifiedResourceProvider(&stubUnifiedResourceProvider{
resources: []unifiedresources.Resource{
{
ID: "public-1",
Name: "public-vm",
Type: unifiedresources.ResourceTypeVM,
Tags: []string{"public"},
},
{
ID: "restricted-1",
Name: "mail-gw",
Type: unifiedresources.ResourceTypePMG,
PMG: &unifiedresources.PMGData{Hostname: "mail.internal"},
},
},
})
setUnexportedField(t, svc, "resourceExportStore", canonicalStore)
setUnexportedField(t, svc.GetPatrolService(), "aiService", svc)
handlers := &AISettingsHandler{defaultAIService: svc}
req := httptest.NewRequest(http.MethodGet, "/api/ai/intelligence", nil)
rec := httptest.NewRecorder()
handlers.HandleGetIntelligence(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status = %d, want 200: %s", rec.Code, rec.Body.String())
}
var payload map[string]interface{}
if err := json.Unmarshal(rec.Body.Bytes(), &payload); err != nil {
t.Fatalf("decode response: %v", err)
}
recentChanges, ok := payload["recent_changes"].([]interface{})
if !ok {
t.Fatalf("expected recent_changes array in response, got %T", payload["recent_changes"])
}
if len(recentChanges) != 1 {
t.Fatalf("expected 1 recent change, got %d", len(recentChanges))
}
policyPosture, ok := payload["policy_posture"].(map[string]interface{})
if !ok {
t.Fatalf("expected policy_posture object in response, got %T", payload["policy_posture"])
}
if got := int(policyPosture["total_resources"].(float64)); got != 2 {
t.Fatalf("expected total_resources=2, got %d", got)
}
}
func TestContract_RecentChangesEndpointUsesCanonicalTimeline(t *testing.T) {
svc := newEnabledAIService(t)
canonicalStore := unifiedresources.NewMemoryStore()
if err := canonicalStore.RecordChange(unifiedresources.ResourceChange{
ID: "change-canonical",
ObservedAt: time.Now().Add(-25 * time.Minute),
ResourceID: "vm-canonical",
Kind: unifiedresources.ChangeRestart,
From: "running",
To: "restarting",
SourceType: unifiedresources.SourcePlatformEvent,
SourceAdapter: unifiedresources.AdapterProxmox,
Reason: "guest restarted after maintenance",
}); err != nil {
t.Fatalf("record canonical change: %v", err)
}
svc.SetUnifiedResourceProvider(&stubUnifiedResourceProvider{
resources: []unifiedresources.Resource{
{
ID: "vm-canonical",
Name: "canonical-vm",
Type: unifiedresources.ResourceTypeVM,
},
},
})
setUnexportedField(t, svc, "resourceExportStore", canonicalStore)
setUnexportedField(t, svc.GetPatrolService(), "aiService", svc)
handlers := &AISettingsHandler{defaultAIService: svc}
req := httptest.NewRequest(http.MethodGet, "/api/ai/intelligence/changes?hours=1", nil)
rec := httptest.NewRecorder()
handlers.HandleGetRecentChanges(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status = %d, want 200: %s", rec.Code, rec.Body.String())
}
var payload map[string]interface{}
if err := json.Unmarshal(rec.Body.Bytes(), &payload); err != nil {
t.Fatalf("decode response: %v", err)
}
changes, ok := payload["changes"].([]interface{})
if !ok {
t.Fatalf("expected changes array in response, got %T", payload["changes"])
}
if len(changes) != 1 {
t.Fatalf("expected 1 recent change, got %d", len(changes))
}
change, ok := changes[0].(map[string]interface{})
if !ok {
t.Fatalf("expected object change, got %#v", changes[0])
}
if change["resource_name"] != "canonical-vm" {
t.Fatalf("expected canonical resource name, got %#v", change["resource_name"])
}
if change["resource_type"] != string(unifiedresources.ResourceTypeVM) {
t.Fatalf("expected resource type vm, got %#v", change["resource_type"])
}
if change["change_type"] != string(unifiedresources.ChangeRestart) {
t.Fatalf("expected canonical change type, got %#v", change["change_type"])
}
if desc, ok := change["description"].(string); !ok || !strings.Contains(desc, "Restart") {
t.Fatalf("expected canonical change description, got %#v", change["description"])
}
}
func TestContract_AIIntelligenceCorrelationsJSONSnapshot(t *testing.T) {
now := time.Date(2026, 3, 18, 17, 30, 0, 0, time.UTC)
payload := map[string]any{
"correlations": []map[string]any{
{
"source_id": "node-1",
"source_name": "node-1",
"source_type": "node",
"target_id": "vm-1",
"target_name": "vm-1",
"target_type": "vm",
"event_pattern": "high_cpu -> restart",
"occurrences": 1,
"avg_delay": "1m0s",
"confidence": 0.1,
"last_seen": now,
"description": "When node-1 experiences high_cpu, vm-1 often follows within 1m0s",
},
},
"count": 1,
}
got, err := json.Marshal(payload)
if err != nil {
t.Fatalf("marshal correlations response: %v", err)
}
const want = `{
"correlations":[
{
"avg_delay":"1m0s",
"confidence":0.1,
"description":"When node-1 experiences high_cpu, vm-1 often follows within 1m0s",
"event_pattern":"high_cpu -\u003e restart",
"last_seen":"2026-03-18T17:30:00Z",
"occurrences":1,
"source_id":"node-1",
"source_name":"node-1",
"source_type":"node",
"target_id":"vm-1",
"target_name":"vm-1",
"target_type":"vm"
}
],
"count":1
}`
assertJSONSnapshot(t, got, want)
}
func TestContract_MonitoredSystemLedgerJSONSnapshot(t *testing.T) {
payload := MonitoredSystemLedgerResponse{
Systems: []MonitoredSystemLedgerEntry{
{
Name: "Tower",
Type: "host",
Status: "warning",
StatusExplanation: MonitoredSystemLedgerStatusExplanation{
Summary: "At least one included source is stale, so Pulse marks this monitored system as warning.",
Reasons: []MonitoredSystemLedgerStatusReason{
{
Kind: "source-stale",
Name: "Tower",
Type: "host",
Source: "agent",
Status: "stale",
ReportedAt: "2026-03-18T17:25:00Z",
Summary: "Agent data for Tower is stale (last reported 2026-03-18T17:25:00Z).",
},
},
},
LatestIncludedSignal: MonitoredSystemLedgerLatestSignal{
Name: "Tower",
Type: "host",
Source: "agent",
At: "2026-03-18T17:30:00Z",
},
Source: "agent",
Explanation: MonitoredSystemLedgerExplanation{
Summary: "Counts as one monitored system because Pulse sees one top-level host view from agent.",
Reasons: []MonitoredSystemLedgerExplanationReason{
{
Kind: "standalone",
Signal: "single-top-level-view",
Summary: "No overlapping top-level source matched this system.",
},
},
Surfaces: []MonitoredSystemLedgerExplanationSurface{
{
Name: "Tower",
Type: "host",
Source: "agent",
},
},
},
},
},
Total: 1,
}
got, err := json.Marshal(payload)
if err != nil {
t.Fatalf("marshal monitored system ledger response: %v", err)
}
const want = `{
"systems":[
{
"name":"Tower",
"type":"host",
"status":"warning",
"status_explanation":{
"summary":"At least one included source is stale, so Pulse marks this monitored system as warning.",
"reasons":[
{
"kind":"source-stale",
"name":"Tower",
"type":"host",
"source":"agent",
"status":"stale",
"reported_at":"2026-03-18T17:25:00Z",
"summary":"Agent data for Tower is stale (last reported 2026-03-18T17:25:00Z)."
}
]
},
"latest_included_signal":{
"name":"Tower",
"type":"host",
"source":"agent",
"at":"2026-03-18T17:30:00Z"
},
"source":"agent",
"explanation":{
"summary":"Counts as one monitored system because Pulse sees one top-level host view from agent.",
"reasons":[
{
"kind":"standalone",
"signal":"single-top-level-view",
"summary":"No overlapping top-level source matched this system."
}
],
"surfaces":[
{
"name":"Tower",
"type":"host",
"source":"agent"
}
]
}
}
],
"total":1
}`
assertJSONSnapshot(t, got, want)
}
func TestContract_MonitoredSystemLedgerPreviewJSONSnapshot(t *testing.T) {
payload := MonitoredSystemLedgerPreviewResponse{
CurrentCount: 1,
ProjectedCount: 1,
AdditionalCount: 0,
Effect: "attaches_existing",
CurrentSystems: []MonitoredSystemLedgerEntry{
{
Name: "Tower",
Type: "host",
Status: "online",
StatusExplanation: MonitoredSystemLedgerStatusExplanation{
Summary: "All included top-level collection paths currently report online status.",
Reasons: []MonitoredSystemLedgerStatusReason{},
},
LatestIncludedSignal: MonitoredSystemLedgerLatestSignal{
Name: "Tower",
Type: "host",
Source: "agent",
At: "2026-03-18T17:30:00Z",
},
Source: "agent",
Explanation: MonitoredSystemLedgerExplanation{
Summary: "Counts as one monitored system because Pulse sees one top-level host view from agent.",
Reasons: []MonitoredSystemLedgerExplanationReason{
{
Kind: "standalone",
Signal: "single-top-level-view",
Summary: "No overlapping top-level source matched this system.",
},
},
Surfaces: []MonitoredSystemLedgerExplanationSurface{
{Name: "Tower", Type: "host", Source: "agent"},
},
},
},
},
ProjectedSystems: []MonitoredSystemLedgerEntry{
{
Name: "tower",
Type: "proxmox-node",
Status: "online",
StatusExplanation: MonitoredSystemLedgerStatusExplanation{
Summary: "All included top-level collection paths currently report online status.",
Reasons: []MonitoredSystemLedgerStatusReason{},
},
LatestIncludedSignal: MonitoredSystemLedgerLatestSignal{
Name: "tower",
Type: "proxmox-node",
Source: "proxmox",
At: "2026-03-18T17:35:00Z",
},
Source: "multiple",
Explanation: MonitoredSystemLedgerExplanation{
Summary: "Counts as one monitored system because Pulse merged 2 top-level views into one canonical system using shared machine identity.",
Reasons: []MonitoredSystemLedgerExplanationReason{
{
Kind: "grouped",
Signal: "identity-match",
Summary: "Pulse matched these top-level views by shared canonical machine identity.",
},
},
Surfaces: []MonitoredSystemLedgerExplanationSurface{
{Name: "Tower", Type: "host", Source: "agent"},
{Name: "tower", Type: "proxmox-node", Source: "proxmox"},
},
},
},
},
CurrentSystem: &MonitoredSystemLedgerEntry{
Name: "Tower",
Type: "host",
Status: "online",
StatusExplanation: MonitoredSystemLedgerStatusExplanation{
Summary: "All included top-level collection paths currently report online status.",
Reasons: []MonitoredSystemLedgerStatusReason{},
},
LatestIncludedSignal: MonitoredSystemLedgerLatestSignal{
Name: "Tower",
Type: "host",
Source: "agent",
At: "2026-03-18T17:30:00Z",
},
Source: "agent",
Explanation: MonitoredSystemLedgerExplanation{
Summary: "Counts as one monitored system because Pulse sees one top-level host view from agent.",
Reasons: []MonitoredSystemLedgerExplanationReason{
{
Kind: "standalone",
Signal: "single-top-level-view",
Summary: "No overlapping top-level source matched this system.",
},
},
Surfaces: []MonitoredSystemLedgerExplanationSurface{
{Name: "Tower", Type: "host", Source: "agent"},
},
},
},
ProjectedSystem: &MonitoredSystemLedgerEntry{
Name: "tower",
Type: "proxmox-node",
Status: "online",
StatusExplanation: MonitoredSystemLedgerStatusExplanation{
Summary: "All included top-level collection paths currently report online status.",
Reasons: []MonitoredSystemLedgerStatusReason{},
},
LatestIncludedSignal: MonitoredSystemLedgerLatestSignal{
Name: "tower",
Type: "proxmox-node",
Source: "proxmox",
At: "2026-03-18T17:35:00Z",
},
Source: "multiple",
Explanation: MonitoredSystemLedgerExplanation{
Summary: "Counts as one monitored system because Pulse merged 2 top-level views into one canonical system using shared machine identity.",
Reasons: []MonitoredSystemLedgerExplanationReason{
{
Kind: "grouped",
Signal: "identity-match",
Summary: "Pulse matched these top-level views by shared canonical machine identity.",
},
},
Surfaces: []MonitoredSystemLedgerExplanationSurface{
{Name: "Tower", Type: "host", Source: "agent"},
{Name: "tower", Type: "proxmox-node", Source: "proxmox"},
},
},
},
}
got, err := json.Marshal(payload.NormalizeCollections())
if err != nil {
t.Fatalf("marshal monitored system ledger preview response: %v", err)
}
const want = `{
"current_count":1,
"projected_count":1,
"additional_count":0,
"effect":"attaches_existing",
"current_systems":[
{
"name":"Tower",
"type":"host",
"status":"online",
"status_explanation":{
"summary":"All included top-level collection paths currently report online status.",
"reasons":[]
},
"latest_included_signal":{
"name":"Tower",
"type":"host",
"source":"agent",
"at":"2026-03-18T17:30:00Z"
},
"source":"agent",
"explanation":{
"summary":"Counts as one monitored system because Pulse sees one top-level host view from agent.",
"reasons":[
{
"kind":"standalone",
"signal":"single-top-level-view",
"summary":"No overlapping top-level source matched this system."
}
],
"surfaces":[
{
"name":"Tower",
"type":"host",
"source":"agent"
}
]
}
}
],
"projected_systems":[
{
"name":"tower",
"type":"proxmox-node",
"status":"online",
"status_explanation":{
"summary":"All included top-level collection paths currently report online status.",
"reasons":[]
},
"latest_included_signal":{
"name":"tower",
"type":"proxmox-node",
"source":"proxmox",
"at":"2026-03-18T17:35:00Z"
},
"source":"multiple",
"explanation":{
"summary":"Counts as one monitored system because Pulse merged 2 top-level views into one canonical system using shared machine identity.",
"reasons":[
{
"kind":"grouped",
"signal":"identity-match",
"summary":"Pulse matched these top-level views by shared canonical machine identity."
}
],
"surfaces":[
{
"name":"Tower",
"type":"host",
"source":"agent"
},
{
"name":"tower",
"type":"proxmox-node",
"source":"proxmox"
}
]
}
}
],
"current_system":{
"name":"Tower",
"type":"host",
"status":"online",
"status_explanation":{
"summary":"All included top-level collection paths currently report online status.",
"reasons":[]
},
"latest_included_signal":{
"name":"Tower",
"type":"host",
"source":"agent",
"at":"2026-03-18T17:30:00Z"
},
"source":"agent",
"explanation":{
"summary":"Counts as one monitored system because Pulse sees one top-level host view from agent.",
"reasons":[
{
"kind":"standalone",
"signal":"single-top-level-view",
"summary":"No overlapping top-level source matched this system."
}
],
"surfaces":[
{
"name":"Tower",
"type":"host",
"source":"agent"
}
]
}
},
"projected_system":{
"name":"tower",
"type":"proxmox-node",
"status":"online",
"status_explanation":{
"summary":"All included top-level collection paths currently report online status.",
"reasons":[]
},
"latest_included_signal":{
"name":"tower",
"type":"proxmox-node",
"source":"proxmox",
"at":"2026-03-18T17:35:00Z"
},
"source":"multiple",
"explanation":{
"summary":"Counts as one monitored system because Pulse merged 2 top-level views into one canonical system using shared machine identity.",
"reasons":[
{
"kind":"grouped",
"signal":"identity-match",
"summary":"Pulse matched these top-level views by shared canonical machine identity."
}
],
"surfaces":[
{
"name":"Tower",
"type":"host",
"source":"agent"
},
{
"name":"tower",
"type":"proxmox-node",
"source":"proxmox"
}
]
}
}
}`
assertJSONSnapshot(t, got, want)
}
func TestContract_MonitoredSystemLedgerDoesNotEmitCompatibilityAliases(t *testing.T) {
entry := monitoredSystemLedgerEntry(unifiedresources.MonitoredSystemRecord{
Name: "Tower",
Type: "host",
Status: unifiedresources.StatusWarning,
StatusExplanation: unifiedresources.MonitoredSystemStatusExplanation{
Summary: "At least one included source is stale, so Pulse marks this monitored system as warning.",
Reasons: []unifiedresources.MonitoredSystemStatusReason{},
},
LastSeen: time.Date(2026, 3, 18, 17, 35, 0, 0, time.UTC),
LatestIncludedSignal: unifiedresources.MonitoredSystemLatestSignal{
Name: "tower.local",
Type: "docker-host",
Source: "docker",
At: time.Date(2026, 3, 18, 17, 30, 0, 0, time.UTC),
},
Source: "multiple",
Explanation: unifiedresources.MonitoredSystemGroupingExplanation{
Summary: "Counts as one monitored system because Pulse merged 2 top-level views into one canonical system using shared machine identity.",
Reasons: []unifiedresources.MonitoredSystemGroupingReason{},
Surfaces: []unifiedresources.MonitoredSystemGroupingSurface{},
},
})
payload := MonitoredSystemLedgerResponse{
Systems: []MonitoredSystemLedgerEntry{entry},
Total: 1,
}
got, err := json.Marshal(payload)
if err != nil {
t.Fatalf("marshal monitored system ledger response: %v", err)
}
const want = `{
"systems":[
{
"name":"Tower",
"type":"host",
"status":"warning",
"status_explanation":{
"summary":"At least one included source is stale, so Pulse marks this monitored system as warning.",
"reasons":[]
},
"latest_included_signal":{
"name":"tower.local",
"type":"docker-host",
"source":"docker",
"at":"2026-03-18T17:30:00Z"
},
"source":"multiple",
"explanation":{
"summary":"Counts as one monitored system because Pulse merged 2 top-level views into one canonical system using shared machine identity.",
"reasons":[],
"surfaces":[]
}
}
],
"total":1
}`
assertJSONSnapshot(t, got, want)
}
func TestContract_ResolveAuthEnvPathUsesCanonicalRuntimeDataDir(t *testing.T) {
envDir := t.TempDir()
t.Setenv("PULSE_DATA_DIR", envDir)
explicitDir := t.TempDir()
if got := resolveAuthEnvPath(explicitDir); got != filepath.Join(explicitDir, ".env") {
t.Fatalf("resolveAuthEnvPath(explicit) = %q, want %q", got, filepath.Join(explicitDir, ".env"))
}
if got := resolveAuthEnvPath(""); got != filepath.Join(envDir, ".env") {
t.Fatalf("resolveAuthEnvPath(env fallback) = %q, want %q", got, filepath.Join(envDir, ".env"))
}
}
func TestContract_ResolveAuthEnvWritePathsDeduplicatesCanonicalFallback(t *testing.T) {
envDir := t.TempDir()
t.Setenv("PULSE_DATA_DIR", envDir)
paths := resolveAuthEnvWritePaths("", "")
if len(paths) != 1 {
t.Fatalf("resolveAuthEnvWritePaths() len = %d, want 1", len(paths))
}
if want := filepath.Join(envDir, ".env"); paths[0] != want {
t.Fatalf("resolveAuthEnvWritePaths()[0] = %q, want %q", paths[0], want)
}
}
func TestContract_WriteAuthEnvFileFallsBackToDataPath(t *testing.T) {
configPathFile := filepath.Join(t.TempDir(), "blocked")
if err := os.WriteFile(configPathFile, []byte("blocked"), 0600); err != nil {
t.Fatalf("write blocked config path file: %v", err)
}
dataDir := t.TempDir()
writtenPath, err := writeAuthEnvFile(configPathFile, dataDir, []byte("PULSE_AUTH_USER='pulse'\n"))
if err != nil {
t.Fatalf("writeAuthEnvFile() error = %v", err)
}
wantPath := filepath.Join(dataDir, ".env")
if writtenPath != wantPath {
t.Fatalf("writeAuthEnvFile() path = %q, want %q", writtenPath, wantPath)
}
if _, err := os.Stat(wantPath); err != nil {
t.Fatalf("stat fallback auth env: %v", err)
}
}
func TestContract_RecoveryTokenPersistenceJSONSnapshot(t *testing.T) {
payload := []*RecoveryToken{
{
TokenHash: recoveryTokenHash("raw-recovery-token"),
CreatedAt: time.Date(2026, 2, 8, 13, 14, 15, 0, time.UTC),
ExpiresAt: time.Date(2026, 2, 8, 14, 14, 15, 0, time.UTC),
Used: true,
UsedAt: time.Date(2026, 2, 8, 13, 24, 15, 0, time.UTC),
IP: "192.168.1.10",
},
}
got, err := json.Marshal(payload)
if err != nil {
t.Fatalf("marshal recovery token persistence: %v", err)
}
const want = `[
{
"token_hash":"59b5880d54ca8c991c09269834d59ea09ab4f467fd4d580a932cd70c5b993fa4",
"created_at":"2026-02-08T13:14:15Z",
"expires_at":"2026-02-08T14:14:15Z",
"used":true,
"used_at":"2026-02-08T13:24:15Z",
"ip":"192.168.1.10"
}
]`
assertJSONSnapshot(t, got, want)
}
func TestContract_PersistentAuthStoresRequireExplicitInitialization(t *testing.T) {
resetPersistentAuthStoresForTests()
t.Cleanup(resetPersistentAuthStoresForTests)
assertPanics := func(name string, fn func()) {
t.Helper()
defer func() {
if recover() == nil {
t.Fatalf("%s should require explicit initialization", name)
}
}()
fn()
}
assertPanics("session store", func() { _ = GetSessionStore() })
assertPanics("csrf store", func() { _ = GetCSRFStore() })
assertPanics("recovery token store", func() { _ = GetRecoveryTokenStore() })
}
func TestContract_HostedSessionAuthPrecedesAnonymousFallback(t *testing.T) {
resetPersistentAuthStoresForTests()
t.Cleanup(resetPersistentAuthStoresForTests)
InitSessionStore(t.TempDir())
store := GetSessionStore()
sessionToken := generateSessionToken()
store.CreateSession(sessionToken, 24*time.Hour, "contract-test", "127.0.0.1", "hosted-owner@example.com")
record, err := config.NewAPITokenRecord("hosted-contract-token.12345678", "hosted-contract", []string{config.ScopeSettingsWrite})
if err != nil {
t.Fatalf("NewAPITokenRecord: %v", err)
}
cfg := &config.Config{
APITokens: []config.APITokenRecord{*record},
}
req := httptest.NewRequest(http.MethodGet, "/api/security/tokens/relay-mobile", nil)
req.AddCookie(&http.Cookie{
Name: "pulse_session",
Value: sessionToken,
})
rec := httptest.NewRecorder()
if !CheckAuth(cfg, rec, req) {
t.Fatal("CheckAuth() = false, want true for valid hosted browser session")
}
if got := rec.Header().Get("X-Authenticated-User"); got != "hosted-owner@example.com" {
t.Fatalf("X-Authenticated-User = %q, want hosted-owner@example.com", got)
}
if got := rec.Header().Get("X-Auth-Method"); got != "session" {
t.Fatalf("X-Auth-Method = %q, want session", got)
}
}
func TestContract_SecureHostedSessionRequiresHostPrefixedCookie(t *testing.T) {
resetPersistentAuthStoresForTests()
t.Cleanup(resetPersistentAuthStoresForTests)
InitSessionStore(t.TempDir())
store := GetSessionStore()
sessionToken := generateSessionToken()
store.CreateSession(sessionToken, 24*time.Hour, "contract-test", "127.0.0.1", "hosted-owner@example.com")
record, err := config.NewAPITokenRecord("hosted-secure-session-token.12345678", "hosted-secure-session", []string{config.ScopeSettingsWrite})
if err != nil {
t.Fatalf("NewAPITokenRecord: %v", err)
}
cfg := &config.Config{
APITokens: []config.APITokenRecord{*record},
}
t.Run("legacy cookie is rejected on secure requests", func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "https://pulse.example.test/api/security/tokens/relay-mobile", nil)
req.TLS = &tls.ConnectionState{}
req.AddCookie(&http.Cookie{
Name: cookieNameSession,
Value: sessionToken,
})
rec := httptest.NewRecorder()
if CheckAuth(cfg, rec, req) {
t.Fatal("CheckAuth() = true, want false for legacy session cookie on secure request")
}
if rec.Code != http.StatusUnauthorized {
t.Fatalf("status = %d, want %d when secure request only presents legacy cookie", rec.Code, http.StatusUnauthorized)
}
})
t.Run("host-prefixed cookie remains valid on secure requests", func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "https://pulse.example.test/api/security/tokens/relay-mobile", nil)
req.TLS = &tls.ConnectionState{}
req.AddCookie(&http.Cookie{
Name: cookieNameSessionSecure,
Value: sessionToken,
})
rec := httptest.NewRecorder()
if !CheckAuth(cfg, rec, req) {
t.Fatal("CheckAuth() = false, want true for __Host- session cookie on secure request")
}
if got := rec.Header().Get("X-Authenticated-User"); got != "hosted-owner@example.com" {
t.Fatalf("X-Authenticated-User = %q, want hosted-owner@example.com", got)
}
})
}
func TestContract_UniversalRateLimitStateIsScopedPerRouterConfig(t *testing.T) {
handler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
})
first := UniversalRateLimitMiddlewareWithConfig(newEndpointRateLimitConfig(), handler)
second := UniversalRateLimitMiddlewareWithConfig(newEndpointRateLimitConfig(), handler)
makeRequest := func(target http.Handler) *httptest.ResponseRecorder {
t.Helper()
req := httptest.NewRequest(http.MethodPost, "/api/auth/login", nil)
req.RemoteAddr = "198.51.100.25:12345"
rec := httptest.NewRecorder()
target.ServeHTTP(rec, req)
return rec
}
for i := 0; i < 10; i++ {
if rec := makeRequest(first); rec.Code != http.StatusOK {
t.Fatalf("first router request %d status = %d, want %d", i+1, rec.Code, http.StatusOK)
}
}
if rec := makeRequest(first); rec.Code != http.StatusTooManyRequests {
t.Fatalf("first router overflow status = %d, want %d", rec.Code, http.StatusTooManyRequests)
}
if rec := makeRequest(second); rec.Code != http.StatusOK {
t.Fatalf("second router first request status = %d, want %d", rec.Code, http.StatusOK)
}
}
func TestContract_RouterShutdownClosesOwnedPersistentAuthStoresAfterGlobalRebind(t *testing.T) {
resetPersistentAuthStoresForTests()
t.Cleanup(resetPersistentAuthStoresForTests)
routerOne := NewRouter(&config.Config{DataPath: t.TempDir()}, nil, nil, nil, nil, "1.0.0")
routerTwo := NewRouter(&config.Config{DataPath: t.TempDir()}, nil, nil, nil, nil, "1.0.0")
t.Cleanup(routerTwo.shutdownBackgroundWorkers)
if routerOne.sessionStore == nil || routerOne.csrfStore == nil {
t.Fatal("routerOne should capture initialized persistent auth stores")
}
if routerOne.recoveryTokenStore == nil {
t.Fatal("routerOne should capture initialized recovery token store")
}
if routerTwo.sessionStore == nil || routerTwo.csrfStore == nil {
t.Fatal("routerTwo should capture initialized persistent auth stores")
}
if routerTwo.recoveryTokenStore == nil {
t.Fatal("routerTwo should capture initialized recovery token store")
}
if routerOne.sessionStore == routerTwo.sessionStore {
t.Fatal("router instances should not share the same session store after rebind")
}
if routerOne.csrfStore == routerTwo.csrfStore {
t.Fatal("router instances should not share the same csrf store after rebind")
}
if routerOne.recoveryTokenStore == routerTwo.recoveryTokenStore {
t.Fatal("router instances should not share the same recovery token store after rebind")
}
routerOne.shutdownBackgroundWorkers()
select {
case <-routerOne.sessionStore.workerDone:
default:
t.Fatal("routerOne session store worker should be closed after router shutdown")
}
select {
case <-routerOne.csrfStore.workerDone:
default:
t.Fatal("routerOne csrf store worker should be closed after router shutdown")
}
select {
case <-routerTwo.sessionStore.workerDone:
t.Fatal("routerTwo session store should remain active when routerOne shuts down")
default:
}
select {
case <-routerTwo.csrfStore.workerDone:
t.Fatal("routerTwo csrf store should remain active when routerOne shuts down")
default:
}
select {
case <-routerOne.recoveryTokenStore.stopCleanup:
default:
t.Fatal("routerOne recovery token store should be closed after router shutdown")
}
select {
case <-routerTwo.recoveryTokenStore.stopCleanup:
t.Fatal("routerTwo recovery token store should remain active when routerOne shuts down")
default:
}
}
func TestContract_HostedOrgManagerSessionCanMintRelayMobileToken(t *testing.T) {
defer SetMultiTenantEnabled(false)
SetMultiTenantEnabled(true)
t.Setenv("PULSE_DEV", "true")
dataDir := t.TempDir()
hashed, err := authpkg.HashPassword("Password!1")
if err != nil {
t.Fatalf("hash password: %v", err)
}
cfg := &config.Config{
DataPath: dataDir,
ConfigPath: dataDir,
AuthUser: "platform-admin",
AuthPass: hashed,
}
mtp := config.NewMultiTenantPersistence(dataDir)
org := &models.Organization{
ID: "org-a",
DisplayName: "Org A",
OwnerUserID: "operator-owner",
Members: []models.OrganizationMember{
{UserID: "legacy-owner", Role: models.OrgRoleOwner, AddedAt: time.Now()},
{UserID: "operator-owner", Role: models.OrgRoleOwner, AddedAt: time.Now()},
},
}
if err := mtp.SaveOrganization(org); err != nil {
t.Fatalf("save organization: %v", err)
}
router := newMultiTenantRouter(t, cfg)
setLicenseTierForHandlersForTests(t, router.licenseHandlers, "org-a", pkglicensing.TierRelay)
sessionToken := "relay-owner-session-" + strings.ReplaceAll(time.Now().UTC().Format(time.RFC3339Nano), ":", "-")
GetSessionStore().CreateSession(sessionToken, time.Hour, "agent", "127.0.0.1", "legacy-owner")
req := httptest.NewRequest(http.MethodPost, "/api/security/tokens/relay-mobile", strings.NewReader(`{}`))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-CSRF-Token", generateCSRFToken(sessionToken))
req.Header.Set("X-Pulse-Org-ID", "org-a")
req.AddCookie(&http.Cookie{Name: cookieNameSession, Value: sessionToken})
rec := httptest.NewRecorder()
router.Handler().ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("relay mobile token route status = %d, want %d: %s", rec.Code, http.StatusOK, rec.Body.String())
}
var payload struct {
Token string `json:"token"`
Record apiTokenDTO `json:"record"`
}
if err := json.NewDecoder(rec.Body).Decode(&payload); err != nil {
t.Fatalf("decode relay mobile response: %v", err)
}
if payload.Token == "" {
t.Fatal("expected relay mobile token in response")
}
if payload.Record.OwnerUserID != "legacy-owner" {
t.Fatalf("ownerUserId = %q, want legacy-owner", payload.Record.OwnerUserID)
}
if len(cfg.APITokens) != 1 {
t.Fatalf("expected one stored token, got %d", len(cfg.APITokens))
}
if cfg.APITokens[0].OrgID != "org-a" {
t.Fatalf("stored token orgId = %q, want org-a", cfg.APITokens[0].OrgID)
}
}
func TestContract_RelayMobileScopeCanReadOnboardingDeepLink(t *testing.T) {
rawToken := "relay-mobile-onboarding-token-123.12345678"
record := newTokenRecord(t, rawToken, []string{config.ScopeRelayMobileAccess}, nil)
cfg := newTestConfigWithTokens(t, record)
router := NewRouter(cfg, nil, nil, nil, nil, "1.0.0")
req := httptest.NewRequest(http.MethodGet, "/api/onboarding/deep-link", nil)
req.Header.Set("X-API-Token", rawToken)
rec := httptest.NewRecorder()
router.Handler().ServeHTTP(rec, req)
if rec.Code == http.StatusForbidden && strings.Contains(rec.Body.String(), "missing_scope") {
t.Fatalf("relay mobile scope should satisfy onboarding deep-link gating, got %d: %s", rec.Code, rec.Body.String())
}
}
func TestContract_OnboardingDeepLinkIgnoresQueryAuthToken(t *testing.T) {
rawToken := "relay-mobile-onboarding-query-token.12345678"
record := newTokenRecord(t, rawToken, []string{config.ScopeRelayMobileAccess}, nil)
cfg := newTestConfigWithTokens(t, record)
router := NewRouter(cfg, nil, nil, nil, nil, "1.0.0")
req := httptest.NewRequest(http.MethodGet, "/api/onboarding/deep-link?auth_token="+url.QueryEscape(rawToken), nil)
rec := httptest.NewRecorder()
router.Handler().ServeHTTP(rec, req)
if rec.Code != http.StatusUnauthorized {
t.Fatalf("status = %d, want %d when auth token is only provided in query string", rec.Code, http.StatusUnauthorized)
}
}
func TestContract_SetupTokenRoutesRejectQueryAuthToken(t *testing.T) {
cfg := newTestConfigWithTokens(t, newTokenRecord(t, "settings-write-token", []string{config.ScopeSettingsWrite}, nil))
router := NewRouter(cfg, nil, nil, nil, nil, "1.0.0")
t.Setenv("HOME", t.TempDir())
token := "fedcba9876543210fedcba9876543210"
tokenHash := authpkg.HashAPIToken(token)
router.configHandlers.codeMutex.Lock()
router.configHandlers.setupTokens[tokenHash] = &SetupTokenRecord{ExpiresAt: time.Now().Add(time.Minute)}
router.configHandlers.codeMutex.Unlock()
t.Run("verify-temperature-ssh requires header token transport", func(t *testing.T) {
req := httptest.NewRequest(http.MethodPost, "/api/system/verify-temperature-ssh?auth_token="+token, strings.NewReader(`{"nodes":""}`))
rec := httptest.NewRecorder()
router.Handler().ServeHTTP(rec, req)
if rec.Code != http.StatusUnauthorized {
t.Fatalf("status = %d, want %d when setup token is only in query string", rec.Code, http.StatusUnauthorized)
}
req = httptest.NewRequest(http.MethodPost, "/api/system/verify-temperature-ssh", strings.NewReader(`{"nodes":""}`))
req.Header.Set("X-Setup-Token", token)
rec = httptest.NewRecorder()
router.Handler().ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status = %d, want %d when setup token is provided in header", rec.Code, http.StatusOK)
}
})
t.Run("ssh-config requires header token transport", func(t *testing.T) {
req := httptest.NewRequest(http.MethodPost, "/api/system/ssh-config?auth_token="+token, strings.NewReader("Host example\nHostname example\n"))
rec := httptest.NewRecorder()
router.Handler().ServeHTTP(rec, req)
if rec.Code != http.StatusUnauthorized {
t.Fatalf("status = %d, want %d when setup token is only in query string", rec.Code, http.StatusUnauthorized)
}
req = httptest.NewRequest(http.MethodPost, "/api/system/ssh-config", strings.NewReader("Host example\nHostname example\n"))
req.Header.Set("X-Setup-Token", token)
rec = httptest.NewRecorder()
router.Handler().ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status = %d, want %d when setup token is provided in header", rec.Code, http.StatusOK)
}
})
}
func TestContract_RelayMobileScopeCannotReadApprovalDetail(t *testing.T) {
rawToken := "relay-mobile-approval-detail-token-123.12345678"
record := newTokenRecord(t, rawToken, []string{config.ScopeRelayMobileAccess}, nil)
cfg := newTestConfigWithTokens(t, record)
router := NewRouter(cfg, nil, nil, nil, nil, "1.0.0")
req := httptest.NewRequest(http.MethodGet, "/api/ai/approvals/approval-1", nil)
req.Header.Set("X-API-Token", rawToken)
rec := httptest.NewRecorder()
router.Handler().ServeHTTP(rec, req)
if rec.Code != http.StatusForbidden {
t.Fatalf("relay mobile scope should not satisfy approval detail gating, got %d: %s", rec.Code, rec.Body.String())
}
if !strings.Contains(rec.Body.String(), config.ScopeAIExecute) {
t.Fatalf("relay mobile approval detail rejection should mention %q, got %s", config.ScopeAIExecute, rec.Body.String())
}
}
func TestContract_UnifiedAgentReportResponseJSONSnapshot(t *testing.T) {
payload := map[string]any{
"success": true,
"agentId": "agent-123",
"lastSeen": "2026-02-08T13:14:15Z",
"platform": "linux",
"osName": "Debian GNU/Linux",
"osVersion": "12",
"config": map[string]any{
"commandsEnabled": true,
},
}
got, err := json.Marshal(payload)
if err != nil {
t.Fatalf("marshal unified agent report response: %v", err)
}
const want = `{
"agentId":"agent-123",
"config":{"commandsEnabled":true},
"lastSeen":"2026-02-08T13:14:15Z",
"osName":"Debian GNU/Linux",
"osVersion":"12",
"platform":"linux",
"success":true
}`
assertJSONSnapshot(t, got, want)
}
func TestContract_UnifiedAgentLookupFailsClosedOnAmbiguousHostname(t *testing.T) {
handler := newUnifiedAgentHandlerForTests(t,
models.Host{ID: "host-1", Hostname: "webserver.corp.example.com", DisplayName: "Web One"},
models.Host{ID: "host-2", Hostname: "webserver.other.example.com", DisplayName: "Web Two"},
)
req := httptest.NewRequest(http.MethodGet, "/api/agents/agent/lookup?hostname=webserver", nil)
rec := httptest.NewRecorder()
handler.HandleLookup(rec, req)
if rec.Code != http.StatusNotFound {
t.Fatalf("expected not found status for ambiguous hostname lookup, got %d: %s", rec.Code, rec.Body.String())
}
var resp struct {
Error string `json:"error"`
Code string `json:"code"`
}
if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil {
t.Fatalf("decode ambiguous lookup error response: %v", err)
}
if resp.Code != "agent_not_found" {
t.Fatalf("expected agent_not_found code, got %q", resp.Code)
}
}
func TestContract_HostsShareResolvedIdentityTreatsLoopbackAliasAsSameNode(t *testing.T) {
if !hostsShareResolvedIdentity("https://localhost:7655", "https://127.0.0.1:7655") {
t.Fatal("expected localhost and loopback IP to resolve as the same host identity")
}
if hostsShareResolvedIdentity("https://192.0.2.10:7655", "https://192.0.2.11:7655") {
t.Fatal("expected different IP endpoints to remain distinct host identities")
}
}
func TestContract_DiagnosticsDockerPrepareTokenInstallCommandUsesLifecycleTransport(t *testing.T) {
baseURL := "https://pulse.example.com/base"
got := buildContainerRuntimeAgentInstallCommand(baseURL, "token-123")
if !strings.Contains(got, posixShellQuote(baseURL+"/install.sh")) {
t.Fatalf("install command missing normalized install script URL: %s", got)
}
if !strings.Contains(got, "--enable-host=false") {
t.Fatalf("install command missing canonical host-disable flag: %s", got)
}
if strings.Contains(got, "--disable-host") {
t.Fatalf("install command preserved stale disable-host flag: %s", got)
}
if !strings.Contains(got, `| { if [ "$(id -u)" -eq 0 ]; then bash -s --`) {
t.Fatalf("install command missing governed root-or-sudo wrapper: %s", got)
}
if strings.Contains(got, "curl -fsSL "+posixShellQuote(baseURL+"/install.sh")+" | sudo bash -s --") {
t.Fatalf("install command preserved raw sudo pipe instead of governed wrapper: %s", got)
}
}
func TestContract_DiagnosticsDockerPrepareTokenOptionalAuthInstallCommandOmitsToken(t *testing.T) {
got := buildContainerRuntimeAgentInstallCommand("http://pulse.example.com:7655/", "")
if strings.Contains(got, "--token") {
t.Fatalf("optional-auth install command preserved token flag: %s", got)
}
if !strings.Contains(got, "--insecure") {
t.Fatalf("optional-auth install command missing insecure flag for plain HTTP Pulse URL: %s", got)
}
}
func TestContract_SetupScriptURLCommandUsesFailFastQuotedTransport(t *testing.T) {
url := "https://pulse.example.com/api/setup-script?type=pve&host=pve1.local"
got := buildSetupScriptCommand(url, "token-123")
if !strings.Contains(got, "curl -fsSL "+posixShellQuote(url)+" | ") {
t.Fatalf("setup-script command missing canonical fail-fast transport: %s", got)
}
if !strings.Contains(got, `if [ "$(id -u)" -eq 0 ]; then PULSE_SETUP_TOKEN=`+posixShellQuote("token-123")+` bash`) {
t.Fatalf("setup-script command missing direct-root execution path: %s", got)
}
if !strings.Contains(got, `elif command -v sudo >/dev/null 2>&1; then sudo env PULSE_SETUP_TOKEN=`+posixShellQuote("token-123")+` bash`) {
t.Fatalf("setup-script command missing sudo execution path: %s", got)
}
if strings.Contains(got, "curl -sSL ") {
t.Fatalf("setup-script command preserved stale non-fail-fast curl transport: %s", got)
}
}
func TestContract_SetupScriptEmbedsFailFastGuidance(t *testing.T) {
tempDir := t.TempDir()
cfg := &config.Config{
DataPath: tempDir,
ConfigPath: tempDir,
}
handlers := newTestConfigHandlers(t, cfg)
req := httptest.NewRequest(http.MethodGet,
"/api/setup-script?type=pve&host=http://sentinel-host:8006&pulse_url=http://sentinel-url:7656", nil)
rec := httptest.NewRecorder()
handlers.HandleSetupScript(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status = %d, want %d", rec.Code, http.StatusOK)
}
script := rec.Body.String()
if !strings.Contains(script, `PULSE_BOOTSTRAP_COMMAND_WITH_ENV='curl -fsSL '"'"'http://sentinel-url:7656/api/setup-script?host=http%3A%2F%2Fsentinel-host%3A8006&pulse_url=http%3A%2F%2Fsentinel-url%3A7656&type=pve'"'"' | `) {
t.Fatalf("setup script missing canonical bootstrap command owner: %s", script)
}
if strings.Contains(script, `PULSE_BOOTSTRAP_COMMAND_WITH_ENV='curl -fsSL '"'"'http://sentinel-url:7656/api/setup-script?host=http%3A%2F%2Fsentinel-host%3A8006&pulse_url=http%3A%2F%2Fsentinel-url%3A7656&type=pve'"'"' | { if [ "$(id -u)" -eq 0 ]; then PULSE_SETUP_TOKEN=`) {
t.Fatalf("setup script bootstrap command should defer setup token to runtime hydration, got: %s", script)
}
if !strings.Contains(script, `echo " $PULSE_BOOTSTRAP_COMMAND_WITH_ENV"`) {
t.Fatalf("setup script missing bootstrap-command retry guidance: %s", script)
}
if !strings.Contains(script, `SETUP_SCRIPT_URL="http://sentinel-url:7656/api/setup-script?host=http%3A%2F%2Fsentinel-host%3A8006&pulse_url=http%3A%2F%2Fsentinel-url%3A7656&type=pve"`) {
t.Fatalf("setup script missing canonical encoded retry URL: %s", script)
}
if !strings.Contains(script, `PULSE_SETUP_TOKEN="${PULSE_SETUP_TOKEN:-`) {
t.Fatalf("setup script missing canonical PVE setup-token initialization before rerun guidance: %s", script)
}
if !strings.Contains(script, `echo "Root privileges required. Run as root (su -) and retry."`) {
t.Fatalf("setup script missing canonical root requirement guidance: %s", script)
}
if !strings.Contains(script, `echo "This setup flow must run on the Proxmox host so Pulse can create"`) {
t.Fatalf("setup script missing canonical off-host rerun guidance: %s", script)
}
if strings.Contains(script, `echo " curl -sSL \"$SETUP_SCRIPT_URL\" | bash"`) || strings.Contains(script, `echo " curl -sSL \"$PULSE_URL/api/setup-script?type=pve&host=YOUR_PVE_URL&pulse_url=$PULSE_URL\" | bash"`) {
t.Fatalf("setup script preserved stale non-fail-fast guidance: %s", script)
}
if strings.Contains(script, `echo "Manual setup steps:"`) || strings.Contains(script, `echo " 2. In Pulse: Settings → Nodes → Add Node (enter token from above)"`) {
t.Fatalf("setup script preserved stale off-host manual token flow: %s", script)
}
if !strings.Contains(script, `done <<< "$OLD_TOKENS_PVE"`) {
t.Fatalf("setup script missing explicit pve old-token cleanup loop: %s", script)
}
if !strings.Contains(script, `done <<< "$OLD_TOKENS_PAM"`) {
t.Fatalf("setup script missing explicit pam old-token cleanup loop: %s", script)
}
if strings.Contains(script, `done <<< "$OLD_TOKENS"`) {
t.Fatalf("setup script preserved stale undefined old-token cleanup variable: %s", script)
}
if !strings.Contains(script, `pveum user token remove pulse-monitor@pve "$TOKEN"`) {
t.Fatalf("setup script missing pve token cleanup command: %s", script)
}
if !strings.Contains(script, `pveum user token remove pulse-monitor@pam "$TOKEN"`) {
t.Fatalf("setup script missing pam token cleanup command: %s", script)
}
if !strings.Contains(script, `TOKEN_MATCH_PREFIX="pulse-sentinel-url"`) {
t.Fatalf("setup script missing canonical token-match prefix for cleanup discovery: %s", script)
}
if !strings.Contains(script, `grep -E "^${TOKEN_MATCH_PREFIX}(-[0-9]+)?$"`) {
t.Fatalf("setup script missing canonical cleanup token discovery matcher: %s", script)
}
if !strings.Contains(script, `awk 'NR>3 {print $2}' | grep -Fx "$TOKEN_NAME" >/dev/null 2>&1`) {
t.Fatalf("setup script missing exact PVE token rotation detection: %s", script)
}
if strings.Contains(script, `PULSE_IP_PATTERN=`) {
t.Fatalf("setup script preserved stale ip-pattern cleanup discovery: %s", script)
}
if strings.Contains(script, `grep -q "$TOKEN_NAME"`) {
t.Fatalf("setup script preserved stale broad PVE token rotation detection: %s", script)
}
if !strings.Contains(script, `resolve_authorized_keys_path() {`) {
t.Fatalf("setup script missing authorized_keys symlink resolver: %s", script)
}
if !strings.Contains(script, `resolved="$(readlink -f "$auth_keys" 2>/dev/null || true)"`) {
t.Fatalf("setup script missing canonical authorized_keys target resolution: %s", script)
}
if !strings.Contains(script, `install_authorized_keys_file() {`) {
t.Fatalf("setup script missing shared authorized_keys install helper: %s", script)
}
if strings.Count(script, `AUTH_KEYS="$(resolve_authorized_keys_path)"`) < 2 {
t.Fatalf("setup script install and uninstall paths must both resolve authorized_keys symlinks: %s", script)
}
if !strings.Contains(script, `grep -vF '# pulse-' "$AUTH_KEYS" > "$TMP_AUTH_KEYS" 2>/dev/null`) {
t.Fatalf("setup script uninstall path must remove all Pulse-managed SSH keys through the resolved authorized_keys target: %s", script)
}
if !strings.Contains(script, `grep -vF "# pulse-" "$AUTH_KEYS" > "$TMP_AUTH_KEYS" 2>/dev/null`) {
t.Fatalf("setup script install path must filter existing Pulse-managed SSH keys through the resolved authorized_keys target: %s", script)
}
if strings.Contains(script, `grep -vF '# pulse-managed-key'`) {
t.Fatalf("setup script preserved stale managed-key-only cleanup: %s", script)
}
if strings.Contains(script, `/root/.ssh/authorized_keys.tmp`) || strings.Contains(script, `echo "$SSH_SENSORS_KEY_ENTRY" >> /root/.ssh/authorized_keys`) {
t.Fatalf("setup script preserved symlink-breaking authorized_keys rewrite: %s", script)
}
if strings.Contains(script, `echo "Please run this script as root"`) {
t.Fatalf("setup script preserved stale root-only guidance: %s", script)
}
if !strings.Contains(script, `grep -Eq '"status"[[:space:]]*:[[:space:]]*"success"'`) {
t.Fatalf("setup script missing secure success detection: %s", script)
}
if strings.Contains(script, `grep -q "success"`) {
t.Fatalf("setup script preserved broad success substring detection: %s", script)
}
if !strings.Contains(script, `curl -fsS -X POST "$PULSE_URL/api/auto-register"`) {
t.Fatalf("setup script missing fail-fast auto-register transport: %s", script)
}
if !strings.Contains(script, `curl -fsS -X POST "$PULSE_URL/api/auto-unregister"`) {
t.Fatalf("setup script missing fail-fast auto-unregister transport: %s", script)
}
if !strings.Contains(script, `"source":"script"`) {
t.Fatalf("setup script missing canonical /api/auto-register source marker: %s", script)
}
if !strings.Contains(script, `echo " • Removing Pulse connection from server..."`) {
t.Fatalf("setup script missing canonical server-side teardown guidance: %s", script)
}
if !strings.Contains(script, `REGISTER_RC=$?`) {
t.Fatalf("setup script missing explicit auto-register curl exit-code handling: %s", script)
}
if !strings.Contains(script, `echo "⚠️ Auto-registration skipped: token value unavailable"`) {
t.Fatalf("setup script missing fail-closed token-value-unavailable guidance: %s", script)
}
if strings.Contains(script, `curl -s -X POST "$PULSE_URL/api/auto-register"`) {
t.Fatalf("setup script preserved stale non-fail-fast auto-register transport: %s", script)
}
if !strings.Contains(script, `echo "The provided Pulse setup token was invalid or expired"`) {
t.Fatalf("setup script missing invalid setup-token guidance: %s", script)
}
if !strings.Contains(script, `echo "Get a fresh setup token from Pulse Settings → Nodes and rerun this script."`) {
t.Fatalf("setup script missing fresh setup-token rerun guidance: %s", script)
}
if !strings.Contains(script, `SETUP_TOKEN_INVALID=true`) {
t.Fatalf("setup script missing PVE auth-failure state tracking: %s", script)
}
if !strings.Contains(script, `echo "Pulse setup token authentication failed."`) {
t.Fatalf("setup script missing PVE auth-failure completion guidance: %s", script)
}
if !strings.Contains(script, `if [ "$AUTO_REG_SUCCESS" != true ] && [ "$SETUP_TOKEN_INVALID" != true ]; then`) {
t.Fatalf("setup script missing PVE auth-failure footer guard: %s", script)
}
if !strings.Contains(script, `echo "📝 Use the token details below in Pulse Settings → Nodes to finish registration."`) {
t.Fatalf("setup script missing canonical auto-register failure continuation guidance: %s", script)
}
if strings.Contains(script, `echo "To enable auto-registration, add your API token to the setup URL"`) {
t.Fatalf("setup script preserved stale API-token auth guidance: %s", script)
}
if strings.Contains(script, `echo "The provided API token was invalid"`) {
t.Fatalf("setup script preserved stale invalid API-token guidance: %s", script)
}
if strings.Contains(script, `echo "To enable auto-registration, rerun with a valid Pulse setup token"`) {
t.Fatalf("setup script preserved stale split setup-token auth guidance: %s", script)
}
if strings.Contains(script, `echo "📝 For manual setup:"`) {
t.Fatalf("setup script preserved stale numbered manual-setup fallback: %s", script)
}
if strings.Contains(script, `echo " 2. Add this node manually in Pulse Settings"`) {
t.Fatalf("setup script preserved stale auto-register failure continuation guidance: %s", script)
}
if !strings.Contains(script, `echo "Pulse monitoring token setup completed."`) {
t.Fatalf("setup script missing truthful manual completion messaging: %s", script)
}
if !strings.Contains(script, `echo "Pulse monitoring token setup failed."`) {
t.Fatalf("setup script missing token-create failure completion messaging: %s", script)
}
if !strings.Contains(script, `echo "Fix the token creation error above and rerun this script on the node."`) {
t.Fatalf("setup script missing immediate token-create failure rerun guidance: %s", script)
}
if !strings.Contains(script, `echo "Resolve the token creation error shown above and rerun this script on the node."`) {
t.Fatalf("setup script missing token-create failure rerun guidance: %s", script)
}
if !strings.Contains(script, `echo " Resolve the token output issue above and rerun this script on the node."`) {
t.Fatalf("setup script missing token-extract failure rerun guidance: %s", script)
}
if !strings.Contains(script, `echo "Successfully registered with Pulse monitoring."`) {
t.Fatalf("setup script missing canonical success messaging: %s", script)
}
if !strings.Contains(script, `echo " Token Value: [See token output above]"`) {
t.Fatalf("setup script missing canonical token placeholder guidance: %s", script)
}
if !strings.Contains(script, `echo "Finish registration in Pulse using the manual setup details below."`) {
t.Fatalf("setup script missing truthful manual registration guidance: %s", script)
}
if !strings.Contains(script, `echo "Add this server to Pulse with:"`) {
t.Fatalf("setup script missing canonical manual-add heading: %s", script)
}
if !strings.Contains(script, `echo "Use these details in Pulse Settings → Nodes to finish registration."`) {
t.Fatalf("setup script missing canonical manual-add continuation guidance: %s", script)
}
if !strings.Contains(script, `echo "⚠️ Auto-registration failed. Finish registration manually in Pulse Settings → Nodes."`) {
t.Fatalf("setup script missing canonical auto-register failure summary: %s", script)
}
if !strings.Contains(script, `echo " Host URL: $SERVER_HOST"`) {
t.Fatalf("setup script missing canonical manual host continuity: %s", script)
}
if strings.Contains(script, `echo "Manual setup instructions:"`) {
t.Fatalf("setup script preserved stale manual setup heading: %s", script)
}
if strings.Contains(script, `echo "Node registered successfully"`) || strings.Contains(script, `echo "Node successfully registered with Pulse monitoring."`) || strings.Contains(script, `echo "✅ Successfully registered with Pulse!"`) || strings.Contains(script, `echo "Server successfully registered with Pulse monitoring."`) {
t.Fatalf("setup script preserved stale success copy variants: %s", script)
}
if strings.Contains(script, `echo " Token Value: [See above]"`) || strings.Contains(script, `echo " Token Value: [Check the output above for the token or instructions]"`) {
t.Fatalf("setup script preserved stale token placeholder guidance: %s", script)
}
if strings.Contains(script, `echo "⚠️ Auto-registration failed. Manual configuration may be needed."`) {
t.Fatalf("setup script preserved stale auto-register failure summary: %s", script)
}
if strings.Contains(script, `PULSE_REG_TOKEN=your-token ./setup.sh`) {
t.Fatalf("setup script preserved stale rerun token guidance: %s", script)
}
if strings.Contains(script, `echo "Manual registration may be required."`) {
t.Fatalf("setup script preserved stale manual-registration token failure guidance: %s", script)
}
if strings.Contains(script, `echo " Host URL: YOUR_PROXMOX_HOST:8006"`) {
t.Fatalf("setup script preserved stale placeholder manual host guidance: %s", script)
}
if !strings.Contains(script, `echo "Pulse monitoring token setup could not be completed."`) {
t.Fatalf("setup script missing token-extract failure completion messaging: %s", script)
}
if !strings.Contains(script, `echo "Resolve the token output issue shown above and rerun this script on the node."`) {
t.Fatalf("setup script missing token-extract completion rerun guidance: %s", script)
}
if !strings.Contains(script, `if [ "$TOKEN_READY" = true ]; then
attempt_auto_registration
else
AUTO_REG_SUCCESS=false
fi`) {
t.Fatalf("setup script does not skip PVE auto-registration when no usable token is ready: %s", script)
}
if !strings.Contains(script, `if [ "$TOKEN_READY" = true ]; then
echo "Add this server to Pulse with:"`) {
t.Fatalf("setup script does not gate PVE manual token details on usable token extraction: %s", script)
}
if strings.Contains(script, `elif [ "$TOKEN_READY" != true ]; then
echo "Pulse monitoring token setup completed."`) {
t.Fatalf("setup script lets PVE token-extract failure fall through to completed token setup: %s", script)
}
pbsReq := httptest.NewRequest(http.MethodGet,
"/api/setup-script?type=pbs&host=https://sentinel-pbs:8007&pulse_url=http://sentinel-url:7656", nil)
pbsRec := httptest.NewRecorder()
handlers.HandleSetupScript(pbsRec, pbsReq)
if pbsRec.Code != http.StatusOK {
t.Fatalf("pbs status = %d, want %d", pbsRec.Code, http.StatusOK)
}
pbsScript := pbsRec.Body.String()
if !strings.Contains(pbsScript, `echo " Host URL: $HOST_URL"`) {
t.Fatalf("setup script missing canonical PBS manual host continuity: %s", pbsScript)
}
if !strings.Contains(pbsScript, `echo "Pulse monitoring token setup failed."`) {
t.Fatalf("setup script missing PBS token-create failure completion messaging: %s", pbsScript)
}
if !strings.Contains(pbsScript, `echo "Pulse monitoring token setup could not be completed."`) {
t.Fatalf("setup script missing PBS token-extract failure completion messaging: %s", pbsScript)
}
if !strings.Contains(pbsScript, `echo "⚠️ Auto-registration skipped: no setup token provided"`) {
t.Fatalf("setup script missing PBS setup-token-skip guidance: %s", pbsScript)
}
if !strings.Contains(pbsScript, `PULSE_SETUP_TOKEN="${PULSE_SETUP_TOKEN:-`) {
t.Fatalf("setup script missing canonical PBS setup-token initialization before rerun guidance: %s", pbsScript)
}
if strings.Contains(pbsScript, `echo "⚠️ Auto-registration skipped: no setup token provided"
AUTO_REG_SUCCESS=false
REGISTER_RESPONSE=""
REGISTER_RC=1`) {
t.Fatalf("setup script still forces fake PBS request-failure state after missing setup-token skip: %s", pbsScript)
}
if strings.Contains(pbsScript, `echo "⚠️ Auto-registration skipped: token value unavailable"
AUTO_REG_SUCCESS=false
REGISTER_RESPONSE=""
REGISTER_RC=1`) {
t.Fatalf("setup script still forces fake PBS request-failure state after token-value skip: %s", pbsScript)
}
if !strings.Contains(pbsScript, `if [ "$REGISTER_ATTEMPTED" != true ]; then`) {
t.Fatalf("setup script does not distinguish skipped PBS auto-registration paths from attempted requests: %s", pbsScript)
}
if !strings.Contains(pbsScript, `echo "The provided Pulse setup token was invalid or expired"`) {
t.Fatalf("setup script missing invalid PBS setup-token guidance: %s", pbsScript)
}
if !strings.Contains(pbsScript, `echo "Get a fresh setup token from Pulse Settings → Nodes and rerun this script."`) {
t.Fatalf("setup script missing fresh PBS setup-token rerun guidance: %s", pbsScript)
}
if !strings.Contains(pbsScript, `SETUP_TOKEN_INVALID=true`) {
t.Fatalf("setup script missing PBS auth-failure state tracking: %s", pbsScript)
}
if !strings.Contains(pbsScript, `echo "Pulse setup token authentication failed."`) {
t.Fatalf("setup script missing PBS auth-failure completion guidance: %s", pbsScript)
}
if !strings.Contains(pbsScript, `if [ "$AUTO_REG_SUCCESS" != true ] && [ "$SETUP_TOKEN_INVALID" != true ]; then`) {
t.Fatalf("setup script missing PBS auth-failure footer guard: %s", pbsScript)
}
if !strings.Contains(pbsScript, `echo "Fix the token creation error above and rerun this script on the node."`) {
t.Fatalf("setup script missing PBS immediate token-create failure rerun guidance: %s", pbsScript)
}
if !strings.Contains(pbsScript, `echo "Resolve the token creation error shown above and rerun this script on the node."`) {
t.Fatalf("setup script missing PBS token-create failure rerun guidance: %s", pbsScript)
}
if !strings.Contains(pbsScript, `echo " Resolve the token output issue above and rerun this script on the node."`) {
t.Fatalf("setup script missing PBS token-extract failure rerun guidance: %s", pbsScript)
}
if !strings.Contains(pbsScript, `echo "Resolve the token output issue shown above and rerun this script on the node."`) {
t.Fatalf("setup script missing PBS token-extract completion rerun guidance: %s", pbsScript)
}
if !strings.Contains(pbsScript, `HOST_URL="https://sentinel-pbs:8007"`) {
t.Fatalf("setup script missing canonical PBS host binding: %s", pbsScript)
}
if !strings.Contains(pbsScript, `TOKEN_MATCH_PREFIX="pulse-sentinel-url"`) {
t.Fatalf("pbs setup script missing canonical token-match prefix for cleanup discovery: %s", pbsScript)
}
if !strings.Contains(pbsScript, `grep -oE "${TOKEN_MATCH_PREFIX}(-[0-9]+)?" | sort -u || true`) {
t.Fatalf("pbs setup script missing canonical cleanup token discovery matcher: %s", pbsScript)
}
if !strings.Contains(pbsScript, `awk '{print $1}' | grep -Fx "$TOKEN_NAME" >/dev/null 2>&1`) {
t.Fatalf("pbs setup script missing exact token rotation detection: %s", pbsScript)
}
tokenCreateIndex := strings.Index(pbsScript, `TOKEN_CREATE_RC=$?`)
bannerIndex := strings.Index(pbsScript, `echo "IMPORTANT: Copy the token value below - it's only shown once!"`)
successBranchIndex := strings.Index(pbsScript, "else\n TOKEN_CREATED=true")
if tokenCreateIndex == -1 || bannerIndex == -1 || successBranchIndex == -1 {
t.Fatalf("pbs setup script missing token-create truth markers: %s", pbsScript)
}
if bannerIndex < tokenCreateIndex {
t.Fatalf("pbs setup script prints token-copy banner before token creation result is known: %s", pbsScript)
}
if bannerIndex < successBranchIndex {
t.Fatalf("pbs setup script prints token-copy banner outside the successful token-create branch: %s", pbsScript)
}
if strings.Contains(pbsScript, `PULSE_IP_PATTERN=`) {
t.Fatalf("pbs setup script preserved stale ip-pattern cleanup discovery: %s", pbsScript)
}
if strings.Contains(pbsScript, `grep -q "$TOKEN_NAME"`) {
t.Fatalf("pbs setup script preserved stale broad token rotation detection: %s", pbsScript)
}
if strings.Index(pbsScript, `HOST_URL="https://sentinel-pbs:8007"`) > strings.Index(pbsScript, `if [ -z "$PULSE_SETUP_TOKEN" ]; then`) {
t.Fatalf("setup script binds PBS host too late for manual fallback continuity: %s", pbsScript)
}
if strings.Index(pbsScript, `HOST_URL="https://sentinel-pbs:8007"`) > strings.Index(pbsScript, `if [ "$TOKEN_CREATE_RC" -ne 0 ]; then`) {
t.Fatalf("setup script binds PBS host too late for token-create failure continuity: %s", pbsScript)
}
if !strings.Contains(pbsScript, `if [ "$TOKEN_READY" = true ]; then
echo "Add this server to Pulse with:"`) {
t.Fatalf("setup script does not gate PBS manual token details on usable token extraction: %s", pbsScript)
}
attemptBannerIndex := strings.Index(pbsScript, `echo "🔄 Attempting auto-registration with Pulse..."`)
authTokenGateIndex := strings.Index(pbsScript, `if [ -n "$PULSE_SETUP_TOKEN" ]; then`)
tokenSkipIndex := strings.Index(pbsScript, `echo "⚠️ Auto-registration skipped: token value unavailable"`)
if attemptBannerIndex == -1 || authTokenGateIndex == -1 || tokenSkipIndex == -1 {
t.Fatalf("setup script missing PBS auto-registration truth markers: %s", pbsScript)
}
if attemptBannerIndex < authTokenGateIndex {
t.Fatalf("setup script prints PBS auto-registration attempt banner before the real request path: %s", pbsScript)
}
if attemptBannerIndex < tokenSkipIndex {
t.Fatalf("setup script prints PBS auto-registration attempt banner before token-unavailable skip handling: %s", pbsScript)
}
if strings.Contains(pbsScript, `elif [ "$TOKEN_READY" != true ]; then
echo "Pulse monitoring token setup completed."`) {
t.Fatalf("setup script lets PBS token-extract failure fall through to completed token setup: %s", pbsScript)
}
if strings.Contains(pbsScript, `echo " Host URL: https://$SERVER_IP:8007"`) {
t.Fatalf("setup script preserved stale PBS runtime-IP host guidance: %s", pbsScript)
}
if strings.Contains(pbsScript, `echo "Manual registration may be required."`) {
t.Fatalf("setup script preserved stale PBS manual-registration token failure guidance: %s", pbsScript)
}
if strings.Contains(pbsScript, `echo "To enable auto-registration, rerun with a valid Pulse setup token"`) {
t.Fatalf("setup script preserved stale split PBS setup-token auth guidance: %s", pbsScript)
}
if !strings.Contains(pbsScript, `echo "⚠️ Auto-registration failed. Finish registration manually in Pulse Settings → Nodes."`) {
t.Fatalf("setup script missing canonical PBS auto-register failure summary: %s", pbsScript)
}
if strings.Count(pbsScript, `echo "📝 Use the token details below in Pulse Settings → Nodes to finish registration."`) < 2 {
t.Fatalf("setup script missing canonical PBS request-failure/manual-response continuity: %s", pbsScript)
}
if strings.Contains(pbsScript, `echo "⚠️ Auto-registration failed. Manual configuration may be needed."`) {
t.Fatalf("setup script preserved stale PBS auto-register failure summary: %s", pbsScript)
}
}
func TestContract_SetupScriptRequiresCanonicalHost(t *testing.T) {
tempDir := t.TempDir()
cfg := &config.Config{
DataPath: tempDir,
ConfigPath: tempDir,
}
handlers := newTestConfigHandlers(t, cfg)
req := httptest.NewRequest(http.MethodGet, "/api/setup-script?type=pve", nil)
rec := httptest.NewRecorder()
handlers.HandleSetupScript(rec, req)
if rec.Code != http.StatusBadRequest {
t.Fatalf("status = %d, want %d", rec.Code, http.StatusBadRequest)
}
if got := strings.TrimSpace(rec.Body.String()); got != "Missing required parameter: host" {
t.Fatalf("body = %q, want canonical missing host guidance", got)
}
}
func TestContract_SetupScriptUsesCanonicalTypeAndHostValidation(t *testing.T) {
tempDir := t.TempDir()
cfg := &config.Config{
DataPath: tempDir,
ConfigPath: tempDir,
}
handlers := newTestConfigHandlers(t, cfg)
invalidTypeReq := httptest.NewRequest(
http.MethodGet,
"/api/setup-script?type=pmg&host=https://node.example.internal:8006&pulse_url=https://pulse.example.com:7655",
nil,
)
invalidTypeRec := httptest.NewRecorder()
handlers.HandleSetupScript(invalidTypeRec, invalidTypeReq)
if invalidTypeRec.Code != http.StatusBadRequest {
t.Fatalf("invalid type status = %d, want %d", invalidTypeRec.Code, http.StatusBadRequest)
}
if got := strings.TrimSpace(invalidTypeRec.Body.String()); got != "type must be 'pve' or 'pbs'" {
t.Fatalf("invalid type body = %q, want canonical type guidance", got)
}
normalizedHostReq := httptest.NewRequest(
http.MethodGet,
"/api/setup-script?type=pve&host=https://pve-node.example.internal&pulse_url=https://pulse.example.com:7655",
nil,
)
normalizedHostRec := httptest.NewRecorder()
handlers.HandleSetupScript(normalizedHostRec, normalizedHostReq)
if normalizedHostRec.Code != http.StatusOK {
t.Fatalf("normalized host status = %d, want %d: %s", normalizedHostRec.Code, http.StatusOK, normalizedHostRec.Body.String())
}
body := normalizedHostRec.Body.String()
if !strings.Contains(body, `SERVER_HOST="https://pve-node.example.internal:8006"`) {
t.Fatalf("normalized host body missing canonical host, got: %s", truncate(body, 500))
}
if !strings.Contains(body, `SETUP_SCRIPT_URL="https://pulse.example.com:7655/api/setup-script?host=https%3A%2F%2Fpve-node.example.internal%3A8006&pulse_url=https%3A%2F%2Fpulse.example.com%3A7655&type=pve"`) {
t.Fatalf("normalized host body missing canonical rerun URL, got: %s", truncate(body, 700))
}
}
func TestContract_SetupScriptRequiresCanonicalPulseURL(t *testing.T) {
tempDir := t.TempDir()
cfg := &config.Config{
DataPath: tempDir,
ConfigPath: tempDir,
}
handlers := newTestConfigHandlers(t, cfg)
req := httptest.NewRequest(http.MethodGet, "/api/setup-script?type=pve&host=https://pve.local:8006", nil)
rec := httptest.NewRecorder()
handlers.HandleSetupScript(rec, req)
if rec.Code != http.StatusBadRequest {
t.Fatalf("status = %d, want %d", rec.Code, http.StatusBadRequest)
}
if got := strings.TrimSpace(rec.Body.String()); got != "Missing required parameter: pulse_url" {
t.Fatalf("body = %q, want canonical missing pulse_url guidance", got)
}
}
func TestContract_SetupScriptUsesCanonicalShellDownloadHeaders(t *testing.T) {
tempDir := t.TempDir()
cfg := &config.Config{
DataPath: tempDir,
ConfigPath: tempDir,
}
handlers := newTestConfigHandlers(t, cfg)
req := httptest.NewRequest(http.MethodGet,
"/api/setup-script?type=pbs&host=https://sentinel-pbs:8007&pulse_url=http://sentinel-url:7656", nil)
rec := httptest.NewRecorder()
handlers.HandleSetupScript(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status = %d, want %d", rec.Code, http.StatusOK)
}
if got := rec.Header().Get("Content-Type"); got != "text/x-shellscript; charset=utf-8" {
t.Fatalf("setup-script content type = %q, want %q", got, "text/x-shellscript; charset=utf-8")
}
if got := rec.Header().Get("Content-Disposition"); got != "attachment; filename=\"pulse-setup-pbs.sh\"" {
t.Fatalf("setup-script content disposition = %q, want %q", got, "attachment; filename=\"pulse-setup-pbs.sh\"")
}
}
func TestContract_SetupScriptDerivesRenderedServerNameFromCanonicalHost(t *testing.T) {
handlers := newTestConfigHandlers(t, &config.Config{
DataPath: t.TempDir(),
ConfigPath: t.TempDir(),
})
req := httptest.NewRequest(
http.MethodGet,
"/api/setup-script?type=pve&host=https://derived-pve.example.internal:8006&pulse_url=https://pulse.example.com:7655",
nil,
)
rec := httptest.NewRecorder()
handlers.HandleSetupScript(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status = %d, want %d: %s", rec.Code, http.StatusOK, rec.Body.String())
}
script := rec.Body.String()
if !strings.Contains(script, "# Pulse Monitoring Setup Script for derived-pve.example.internal") {
t.Fatalf("setup script missing derived canonical host label: %s", script)
}
if strings.Contains(script, "# Pulse Monitoring Setup Script for your-server") {
t.Fatalf("setup script preserved placeholder server label for canonical host: %s", script)
}
}
func TestContract_AssignProfileRejectsMissingProfile(t *testing.T) {
tempDir := t.TempDir()
mtp := config.NewMultiTenantPersistence(tempDir)
if _, err := mtp.GetPersistence("default"); err != nil {
t.Fatalf("init default persistence: %v", err)
}
handler := NewConfigProfileHandler(mtp)
body := bytes.NewBufferString(`{"agent_id":"agent-1","profile_id":"missing-profile"}`)
req := httptest.NewRequest(http.MethodPost, "/assignments", body)
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
if rec.Code != http.StatusNotFound {
t.Fatalf("status = %d, want %d: %s", rec.Code, http.StatusNotFound, rec.Body.String())
}
if got := strings.TrimSpace(rec.Body.String()); got != "Profile not found" {
t.Fatalf("body = %q, want %q", got, "Profile not found")
}
}
func TestContract_ResolveLoopbackAwarePublicBaseURLPreservesConfiguredHTTPS(t *testing.T) {
cfg := &config.Config{
PublicURL: "https://public.example.com/base/",
FrontendPort: 7655,
}
req := httptest.NewRequest(http.MethodGet, "/", nil)
req.Host = "127.0.0.1:7655"
if got := resolveLoopbackAwarePublicBaseURL(req, cfg); got != "https://public.example.com/base" {
t.Fatalf("baseURL = %q, want %q", got, "https://public.example.com/base")
}
}
func TestContract_CanonicalPulseMonitorTokenNamePrefersPulseURL(t *testing.T) {
got := buildPulseMonitorTokenName("https://public.example.com/base", "127.0.0.1:7655")
if got != "pulse-public-example-com" {
t.Fatalf("tokenName = %q, want %q", got, "pulse-public-example-com")
}
}
func TestContract_FilterRecoveryPointsForRollupsIncludesNormalizedFilters(t *testing.T) {
verified := true
unverified := false
points := []recovery.RecoveryPoint{
{
ID: "point-1",
Provider: recovery.ProviderKubernetes,
Kind: recovery.KindBackup,
Mode: recovery.ModeRemote,
Outcome: recovery.OutcomeSuccess,
SubjectResourceID: "pod-1",
Verified: &verified,
Display: &recovery.RecoveryPointDisplay{
SubjectLabel: "pod-1",
SubjectType: "pod",
ItemType: "pod",
ClusterLabel: "prod-cluster",
NodeHostLabel: "worker-1",
NamespaceLabel: "default",
RepositoryLabel: "repo-a",
IsWorkload: true,
},
},
{
ID: "point-2",
Provider: recovery.ProviderKubernetes,
Kind: recovery.KindBackup,
Mode: recovery.ModeRemote,
Outcome: recovery.OutcomeFailed,
SubjectResourceID: "pod-2",
Verified: &unverified,
Display: &recovery.RecoveryPointDisplay{
SubjectLabel: "pod-2",
SubjectType: "pod",
ItemType: "pod",
ClusterLabel: "other-cluster",
NodeHostLabel: "worker-2",
NamespaceLabel: "kube-system",
IsWorkload: true,
},
},
}
filtered := filterRecoveryPointsForRollups(points, recovery.ListPointsOptions{
Query: "repo-a",
ItemType: "pod",
ClusterLabel: "prod-cluster",
NodeHostLabel: "worker-1",
NamespaceLabel: "default",
Verification: "verified",
WorkloadOnly: true,
})
if len(filtered) != 1 {
t.Fatalf("len(filtered) = %d, want 1", len(filtered))
}
if got := filtered[0].SubjectResourceID; got != "pod-1" {
t.Fatalf("subjectResourceID = %q, want %q", got, "pod-1")
}
}
func TestContract_ParseRecoveryPlatformQueryPrefersCanonicalPlatformAlias(t *testing.T) {
t.Parallel()
if got := parseRecoveryPlatformQuery(url.Values{
"platform": []string{" truenas "},
"provider": []string{"proxmox-pve"},
}); got != recovery.Provider("truenas") {
t.Fatalf("parseRecoveryPlatformQuery(platform first) = %q, want %q", got, "truenas")
}
if got := parseRecoveryPlatformQuery(url.Values{
"provider": []string{" proxmox-pbs "},
}); got != recovery.Provider("proxmox-pbs") {
t.Fatalf("parseRecoveryPlatformQuery(provider fallback) = %q, want %q", got, "proxmox-pbs")
}
}
func TestContract_RecoveryPointPayloadUsesCanonicalPlatformField(t *testing.T) {
payload := buildRecoveryPointPayload(recovery.RecoveryPoint{
ID: "point-1",
Provider: recovery.Provider("truenas"),
Kind: recovery.Kind("snapshot"),
Mode: recovery.Mode("snapshot"),
Outcome: recovery.Outcome("success"),
})
got, err := json.Marshal(payload)
if err != nil {
t.Fatalf("marshal recovery point payload: %v", err)
}
const want = `{
"id":"point-1",
"platform":"truenas",
"provider":"truenas",
"kind":"snapshot",
"mode":"snapshot",
"outcome":"success"
}`
assertJSONSnapshot(t, got, want)
}
func TestContract_RecoveryPointsMockPathReturnsCanonicalProviderBackedFixtures(t *testing.T) {
previousEnabled := mock.IsMockEnabled()
previousConfig := mock.GetConfig()
t.Cleanup(func() {
mock.SetEnabled(false)
mock.SetMockConfig(previousConfig)
if previousEnabled {
mock.SetEnabled(true)
mock.SetMockConfig(previousConfig)
}
})
t.Setenv("PULSE_MOCK_NODES", "1")
t.Setenv("PULSE_MOCK_VMS_PER_NODE", "0")
t.Setenv("PULSE_MOCK_LXCS_PER_NODE", "0")
t.Setenv("PULSE_MOCK_DOCKER_HOSTS", "0")
t.Setenv("PULSE_MOCK_DOCKER_CONTAINERS", "0")
t.Setenv("PULSE_MOCK_GENERIC_HOSTS", "0")
t.Setenv("PULSE_MOCK_K8S_CLUSTERS", "0")
t.Setenv("PULSE_MOCK_K8S_NODES", "0")
t.Setenv("PULSE_MOCK_K8S_PODS", "0")
t.Setenv("PULSE_MOCK_K8S_DEPLOYMENTS", "0")
mock.SetEnabled(false)
mock.SetEnabled(true)
req := httptest.NewRequest(http.MethodGet, "/api/recovery/points?platform=truenas&limit=10", nil)
rec := httptest.NewRecorder()
NewRecoveryHandlers(nil).HandleListPoints(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("expected 200, got %d: %s", rec.Code, rec.Body.String())
}
var resp recoveryPointsResponse
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
t.Fatalf("unmarshal recovery points response: %v", err)
}
if len(resp.Data) == 0 {
t.Fatal("expected provider-backed mock recovery points in response")
}
if resp.Data[0].Platform != recovery.Provider("truenas") {
t.Fatalf("platform = %q, want %q", resp.Data[0].Platform, "truenas")
}
if resp.Data[0].Display == nil {
t.Fatal("expected normalized recovery display payload on mock points response")
}
}
func TestContract_RecoveryRollupPayloadUsesCanonicalPlatformsField(t *testing.T) {
payload := buildRecoveryRollupPayload(recovery.ProtectionRollup{
RollupID: "rollup-1",
LastOutcome: recovery.Outcome("success"),
Providers: []recovery.Provider{
recovery.Provider("proxmox-pbs"),
recovery.Provider("truenas"),
},
})
got, err := json.Marshal(payload)
if err != nil {
t.Fatalf("marshal recovery rollup payload: %v", err)
}
const want = `{
"rollupId":"rollup-1",
"lastOutcome":"success",
"platforms":["proxmox-pbs","truenas"],
"providers":["proxmox-pbs","truenas"]
}`
assertJSONSnapshot(t, got, want)
}
func TestContract_BillingStateJSONSnapshot(t *testing.T) {
payload := entitlements.BillingState{
Capabilities: []string{"relay", "mobile_app"},
Limits: map[string]int64{"max_monitored_systems": 10},
MetersEnabled: []string{"api_requests"},
PlanVersion: "cloud_starter",
SubscriptionState: entitlements.SubStateActive,
StripeCustomerID: "cus_123",
StripeSubscriptionID: "sub_123",
StripePriceID: "price_123",
}
got, err := json.Marshal(payload)
if err != nil {
t.Fatalf("marshal billing state: %v", err)
}
const want = `{
"capabilities":["relay","mobile_app"],
"limits":{"max_monitored_systems":10},
"meters_enabled":["api_requests"],
"plan_version":"cloud_starter",
"subscription_state":"active",
"stripe_customer_id":"cus_123",
"stripe_subscription_id":"sub_123",
"stripe_price_id":"price_123"
}`
assertJSONSnapshot(t, got, want)
}
func TestContract_HostedTenantEntitlementsFallbackToDefaultBillingState(t *testing.T) {
baseDir := t.TempDir()
mtp := config.NewMultiTenantPersistence(baseDir)
if _, err := mtp.GetPersistence("default"); err != nil {
t.Fatalf("init default persistence: %v", err)
}
if _, err := mtp.GetPersistence("t-tenant"); err != nil {
t.Fatalf("init tenant persistence: %v", err)
}
store := config.NewFileBillingStore(baseDir)
if err := store.SaveBillingState("default", &entitlements.BillingState{
Capabilities: []string{pkglicensing.FeatureRelay, pkglicensing.FeatureRBAC},
Limits: map[string]int64{"max_monitored_systems": 50},
PlanVersion: "msp_starter",
SubscriptionState: entitlements.SubStateActive,
}); err != nil {
t.Fatalf("save default billing state: %v", err)
}
handlers := NewLicenseHandlers(mtp, true)
ctx := context.WithValue(context.Background(), OrgIDContextKey, "t-tenant")
req := httptest.NewRequest(http.MethodGet, "/api/license/entitlements", nil).WithContext(ctx)
rec := httptest.NewRecorder()
handlers.HandleEntitlements(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("entitlements status=%d, want %d: %s", rec.Code, http.StatusOK, rec.Body.String())
}
var payload EntitlementPayload
if err := json.NewDecoder(rec.Body).Decode(&payload); err != nil {
t.Fatalf("decode entitlements payload: %v", err)
}
if payload.SubscriptionState != string(pkglicensing.SubStateActive) {
t.Fatalf("subscription_state=%q, want %q", payload.SubscriptionState, pkglicensing.SubStateActive)
}
if !sliceContainsString(payload.Capabilities, pkglicensing.FeatureRelay) {
t.Fatalf("expected hosted tenant payload to include %q from default hosted billing state", pkglicensing.FeatureRelay)
}
for _, limit := range payload.Limits {
if limit.Key == pkglicensing.MaxMonitoredSystemsLicenseGateKey {
t.Fatalf("expected retired max_monitored_systems limit to be omitted, got %+v", payload.Limits)
}
}
}
func TestContract_HostedTenantEntitlementRefreshFallsBackToDefaultBillingState(t *testing.T) {
baseDir := t.TempDir()
mtp := config.NewMultiTenantPersistence(baseDir)
if _, err := mtp.GetPersistence("default"); err != nil {
t.Fatalf("init default persistence: %v", err)
}
if _, err := mtp.GetPersistence("t-tenant"); err != nil {
t.Fatalf("init tenant persistence: %v", err)
}
pub, priv, err := ed25519.GenerateKey(nil)
if err != nil {
t.Fatalf("GenerateKey: %v", err)
}
t.Setenv(pkglicensing.TrialActivationPublicKeyEnvVar, base64.StdEncoding.EncodeToString(pub))
refreshServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/api/entitlements/refresh" {
http.NotFound(w, r)
return
}
var req hostedTrialLeaseRefreshRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
t.Fatalf("decode refresh request: %v", err)
}
if req.OrgID != "default" {
t.Fatalf("req.OrgID=%q, want %q", req.OrgID, "default")
}
if req.InstanceHost != "pulse.example.com" {
t.Fatalf("req.InstanceHost=%q, want %q", req.InstanceHost, "pulse.example.com")
}
if req.EntitlementRefreshToken != "etr_hosted_default" {
t.Fatalf("req.EntitlementRefreshToken=%q, want %q", req.EntitlementRefreshToken, "etr_hosted_default")
}
entitlementJWT, err := pkglicensing.SignEntitlementLeaseToken(priv, pkglicensing.EntitlementLeaseClaims{
OrgID: "default",
InstanceHost: "pulse.example.com",
PlanVersion: "msp_starter",
SubscriptionState: pkglicensing.SubStateActive,
Capabilities: []string{
pkglicensing.FeatureRelay,
pkglicensing.FeatureAIAutoFix,
},
})
if err != nil {
t.Fatalf("SignEntitlementLeaseToken: %v", err)
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(hostedTrialLeaseRefreshResponse{
EntitlementJWT: entitlementJWT,
})
}))
defer refreshServer.Close()
store := config.NewFileBillingStore(baseDir)
expiredLease, err := pkglicensing.SignEntitlementLeaseToken(priv, pkglicensing.EntitlementLeaseClaims{
OrgID: "default",
InstanceHost: "pulse.example.com",
PlanVersion: "msp_starter",
SubscriptionState: pkglicensing.SubStateActive,
Capabilities: []string{pkglicensing.FeatureRelay},
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(-time.Hour)),
},
})
if err != nil {
t.Fatalf("SignEntitlementLeaseToken(expired): %v", err)
}
if err := store.SaveBillingState("default", &entitlements.BillingState{
EntitlementJWT: expiredLease,
EntitlementRefreshToken: "etr_hosted_default",
}); err != nil {
t.Fatalf("save default billing state: %v", err)
}
handlers := NewLicenseHandlers(mtp, true, &config.Config{
PublicURL: "https://pulse.example.com",
ProTrialSignupURL: refreshServer.URL + "/start-pro-trial",
})
refreshed, permanent, err := handlers.refreshHostedEntitlementLeaseOnce("t-tenant", nil)
if err != nil {
t.Fatalf("refreshHostedEntitlementLeaseOnce: %v", err)
}
if !refreshed || permanent {
t.Fatalf("refreshed=%v permanent=%v, want refreshed=true permanent=false", refreshed, permanent)
}
state, err := store.GetBillingState("default")
if err != nil {
t.Fatalf("GetBillingState(default): %v", err)
}
if state == nil {
t.Fatal("expected default billing state after hosted tenant refresh")
}
if state.SubscriptionState != entitlements.SubStateActive {
t.Fatalf("subscription_state=%q, want %q", state.SubscriptionState, entitlements.SubStateActive)
}
if state.PlanVersion != "msp_starter" {
t.Fatalf("plan_version=%q, want %q", state.PlanVersion, "msp_starter")
}
if !sliceContainsString(state.Capabilities, pkglicensing.FeatureAIAutoFix) {
t.Fatalf("expected default hosted billing state to include %q after tenant refresh, got %v", pkglicensing.FeatureAIAutoFix, state.Capabilities)
}
}
func TestContract_HostedEntitlementVerifierBridgeUsesCompatibilityEnvAlias(t *testing.T) {
if pkglicensing.HostedEntitlementPublicKeyEnvVar != pkglicensing.TrialActivationPublicKeyEnvVar {
t.Fatalf("hosted entitlement verifier env=%q, want legacy env alias %q", pkglicensing.HostedEntitlementPublicKeyEnvVar, pkglicensing.TrialActivationPublicKeyEnvVar)
}
pub, _, err := ed25519.GenerateKey(nil)
if err != nil {
t.Fatalf("GenerateKey: %v", err)
}
t.Setenv("PULSE_HOSTED_MODE", "true")
t.Setenv(pkglicensing.HostedEntitlementPublicKeyEnvVar, base64.StdEncoding.EncodeToString(pub))
got, err := trialActivationPublicKeyFromLicensing()
if err != nil {
t.Fatalf("trialActivationPublicKeyFromLicensing: %v", err)
}
if !bytes.Equal(got, pub) {
t.Fatal("hosted entitlement verifier bridge did not resolve the compatibility env key")
}
}
func TestContract_EntitlementPayloadMonitoredSystemUsageJSONSnapshot(t *testing.T) {
payload := buildEntitlementPayloadWithUsage(&licenseStatus{
Valid: true,
Tier: pkglicensing.TierPro,
Features: append([]string(nil), pkglicensing.TierFeatures[pkglicensing.TierPro]...),
}, string(pkglicensing.SubStateActive), entitlementUsageSnapshot{
MonitoredSystems: 7,
MonitoredSystemsAvailable: true,
LegacyConnections: legacyConnectionCountsModel{
ProxmoxNodes: 2,
DockerHosts: 1,
KubernetesClusters: 1,
},
}, nil)
got, err := json.Marshal(payload)
if err != nil {
t.Fatalf("marshal entitlement payload: %v", err)
}
const want = `{
"capabilities":["update_alerts","sso","advanced_sso","ai_patrol","relay","mobile_app","push_notifications","long_term_metrics","ai_alerts","ai_autofix","kubernetes_ai","agent_profiles","rbac","audit_logging","advanced_reporting"],
"limits":[],
"subscription_state":"active",
"upgrade_reasons":[],
"tier":"pro",
"hosted_mode":false,
"valid":true,
"is_lifetime":false,
"days_remaining":0,
"trial_eligible":false,
"max_history_days":90,
"legacy_connections":{"proxmox_nodes":2,"docker_hosts":1,"kubernetes_clusters":1},
"has_migration_gap":false
}`
assertJSONSnapshot(t, got, want)
}
func TestContract_SelfHostedCommunityEntitlementsJSONSnapshot(t *testing.T) {
baseDir := t.TempDir()
mtp := config.NewMultiTenantPersistence(baseDir)
handlers := NewLicenseHandlers(mtp, false)
req := httptest.NewRequest(http.MethodGet, "/api/license/entitlements", nil).
WithContext(context.WithValue(context.Background(), OrgIDContextKey, "default"))
rec := httptest.NewRecorder()
handlers.HandleEntitlements(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("entitlements status=%d, want %d: %s", rec.Code, http.StatusOK, rec.Body.String())
}
var payload EntitlementPayload
if err := json.Unmarshal(rec.Body.Bytes(), &payload); err != nil {
t.Fatalf("decode entitlements: %v", err)
}
got, err := json.Marshal(payload)
if err != nil {
t.Fatalf("marshal entitlements payload: %v", err)
}
const want = `{
"capabilities":["update_alerts","sso","advanced_sso","ai_patrol"],
"limits":[],
"subscription_state":"active",
"upgrade_reasons":[
{"key":"mobile_app","reason":"Get Relay so Pulse Mobile can pair with this instance for secure handoff and notifications.","action_url":"https://pulserelay.pro/pricing?utm_source=pulse\u0026utm_medium=app\u0026utm_campaign=upgrade\u0026feature=mobile_app"},
{"key":"push_notifications","reason":"Get Relay so important alerts reach you immediately on mobile instead of waiting for you to reopen Pulse.","action_url":"https://pulserelay.pro/pricing?utm_source=pulse\u0026utm_medium=app\u0026utm_campaign=upgrade\u0026feature=push_notifications"},
{"key":"relay","reason":"Get Relay so Pulse stays reachable securely from anywhere instead of only on the local dashboard.","action_url":"https://pulserelay.pro/pricing?utm_source=pulse\u0026utm_medium=app\u0026utm_campaign=upgrade\u0026feature=relay"},
{"key":"long_term_metrics","reason":"Get Relay for 14 days of history, or Pro for 90 days, so you can see what changed before and after an incident.","action_url":"https://pulserelay.pro/pricing?utm_source=pulse\u0026utm_medium=app\u0026utm_campaign=upgrade\u0026feature=long_term_metrics"},
{"key":"ai_autofix","reason":"Upgrade to Pro so Pulse can move from finding issues to governed remediation with your approval or autonomy policy.","action_url":"https://pulserelay.pro/pricing?utm_source=pulse\u0026utm_medium=app\u0026utm_campaign=upgrade\u0026feature=ai_autofix"},
{"key":"ai_alerts","reason":"Upgrade to Pro so alerts arrive with root-cause analysis instead of a stack of symptoms.","action_url":"https://pulserelay.pro/pricing?utm_source=pulse\u0026utm_medium=app\u0026utm_campaign=upgrade\u0026feature=ai_alerts"},
{"key":"rbac","reason":"Upgrade to Pro when more than one operator needs safe access boundaries around infrastructure changes.","action_url":"https://pulserelay.pro/pricing?utm_source=pulse\u0026utm_medium=app\u0026utm_campaign=upgrade\u0026feature=rbac"},
{"key":"agent_profiles","reason":"Upgrade to Pro to standardize agent behavior across systems without reconfiguring every install by hand.","action_url":"https://pulserelay.pro/pricing?utm_source=pulse\u0026utm_medium=app\u0026utm_campaign=upgrade\u0026feature=agent_profiles"},
{"key":"audit_logging","reason":"Upgrade to Pro to keep a trustworthy action trail for incident review, accountability, and compliance.","action_url":"https://pulserelay.pro/pricing?utm_source=pulse\u0026utm_medium=app\u0026utm_campaign=upgrade\u0026feature=audit_logging"},
{"key":"advanced_reporting","reason":"Upgrade to Pro to turn live infrastructure state into shareable reports without manual screenshot work.","action_url":"https://pulserelay.pro/pricing?utm_source=pulse\u0026utm_medium=app\u0026utm_campaign=upgrade\u0026feature=advanced_reporting"}
],
"tier":"free",
"hosted_mode":false,
"valid":false,
"is_lifetime":false,
"days_remaining":0,
"trial_eligible":false,
"max_history_days":7,
"overflow_days_remaining":14,
"legacy_connections":{"proxmox_nodes":0,"docker_hosts":0,"kubernetes_clusters":0},
"has_migration_gap":false,
"runtime":{"build":"community","label":"Pulse Community runtime"}
}`
assertJSONSnapshot(t, got, want)
}
func TestContract_SelfHostedCommunityRuntimeCapabilitiesJSONSnapshot(t *testing.T) {
baseDir := t.TempDir()
mtp := config.NewMultiTenantPersistence(baseDir)
handlers := NewLicenseHandlers(mtp, false)
req := httptest.NewRequest(http.MethodGet, "/api/license/runtime-capabilities", nil).
WithContext(context.WithValue(context.Background(), OrgIDContextKey, "default"))
rec := httptest.NewRecorder()
handlers.HandleRuntimeCapabilities(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf(
"runtime capabilities status=%d, want %d: %s",
rec.Code,
http.StatusOK,
rec.Body.String(),
)
}
var payload RuntimeCapabilitiesPayload
if err := json.Unmarshal(rec.Body.Bytes(), &payload); err != nil {
t.Fatalf("decode runtime capabilities: %v", err)
}
got, err := json.Marshal(payload)
if err != nil {
t.Fatalf("marshal runtime capabilities payload: %v", err)
}
const want = `{
"capabilities":["update_alerts","sso","advanced_sso","ai_patrol"],
"limits":[],
"hosted_mode":false,
"max_history_days":7,
"runtime":{"build":"community","label":"Pulse Community runtime"}
}`
assertJSONSnapshot(t, got, want)
}
func TestContract_LicenseHandlersPassRuntimeIdentityToActivationService(t *testing.T) {
source, err := os.ReadFile("licensing_handlers.go")
if err != nil {
t.Fatalf("read licensing_handlers.go: %v", err)
}
text := string(source)
if count := strings.Count(text, "SetRuntimeIdentity(h.currentRuntimeIdentity())"); count < 2 {
t.Fatalf("expected cached and new tenant services to receive runtime identity, count=%d", count)
}
}
func TestContract_EntitlementPayloadMonitoredSystemUsageUnavailableJSONSnapshot(t *testing.T) {
payload := buildEntitlementPayloadWithUsage(&licenseStatus{
Valid: true,
Tier: pkglicensing.TierCloud,
PlanVersion: "cloud_starter",
Features: append([]string(nil), pkglicensing.TierFeatures[pkglicensing.TierPro]...),
}, string(pkglicensing.SubStateActive), entitlementUsageSnapshot{
MonitoredSystemsUnavailableReason: "supplemental_inventory_unsettled",
}, nil)
got, err := json.Marshal(payload)
if err != nil {
t.Fatalf("marshal entitlement payload: %v", err)
}
const want = `{
"capabilities":["update_alerts","sso","advanced_sso","ai_patrol","relay","mobile_app","push_notifications","long_term_metrics","ai_alerts","ai_autofix","kubernetes_ai","agent_profiles","rbac","audit_logging","advanced_reporting"],
"limits":[],
"subscription_state":"active",
"upgrade_reasons":[],
"plan_version":"cloud_starter",
"tier":"cloud",
"hosted_mode":false,
"valid":true,
"is_lifetime":false,
"days_remaining":0,
"trial_eligible":false,
"max_history_days":90,
"legacy_connections":{"proxmox_nodes":0,"docker_hosts":0,"kubernetes_clusters":0},
"has_migration_gap":false
}`
assertJSONSnapshot(t, got, want)
}
func TestContract_EntitlementPayloadLifetimeJSONSnapshot(t *testing.T) {
payload := buildEntitlementPayloadWithUsage(&licenseStatus{
Valid: true,
Tier: pkglicensing.TierLifetime,
PlanVersion: "v5_lifetime_grandfathered",
IsLifetime: true,
Features: append([]string(nil), pkglicensing.TierFeatures[pkglicensing.TierLifetime]...),
MaxGuests: 0,
}, string(pkglicensing.SubStateActive), entitlementUsageSnapshot{
MonitoredSystems: 15,
MonitoredSystemsAvailable: true,
LegacyConnections: legacyConnectionCountsModel{
ProxmoxNodes: 1,
DockerHosts: 1,
},
}, nil)
got, err := json.Marshal(payload)
if err != nil {
t.Fatalf("marshal entitlement payload: %v", err)
}
const want = `{
"capabilities":["update_alerts","sso","advanced_sso","ai_patrol","relay","mobile_app","push_notifications","long_term_metrics","ai_alerts","ai_autofix","kubernetes_ai","agent_profiles","rbac","audit_logging","advanced_reporting"],
"limits":[],
"subscription_state":"active",
"upgrade_reasons":[],
"plan_version":"v5_lifetime_grandfathered",
"tier":"lifetime",
"hosted_mode":false,
"valid":true,
"is_lifetime":true,
"days_remaining":0,
"trial_eligible":false,
"max_history_days":90,
"legacy_connections":{"proxmox_nodes":1,"docker_hosts":1,"kubernetes_clusters":0},
"has_migration_gap":false
}`
assertJSONSnapshot(t, got, want)
}
func TestContract_EntitlementUsageSnapshotWaitsForSettledSupplementalInventory(t *testing.T) {
provider := &contractSupplementalUsageProvider{}
monitor := &monitoring.Monitor{}
monitor.SetResourceStore(unifiedresources.NewMonitorAdapter(nil))
monitor.SetSupplementalRecordsProvider(unifiedresources.SourceTrueNAS, provider)
handlers := &LicenseHandlers{monitor: monitor}
usage := handlers.entitlementUsageSnapshot(context.Background())
if usage.MonitoredSystemsAvailable {
t.Fatalf("expected unsettled supplemental inventory to keep usage unavailable, got %+v", usage)
}
provider.settle(1)
usage = handlers.entitlementUsageSnapshot(context.Background())
if usage.MonitoredSystemsAvailable {
t.Fatalf("expected stale store freshness to keep usage unavailable, got %+v", usage)
}
monitor.SetSupplementalRecordsProvider(unifiedresources.SourceTrueNAS, provider)
usage = handlers.entitlementUsageSnapshot(context.Background())
if !usage.MonitoredSystemsAvailable {
t.Fatalf("expected usage to become available after canonical store rebuild, got %+v", usage)
}
if usage.MonitoredSystems != 1 {
t.Fatalf("MonitoredSystems=%d, want 1", usage.MonitoredSystems)
}
}
func TestContract_LegacyMigrationFallbackStaysUncappedJSONSnapshot(t *testing.T) {
t.Setenv("PULSE_LICENSE_DEV_MODE", "false")
const expectedClientVersion = "6.0.0-rc.1"
grantJWT, grantPublicKey, err := licensetestsupport.GenerateGrantJWTForTesting(pkglicensing.GrantClaims{
LicenseID: "lic_contract_floor",
Tier: "pro",
PlanKey: "legacy_migration_fallback",
State: "active",
Features: []string{"relay"},
IssuedAt: time.Now().Unix(),
ExpiresAt: time.Now().Add(72 * time.Hour).Unix(),
Email: "contract-floor@example.com",
})
if err != nil {
t.Fatalf("generate grant jwt: %v", err)
}
pkglicensing.SetPublicKey(grantPublicKey)
t.Cleanup(func() { pkglicensing.SetPublicKey(nil) })
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/v1/licenses/exchange" {
t.Fatalf("path = %q, want /v1/licenses/exchange", r.URL.Path)
}
var exchangeReq pkglicensing.ExchangeLegacyLicenseRequest
if err := json.NewDecoder(r.Body).Decode(&exchangeReq); err != nil {
t.Fatalf("decode exchange request: %v", err)
}
if exchangeReq.ClientVersion != expectedClientVersion {
t.Fatalf("exchange client version=%q, want %q", exchangeReq.ClientVersion, expectedClientVersion)
}
w.WriteHeader(http.StatusCreated)
_ = json.NewEncoder(w).Encode(pkglicensing.ActivateInstallationResponse{
License: pkglicensing.ActivateResponseLicense{
LicenseID: "lic_contract_floor",
State: "active",
Tier: "pro",
Features: []string{"relay"},
},
Installation: pkglicensing.ActivateResponseInstallation{
InstallationID: "inst_contract_floor",
InstallationToken: "pit_live_contract_floor",
Status: "active",
},
Grant: pkglicensing.GrantEnvelope{
JWT: grantJWT,
JTI: "grant_contract_floor",
ExpiresAt: time.Now().Add(72 * time.Hour).UTC().Format(time.RFC3339),
},
})
}))
defer server.Close()
t.Setenv("PULSE_LICENSE_SERVER_URL", server.URL)
baseDir := t.TempDir()
mtp := config.NewMultiTenantPersistence(baseDir)
cp, err := mtp.GetPersistence("default")
if err != nil {
t.Fatalf("init default persistence: %v", err)
}
persistence, err := pkglicensing.NewPersistence(cp.GetConfigDir())
if err != nil {
t.Fatalf("new persistence: %v", err)
}
legacyJWT, err := licensetestsupport.GenerateLicenseForTesting("contract-floor@example.com", pkglicensing.TierPro, 24*time.Hour)
if err != nil {
t.Fatalf("generate test license: %v", err)
}
if err := persistence.Save(legacyJWT); err != nil {
t.Fatalf("save legacy jwt: %v", err)
}
handlers := NewLicenseHandlers(mtp, false)
handlers.SetRuntimeVersion(expectedClientVersion)
handlers.SetMonitors(buildGrandfatherFloorMonitor(23), nil)
t.Cleanup(handlers.StopAllBackgroundLoops)
ctx := context.WithValue(context.Background(), OrgIDContextKey, "default")
statusReq := httptest.NewRequest(http.MethodGet, "/api/license/status", nil).WithContext(ctx)
statusRec := httptest.NewRecorder()
handlers.HandleLicenseStatus(statusRec, statusReq)
if statusRec.Code != http.StatusOK {
t.Fatalf("status=%d, want %d: %s", statusRec.Code, http.StatusOK, statusRec.Body.String())
}
entReq := httptest.NewRequest(http.MethodGet, "/api/license/entitlements", nil).WithContext(ctx)
entRec := httptest.NewRecorder()
handlers.HandleEntitlements(entRec, entReq)
if entRec.Code != http.StatusOK {
t.Fatalf("entitlements status=%d, want %d: %s", entRec.Code, http.StatusOK, entRec.Body.String())
}
var status pkglicensing.LicenseStatus
if err := json.Unmarshal(statusRec.Body.Bytes(), &status); err != nil {
t.Fatalf("decode status: %v", err)
}
var payload EntitlementPayload
if err := json.Unmarshal(entRec.Body.Bytes(), &payload); err != nil {
t.Fatalf("decode entitlements: %v", err)
}
got, err := json.Marshal(struct {
Status struct {
Tier pkglicensing.Tier `json:"tier"`
PlanVersion string `json:"plan_version"`
Valid bool `json:"valid"`
} `json:"status"`
Entitlements struct {
Tier string `json:"tier"`
PlanVersion string `json:"plan_version"`
SubscriptionState string `json:"subscription_state"`
Limits []pkglicensing.LimitStatus `json:"limits"`
} `json:"entitlements"`
}{
Status: struct {
Tier pkglicensing.Tier `json:"tier"`
PlanVersion string `json:"plan_version"`
Valid bool `json:"valid"`
}{
Tier: status.Tier,
PlanVersion: status.PlanVersion,
Valid: status.Valid,
},
Entitlements: struct {
Tier string `json:"tier"`
PlanVersion string `json:"plan_version"`
SubscriptionState string `json:"subscription_state"`
Limits []pkglicensing.LimitStatus `json:"limits"`
}{
Tier: payload.Tier,
PlanVersion: payload.PlanVersion,
SubscriptionState: payload.SubscriptionState,
Limits: payload.Limits,
},
})
if err != nil {
t.Fatalf("marshal snapshot payload: %v", err)
}
const want = `{
"status":{
"tier":"pro",
"plan_version":"legacy_migration_fallback",
"valid":true
},
"entitlements":{
"tier":"pro",
"plan_version":"legacy_migration_fallback",
"subscription_state":"active",
"limits":[]
}
}`
assertJSONSnapshot(t, got, want)
}
func TestContract_HostedBillingStateFallbackJSONSnapshot(t *testing.T) {
baseDir := t.TempDir()
store := config.NewFileBillingStore(baseDir)
if err := store.SaveBillingState("default", &entitlements.BillingState{
Capabilities: []string{pkglicensing.FeatureRelay, pkglicensing.FeatureRBAC},
Limits: map[string]int64{"max_monitored_systems": 50},
MetersEnabled: []string{},
PlanVersion: "msp_starter",
SubscriptionState: entitlements.SubStateActive,
StripeCustomerID: "cus_hosted",
StripeSubscriptionID: "sub_hosted",
StripePriceID: "price_hosted",
}); err != nil {
t.Fatalf("save default billing state: %v", err)
}
handlers := NewBillingStateHandlers(store, true)
req := httptest.NewRequest(http.MethodGet, "/api/admin/orgs/t-tenant/billing-state", nil)
req.SetPathValue("id", "t-tenant")
rec := httptest.NewRecorder()
handlers.HandleGetBillingState(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status=%d, want %d: %s", rec.Code, http.StatusOK, rec.Body.String())
}
const want = `{
"capabilities":["relay","rbac"],
"limits":{},
"meters_enabled":[],
"plan_version":"msp_starter",
"subscription_state":"active",
"stripe_customer_id":"cus_hosted",
"stripe_subscription_id":"sub_hosted",
"stripe_price_id":"price_hosted"
}`
assertJSONSnapshot(t, rec.Body.Bytes(), want)
}
func TestContract_MonitoredSystemUsageUnavailableIncludesReason(t *testing.T) {
body := bytes.NewBufferString(`{
"candidate":{
"source":"proxmox",
"name":"tower",
"hostname":"tower.local",
"host_url":"https://tower.local:8006"
}
}`)
req := httptest.NewRequest(http.MethodPost, "/api/license/monitored-system-ledger/preview", body)
rec := httptest.NewRecorder()
router := &Router{}
router.handleMonitoredSystemLedgerPreview(rec, req)
if rec.Code != http.StatusServiceUnavailable {
t.Fatalf("status=%d, want %d: %s", rec.Code, http.StatusServiceUnavailable, rec.Body.String())
}
var payload APIError
if err := json.NewDecoder(rec.Body).Decode(&payload); err != nil {
t.Fatalf("decode unavailable payload: %v", err)
}
payload = payload.NormalizeCollections()
if payload.Code != "monitored_system_usage_unavailable" {
t.Fatalf("code=%q, want monitored_system_usage_unavailable", payload.Code)
}
if got := payload.Details["reason"]; got != monitoring.MonitoredSystemUsageUnavailableMonitorState {
t.Fatalf("details.reason=%q, want %q", got, monitoring.MonitoredSystemUsageUnavailableMonitorState)
}
}
func TestContract_HostReportAdmissionPreservesRestartContinuityAtLimit(t *testing.T) {
setMaxMonitoredSystemsLicenseForTests(t, 1)
cfg := &config.Config{DataPath: t.TempDir()}
report := agentshost.Report{
Agent: agentshost.AgentInfo{
ID: "agent-1",
Version: "6.0.0-rc.1",
},
Host: agentshost.HostInfo{
ID: "machine-1",
MachineID: "machine-1",
Hostname: "host-1.local",
Platform: "linux",
},
Timestamp: time.Now().UTC(),
}
postReport := func(t *testing.T, handler *UnifiedAgentHandlers, input agentshost.Report) *httptest.ResponseRecorder {
t.Helper()
body, err := json.Marshal(input)
if err != nil {
t.Fatalf("marshal host report: %v", err)
}
req := httptest.NewRequest(http.MethodPost, "/api/agents/agent/report", bytes.NewReader(body))
rec := httptest.NewRecorder()
handler.HandleReport(rec, req)
return rec
}
handler, _ := newUnifiedAgentHandlers(t, cfg)
initialRec := postReport(t, handler, report)
if initialRec.Code != http.StatusOK {
t.Fatalf("initial host report should pass, got %d: %s", initialRec.Code, initialRec.Body.String())
}
restartedHandler, _ := newUnifiedAgentHandlers(t, cfg)
restartReport := report
restartReport.Timestamp = report.Timestamp.Add(30 * time.Second)
restartRec := postReport(t, restartedHandler, restartReport)
if restartRec.Code != http.StatusOK {
t.Fatalf("restart continuity report should stay admitted, got %d: %s", restartRec.Code, restartRec.Body.String())
}
newHostReport := agentshost.Report{
Agent: agentshost.AgentInfo{
ID: "agent-2",
Version: "6.0.0-rc.1",
},
Host: agentshost.HostInfo{
ID: "machine-2",
MachineID: "machine-2",
Hostname: "host-2.local",
Platform: "linux",
},
Timestamp: report.Timestamp.Add(time.Minute),
}
newHostRec := postReport(t, restartedHandler, newHostReport)
if newHostRec.Code != http.StatusOK {
t.Fatalf("new host should be admitted because monitored-system caps are retired, got %d: %s", newHostRec.Code, newHostRec.Body.String())
}
}
func TestContract_PlatformConnectionWritesIgnoreUsageUnavailableWithCapsRetired(t *testing.T) {
t.Run("truenas add", func(t *testing.T) {
setTrueNASFeatureForTest(t, true)
setMockModeForTrueNASTest(t, false)
setMaxMonitoredSystemsLicenseForTests(t, 1)
handler, _, monitor := newTrueNASHandlersForTest(t, nil)
bindUnavailableSupplementalUsageProviderForTest(
t,
monitor,
unifiedresources.SourceTrueNAS,
monitoring.MonitoredSystemUsageUnavailableSupplementalInventoryUnsettled,
)
body := marshalTrueNASRequest(t, map[string]any{
"name": "tower",
"host": "tower.local",
"apiKey": "super-secret",
})
req := httptest.NewRequest(http.MethodPost, "/api/truenas/connections", bytes.NewReader(body))
rec := httptest.NewRecorder()
handler.HandleAdd(rec, req)
if rec.Code != http.StatusCreated {
t.Fatalf("expected 201 with monitored-system caps retired, got %d: %s", rec.Code, rec.Body.String())
}
})
t.Run("vmware add", func(t *testing.T) {
setVMwareFeatureForTest(t, true)
setMockModeForVMwareTest(t, false)
setMaxMonitoredSystemsLicenseForTests(t, 1)
handler, _ := newVMwareHandlersForTest(t)
monitor, _, _ := newTestMonitor(t)
handler.getMonitor = func(context.Context) *monitoring.Monitor { return monitor }
bindUnavailableSupplementalUsageProviderForTest(
t,
monitor,
unifiedresources.SourceVMware,
monitoring.MonitoredSystemUsageUnavailableSupplementalInventoryUnsettled,
)
previewRecordsCalled := false
handler.previewRecords = func(context.Context, config.VMwareVCenterInstance) ([]unifiedresources.IngestRecord, error) {
previewRecordsCalled = true
return nil, nil
}
body := marshalVMwareRequest(t, map[string]any{
"name": "lab-vcenter",
"host": "vcsa.lab.local",
"port": 443,
"username": "administrator@vsphere.local",
"password": "super-secret",
"enabled": true,
})
req := httptest.NewRequest(http.MethodPost, "/api/vmware/connections", bytes.NewReader(body))
rec := httptest.NewRecorder()
handler.HandleAdd(rec, req)
if rec.Code != http.StatusCreated {
t.Fatalf("expected 201 with monitored-system caps retired, got %d: %s", rec.Code, rec.Body.String())
}
if previewRecordsCalled {
t.Fatal("expected VMware write not to preview external inventory when monitored-system caps are retired")
}
})
}
func TestContract_DisabledPlatformConnectionWritesBypassUnavailableUsageGate(t *testing.T) {
t.Run("truenas add", func(t *testing.T) {
setTrueNASFeatureForTest(t, true)
setMockModeForTrueNASTest(t, false)
setMaxMonitoredSystemsLicenseForTests(t, 1)
handler, _, monitor := newTrueNASHandlersForTest(t, nil)
bindUnavailableSupplementalUsageProviderForTest(
t,
monitor,
unifiedresources.SourceTrueNAS,
monitoring.MonitoredSystemUsageUnavailableSupplementalInventoryUnsettled,
)
body := marshalTrueNASRequest(t, map[string]any{
"name": "tower",
"host": "tower.local",
"apiKey": "super-secret",
"enabled": false,
})
req := httptest.NewRequest(http.MethodPost, "/api/truenas/connections", bytes.NewReader(body))
rec := httptest.NewRecorder()
handler.HandleAdd(rec, req)
if rec.Code != http.StatusCreated {
t.Fatalf("status=%d, want %d: %s", rec.Code, http.StatusCreated, rec.Body.String())
}
})
t.Run("truenas update", func(t *testing.T) {
setTrueNASFeatureForTest(t, true)
setMockModeForTrueNASTest(t, false)
setMaxMonitoredSystemsLicenseForTests(t, 1)
handler, persistence, monitor := newTrueNASHandlersForTest(t, nil)
bindUnavailableSupplementalUsageProviderForTest(
t,
monitor,
unifiedresources.SourceTrueNAS,
monitoring.MonitoredSystemUsageUnavailableSupplementalInventoryRebuildPending,
)
if err := persistence.SaveTrueNASConfig([]config.TrueNASInstance{{
ID: "alpha",
Name: "archive",
Host: "archive.local",
APIKey: "super-secret",
UseHTTPS: true,
Enabled: true,
}}); err != nil {
t.Fatalf("seed truenas config: %v", err)
}
body := marshalTrueNASRequest(t, map[string]any{
"name": "archive",
"host": "archive.local",
"apiKey": "********",
"useHttps": true,
"enabled": false,
})
req := httptest.NewRequest(http.MethodPut, "/api/truenas/connections/alpha", bytes.NewReader(body))
rec := httptest.NewRecorder()
handler.HandleUpdate(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status=%d, want %d: %s", rec.Code, http.StatusOK, rec.Body.String())
}
})
t.Run("vmware add", func(t *testing.T) {
setVMwareFeatureForTest(t, true)
setMockModeForVMwareTest(t, false)
setMaxMonitoredSystemsLicenseForTests(t, 1)
handler, _ := newVMwareHandlersForTest(t)
monitor, _, _ := newTestMonitor(t)
handler.getMonitor = func(context.Context) *monitoring.Monitor { return monitor }
bindUnavailableSupplementalUsageProviderForTest(
t,
monitor,
unifiedresources.SourceVMware,
monitoring.MonitoredSystemUsageUnavailableSupplementalInventoryUnsettled,
)
previewRecordsCalled := false
handler.previewRecords = func(context.Context, config.VMwareVCenterInstance) ([]unifiedresources.IngestRecord, error) {
previewRecordsCalled = true
return nil, nil
}
body := marshalVMwareRequest(t, map[string]any{
"name": "lab-vcenter",
"host": "vcsa.lab.local",
"port": 443,
"username": "administrator@vsphere.local",
"password": "super-secret",
"enabled": false,
})
req := httptest.NewRequest(http.MethodPost, "/api/vmware/connections", bytes.NewReader(body))
rec := httptest.NewRecorder()
handler.HandleAdd(rec, req)
if rec.Code != http.StatusCreated {
t.Fatalf("status=%d, want %d: %s", rec.Code, http.StatusCreated, rec.Body.String())
}
if previewRecordsCalled {
t.Fatal("expected disabled VMware add to bypass external inventory preview")
}
})
t.Run("vmware update", func(t *testing.T) {
setVMwareFeatureForTest(t, true)
setMockModeForVMwareTest(t, false)
setMaxMonitoredSystemsLicenseForTests(t, 1)
handler, persistence := newVMwareHandlersForTest(t)
monitor, _, _ := newTestMonitor(t)
handler.getMonitor = func(context.Context) *monitoring.Monitor { return monitor }
bindUnavailableSupplementalUsageProviderForTest(
t,
monitor,
unifiedresources.SourceVMware,
monitoring.MonitoredSystemUsageUnavailableSupplementalInventoryRebuildPending,
)
if err := persistence.SaveVMwareConfig([]config.VMwareVCenterInstance{{
ID: "alpha",
Name: "vc-a",
Host: "vc-a.lab.local",
Port: 443,
Username: "administrator@vsphere.local",
Password: "super-secret",
InsecureSkipVerify: true,
Enabled: true,
}}); err != nil {
t.Fatalf("seed vmware config: %v", err)
}
previewRecordsCalled := false
handler.previewRecords = func(context.Context, config.VMwareVCenterInstance) ([]unifiedresources.IngestRecord, error) {
previewRecordsCalled = true
return nil, nil
}
body := marshalVMwareRequest(t, map[string]any{
"name": "vc-a",
"host": "vc-b.lab.local",
"port": 443,
"username": "administrator@vsphere.local",
"password": "********",
"insecureSkipVerify": true,
"enabled": false,
})
req := httptest.NewRequest(http.MethodPut, "/api/vmware/connections/alpha", bytes.NewReader(body))
rec := httptest.NewRecorder()
handler.HandleUpdate(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status=%d, want %d: %s", rec.Code, http.StatusOK, rec.Body.String())
}
if previewRecordsCalled {
t.Fatal("expected disabled VMware update to bypass external inventory preview")
}
})
}
func TestContract_PlatformConnectionPreviewPreservesCanonicalEnabledDefaults(t *testing.T) {
t.Run("truenas new preview defaults omitted enabled to active", func(t *testing.T) {
setTrueNASFeatureForTest(t, true)
handler, _, monitor := newTrueNASHandlersForTest(t, nil)
registry := unifiedresources.NewRegistry(nil)
registry.IngestRecords(unifiedresources.SourceAgent, []unifiedresources.IngestRecord{
{
SourceID: "host-1",
Resource: unifiedresources.Resource{
ID: "host-1",
Type: unifiedresources.ResourceTypeAgent,
Name: "tower.local",
Status: unifiedresources.StatusOnline,
Agent: &unifiedresources.AgentData{
AgentID: "agent-1",
Hostname: "tower.local",
MachineID: "machine-1",
},
Identity: unifiedresources.ResourceIdentity{
MachineID: "machine-1",
Hostnames: []string{"tower.local"},
},
},
},
})
setUnexportedField(t, monitor, "resourceStore", monitoring.ResourceStoreInterface(unifiedresources.NewMonitorAdapter(registry)))
body := marshalTrueNASRequest(t, map[string]any{
"name": "tower",
"host": "tower.local",
"apiKey": "super-secret",
})
req := httptest.NewRequest(http.MethodPost, "/api/truenas/connections/preview", bytes.NewReader(body))
rec := httptest.NewRecorder()
handler.HandlePreviewConnection(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("expected 200, got %d: %s", rec.Code, rec.Body.String())
}
var preview MonitoredSystemLedgerPreviewResponse
if err := json.NewDecoder(rec.Body).Decode(&preview); err != nil {
t.Fatalf("decode preview response: %v", err)
}
if preview.Effect != "attaches_existing" {
t.Fatalf("Effect = %q, want attaches_existing", preview.Effect)
}
})
t.Run("vmware saved preview preserves stored enabled when omitted", func(t *testing.T) {
setVMwareFeatureForTest(t, true)
handler, persistence := newVMwareHandlersForTest(t)
monitor, _, _ := newTestMonitor(t)
handler.getMonitor = func(context.Context) *monitoring.Monitor { return monitor }
if err := persistence.SaveVMwareConfig([]config.VMwareVCenterInstance{
{
ID: "conn-1",
Name: "lab-vcenter",
Host: "vcsa.lab.local",
Username: "administrator@vsphere.local",
Password: "super-secret",
Enabled: true,
},
}); err != nil {
t.Fatalf("seed vmware config: %v", err)
}
previewRecordsCalled := false
handler.previewRecords = func(context.Context, config.VMwareVCenterInstance) ([]unifiedresources.IngestRecord, error) {
previewRecordsCalled = true
return []unifiedresources.IngestRecord{
{
SourceID: "vc-1:host:host-101",
Resource: unifiedresources.Resource{
Type: unifiedresources.ResourceTypeAgent,
Name: "esxi-01.lab.local",
Status: unifiedresources.StatusOnline,
VMware: &unifiedresources.VMwareData{
ConnectionID: "conn-1",
ConnectionName: "lab-vcenter",
ManagedObjectID: "host-101",
EntityType: "host",
HostUUID: "uuid-host-1",
},
},
Identity: unifiedresources.ResourceIdentity{
DMIUUID: "uuid-host-1",
Hostnames: []string{"esxi-01.lab.local"},
},
},
}, nil
}
body := marshalVMwareRequest(t, map[string]any{
"host": "edited.lab.local",
"username": "operator@vsphere.local",
"password": "********",
})
req := httptest.NewRequest(http.MethodPost, "/api/vmware/connections/conn-1/preview", bytes.NewReader(body))
rec := httptest.NewRecorder()
handler.HandlePreviewSavedConnection(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("expected 200, got %d: %s", rec.Code, rec.Body.String())
}
if !previewRecordsCalled {
t.Fatal("expected saved VMware preview with omitted enabled to stay on the active preview path")
}
})
}
func TestContract_DemoModeCommercialSurfacePolicy(t *testing.T) {
t.Run("hidden routes return not found", func(t *testing.T) {
next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
t.Fatalf("hidden commercial route should not reach downstream handler: %s %s", r.Method, r.URL.Path)
})
handler := DemoModeMiddleware(&config.Config{DemoMode: true}, next)
testCases := []struct {
method string
path string
}{
{method: http.MethodGet, path: "/api/license/status"},
{method: http.MethodGet, path: "/api/license/features"},
{method: http.MethodGet, path: "/api/license/commercial-posture"},
{method: http.MethodGet, path: "/api/license/entitlements"},
{method: http.MethodPost, path: "/api/license/activate"},
{method: http.MethodPost, path: "/api/license/clear"},
{method: http.MethodGet, path: "/api/license/monitored-system-ledger"},
{method: http.MethodPost, path: "/api/license/monitored-system-ledger/explain"},
{method: http.MethodPost, path: "/api/license/monitored-system-ledger/preview"},
{method: http.MethodPost, path: "/api/truenas/connections/preview"},
{method: http.MethodPost, path: "/api/truenas/connections/conn-1/preview"},
{method: http.MethodPost, path: "/api/vmware/connections/preview"},
{method: http.MethodPost, path: "/api/vmware/connections/conn-1/preview"},
{method: http.MethodGet, path: "/api/admin/orgs/t-tenant/billing-state"},
{method: http.MethodPut, path: "/api/admin/orgs/t-tenant/billing-state"},
{method: http.MethodGet, path: "/api/diagnostics"},
{method: http.MethodPost, path: "/api/diagnostics/docker/prepare-token"},
{method: http.MethodGet, path: "/api/logs/stream"},
{method: http.MethodGet, path: "/api/logs/download"},
{method: http.MethodGet, path: "/api/logs/level"},
{method: http.MethodPost, path: "/api/logs/level"},
{method: http.MethodGet, path: "/api/admin/users"},
{method: http.MethodGet, path: "/api/admin/users/"},
{method: http.MethodHead, path: "/api/admin/users"},
{method: http.MethodGet, path: "/api/discover"},
{method: http.MethodGet, path: "/api/discover/"},
{method: http.MethodHead, path: "/api/discover"},
{method: http.MethodGet, path: licensePurchaseStartPath},
}
for _, tc := range testCases {
req := httptest.NewRequest(tc.method, tc.path, nil)
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
if rec.Code != http.StatusNotFound {
t.Fatalf("%s %s status=%d, want %d: %s", tc.method, tc.path, rec.Code, http.StatusNotFound, rec.Body.String())
}
}
})
t.Run("runtime capabilities stay available but sanitize public demo limit details", func(t *testing.T) {
t.Setenv("PULSE_LICENSE_DEV_MODE", "true")
handlers := createTestHandler(t)
handlers.SetConfig(&config.Config{DemoMode: true})
licenseKey, err := licensetestsupport.GenerateLicenseForTesting("contract-demo@example.com", pkglicensing.TierPro, 24*time.Hour)
if err != nil {
t.Fatalf("GenerateLicenseForTesting: %v", err)
}
if _, err := handlers.Service(context.Background()).Activate(licenseKey); err != nil {
t.Fatalf("Activate() error = %v", err)
}
req := httptest.NewRequest(http.MethodGet, "/api/license/runtime-capabilities", nil)
rec := httptest.NewRecorder()
handlers.HandleRuntimeCapabilities(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status=%d, want %d: %s", rec.Code, http.StatusOK, rec.Body.String())
}
var payload RuntimeCapabilitiesPayload
if err := json.NewDecoder(rec.Body).Decode(&payload); err != nil {
t.Fatalf("decode payload: %v", err)
}
if len(payload.Capabilities) == 0 {
t.Fatalf("expected sanitized runtime capabilities to preserve capabilities, got %+v", payload)
}
if sliceContainsString(payload.Capabilities, pkglicensing.FeatureDemoFixtures) {
t.Fatalf("public demo runtime capabilities leaked internal feature %q: %v", pkglicensing.FeatureDemoFixtures, payload.Capabilities)
}
for _, limit := range payload.Limits {
if limit.Limit != 0 || limit.Current != 0 || limit.State != "ok" {
t.Fatalf("sanitized limit=%+v, want limit=0 current=0 state=ok", limit)
}
}
})
}
func TestContract_ReleaseDemoFixtureRuntimeGuardrailsRemainCanonical(t *testing.T) {
requiredSnippets := map[string][]string{
"licensing_handlers.go": {
"func (h *LicenseHandlers) syncReleaseDemoFixtureRuntime(orgID string, service *licenseService) {",
"if !shouldEnforceReleaseDemoFixtureRuntime() {",
"authorized := h.demoFixturesAuthorized(service)",
"mock.SetReleaseFixturesAuthorized(authorized)",
"if !wantsMockFixturesFromEnv() {",
},
"release_demo_fixtures_dev.go": {
"//go:build !release",
"return false",
},
"release_demo_fixtures_release.go": {
"//go:build release",
"return true",
},
}
for file, snippets := range requiredSnippets {
data, err := os.ReadFile(file)
if err != nil {
t.Fatalf("failed to read %s: %v", file, err)
}
source := string(data)
for _, snippet := range snippets {
if !strings.Contains(source, snippet) {
t.Fatalf("%s must contain %q", file, snippet)
}
}
}
}
func TestContract_SelfHostedPurchaseHandoffJSONSnapshot(t *testing.T) {
handler := createTestHandler(t)
handler.SetConfig(&config.Config{PublicURL: "https://pulse.example.com"})
var capturedReq struct {
Feature string `json:"feature"`
SuccessURL string `json:"success_url"`
CancelURL string `json:"cancel_url"`
PurchaseReturnJTI string `json:"purchase_return_jti"`
}
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/v1/checkout/portal-handoff" {
t.Fatalf("unexpected path %q", r.URL.Path)
}
if err := json.NewDecoder(r.Body).Decode(&capturedReq); err != nil {
t.Fatalf("decode checkout portal handoff request: %v", err)
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]any{
"portal_handoff_id": "cph_placeholder",
"feature": capturedReq.Feature,
})
}))
defer server.Close()
t.Setenv("PULSE_LICENSE_SERVER_URL", server.URL)
req := httptest.NewRequest(
http.MethodGet,
"https://pulse.example.com"+licensePurchaseStartPath+
"?feature=max_monitored_systems&utm_content=legacy-bookmark",
nil,
)
rec := httptest.NewRecorder()
handler.HandleCheckoutStart(rec, req)
if rec.Code != http.StatusSeeOther {
t.Fatalf("status=%d, want %d: %s", rec.Code, http.StatusSeeOther, rec.Body.String())
}
location := rec.Header().Get("Location")
redirectURL, err := url.Parse(location)
if err != nil {
t.Fatalf("parse redirect location: %v", err)
}
if redirectURL.Scheme != "https" || redirectURL.Host != "cloud.pulserelay.pro" || redirectURL.Path != "/portal" {
t.Fatalf("redirect location=%q, want Pulse Account portal", location)
}
if got := redirectURL.Query().Get("feature"); got != "" {
t.Fatalf("feature=%q, want omitted portal query", got)
}
if got := redirectURL.Query().Get("return_url"); got != "" {
t.Fatalf("return_url=%q, want omitted portal query", got)
}
if got := redirectURL.Query().Get("purchase_return_token"); got != "" {
t.Fatalf("purchase_return_token=%q, want omitted portal query", got)
}
if got := redirectURL.Query().Get("purchase_handoff_url"); got != "" {
t.Fatalf("purchase_handoff_url=%q, want omitted portal query", got)
}
if got := redirectURL.Query().Get("portal_handoff_id"); got != "cph_placeholder" {
t.Fatalf("portal_handoff_id=%q, want cph_placeholder", got)
}
activationURL, err := url.Parse(capturedReq.SuccessURL)
if err != nil {
t.Fatalf("parse success_url: %v", err)
}
returnToken := strings.TrimSpace(activationURL.Query().Get(licensePurchaseReturnTokenField))
if returnToken == "" {
t.Fatal("expected signed purchase_return_token in success_url")
}
for _, entitlementField := range []string{
"entitlements",
"runtime",
"commercial_posture",
"subscription_state",
"tier",
"plan_version",
} {
if got := activationURL.Query().Get(entitlementField); got != "" {
t.Fatalf("success_url query %s=%q, want omitted checkout UX metadata only", entitlementField, got)
}
}
signingKey, err := handler.purchaseReturnSigningKey()
if err != nil {
t.Fatalf("purchaseReturnSigningKey: %v", err)
}
claims, err := verifyPurchaseReturnTokenFromLicensing(returnToken, signingKey, "pulse.example.com", time.Now().UTC())
if err != nil {
t.Fatalf("verifyPurchaseReturnTokenFromLicensing: %v", err)
}
if claims.Feature != "self_hosted_plan" {
t.Fatalf("claims.Feature=%q, want self_hosted_plan", claims.Feature)
}
if claims.OrgID != "default" {
t.Fatalf("claims.OrgID=%q, want default", claims.OrgID)
}
if capturedReq.PurchaseReturnJTI != claims.ID {
t.Fatalf("purchase_return_jti=%q, want %q", capturedReq.PurchaseReturnJTI, claims.ID)
}
activationQuery := activationURL.Query()
activationQuery.Set(licensePurchaseReturnTokenField, "placeholder")
activationURL.RawQuery = activationQuery.Encode()
got, err := json.Marshal(struct {
PortalUtmContent string `json:"portal_utm_content"`
PortalHandoffID string `json:"portal_handoff_id"`
IntentFeature string `json:"intent_feature"`
PurchaseReturnJTI string `json:"purchase_return_jti"`
SuccessURLTemplate string `json:"success_url_template"`
CancelURL string `json:"cancel_url"`
}{
PortalUtmContent: redirectURL.Query().Get("utm_content"),
PortalHandoffID: redirectURL.Query().Get("portal_handoff_id"),
IntentFeature: capturedReq.Feature,
PurchaseReturnJTI: "placeholder",
SuccessURLTemplate: activationURL.String(),
CancelURL: capturedReq.CancelURL,
})
if err != nil {
t.Fatalf("marshal normalized handoff snapshot: %v", err)
}
const want = `{
"portal_utm_content":"legacy-bookmark",
"portal_handoff_id":"cph_placeholder",
"intent_feature":"self_hosted_plan",
"purchase_return_jti":"placeholder",
"success_url_template":"https://pulse.example.com/auth/license-purchase-activate?purchase_return_token=placeholder\u0026session_id=%7BCHECKOUT_SESSION_ID%7D",
"cancel_url":"https://pulse.example.com/settings/system/billing/plan?intent=self_hosted_plan\u0026purchase=cancelled"
}`
assertJSONSnapshot(t, got, want)
}
func TestContract_SelfHostedPurchaseHandoffUnavailableJSONSnapshot(t *testing.T) {
handler := createTestHandler(t)
handler.SetConfig(&config.Config{PublicURL: "https://pulse.example.com"})
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/v1/checkout/portal-handoff" {
t.Fatalf("unexpected path %q", r.URL.Path)
}
http.Error(w, "portal handoff unavailable", http.StatusBadGateway)
}))
defer server.Close()
t.Setenv("PULSE_LICENSE_SERVER_URL", server.URL)
req := httptest.NewRequest(
http.MethodGet,
"https://pulse.example.com"+licensePurchaseStartPath+"?feature=max_monitored_systems",
nil,
)
rec := httptest.NewRecorder()
handler.HandleCheckoutStart(rec, req)
got, err := json.MarshalIndent(struct {
StatusCode int `json:"status_code"`
TitlePresent bool `json:"title_present"`
RetryMessagePresent bool `json:"retry_message_present"`
OwnedRedirectPath bool `json:"owned_redirect_path"`
}{
StatusCode: rec.Code,
TitlePresent: strings.Contains(rec.Body.String(), "Pulse Account unavailable"),
RetryMessagePresent: strings.Contains(rec.Body.String(), "Retry from this instance in a moment"),
OwnedRedirectPath: strings.Contains(rec.Body.String(), "/settings/system/billing/plan?intent=self_hosted_plan&purchase=unavailable"),
}, "", "\t")
if err != nil {
t.Fatalf("marshal unavailable handoff snapshot: %v", err)
}
const want = `{
"status_code": 503,
"title_present": true,
"retry_message_present": true,
"owned_redirect_path": true
}`
assertJSONSnapshot(t, got, want)
}
func TestContract_SelfHostedPurchaseActivationBridgeCopyStaysPlanOwned(t *testing.T) {
continueRec := httptest.NewRecorder()
writeLicensePurchaseActivationContinuePage(
continueRec,
"session_contract",
"token_contract",
"self_hosted_plan",
"portal_handoff_contract",
)
successRec := httptest.NewRecorder()
writeLicensePurchaseActivationSuccessPage(successRec, "self_hosted_plan")
failureRec := httptest.NewRecorder()
writeLicensePurchaseActivationFailurePage(
failureRec,
http.StatusConflict,
"self_hosted_plan",
selfHostedBillingPurchaseExpired,
"Reopen the upgrade flow from Plans in Pulse.",
)
got, err := json.MarshalIndent(struct {
ContinueTitle bool `json:"continue_title"`
ContinueAction bool `json:"continue_action"`
ContinueLegacyBrand bool `json:"continue_legacy_brand"`
SuccessTitle bool `json:"success_title"`
SuccessReturnLabel bool `json:"success_return_label"`
SuccessLegacyBrand bool `json:"success_legacy_brand"`
FailureTitle bool `json:"failure_title"`
FailureOpenLabel bool `json:"failure_open_label"`
FailureGuidance bool `json:"failure_guidance"`
FailureLegacyBrand bool `json:"failure_legacy_brand"`
}{
ContinueTitle: strings.Contains(continueRec.Body.String(), "Finalizing plan upgrade"),
ContinueAction: strings.Contains(continueRec.Body.String(), "Continue to Plans"),
ContinueLegacyBrand: strings.Contains(continueRec.Body.String(), "Pulse Pro"),
SuccessTitle: strings.Contains(successRec.Body.String(), "Plan activated"),
SuccessReturnLabel: strings.Contains(successRec.Body.String(), "Return to Plans"),
SuccessLegacyBrand: strings.Contains(successRec.Body.String(), "Pulse Pro"),
FailureTitle: strings.Contains(failureRec.Body.String(), "Plan activation needs attention"),
FailureOpenLabel: strings.Contains(failureRec.Body.String(), "Open Plans"),
FailureGuidance: strings.Contains(failureRec.Body.String(), "Reopen the upgrade flow from Plans in Pulse."),
FailureLegacyBrand: strings.Contains(failureRec.Body.String(), "Pulse Pro"),
}, "", "\t")
if err != nil {
t.Fatalf("marshal activation bridge snapshot: %v", err)
}
const want = `{
"continue_title": true,
"continue_action": true,
"continue_legacy_brand": false,
"success_title": true,
"success_return_label": true,
"success_legacy_brand": false,
"failure_title": true,
"failure_open_label": true,
"failure_guidance": true,
"failure_legacy_brand": false
}`
assertJSONSnapshot(t, got, want)
}
func TestContract_SelfHostedPurchaseHandoffRejectsInsecureNonLoopbackCallbackURL(t *testing.T) {
handler := createTestHandler(t)
req := httptest.NewRequest(
http.MethodGet,
"http://192.168.1.25:7655"+licensePurchaseStartPath+"?feature=max_monitored_systems",
nil,
)
req.Host = "192.168.1.25:7655"
req.RemoteAddr = "192.168.1.50:54321"
rec := httptest.NewRecorder()
handler.HandleCheckoutStart(rec, req)
got, err := json.MarshalIndent(struct {
StatusCode int `json:"status_code"`
TitlePresent bool `json:"title_present"`
SecureFailureMessage bool `json:"secure_failure_message"`
OwnedRedirectPath bool `json:"owned_redirect_path"`
}{
StatusCode: rec.Code,
TitlePresent: strings.Contains(rec.Body.String(), "Pulse Account unavailable"),
SecureFailureMessage: strings.Contains(rec.Body.String(), "Pulse could not open Pulse Account from this instance right now"),
OwnedRedirectPath: strings.Contains(rec.Body.String(), "/settings/system/billing/plan?intent=self_hosted_plan&purchase=unavailable"),
}, "", "\t")
if err != nil {
t.Fatalf("marshal insecure handoff snapshot: %v", err)
}
const want = `{
"status_code": 503,
"title_present": true,
"secure_failure_message": true,
"owned_redirect_path": true
}`
assertJSONSnapshot(t, got, want)
}
func TestContract_HandoffExchangeJSONSnapshot(t *testing.T) {
key := []byte("test-handoff-key")
configDir := t.TempDir()
secretsDir := filepath.Join(configDir, "secrets")
if err := os.MkdirAll(secretsDir, 0o755); err != nil {
t.Fatalf("MkdirAll() error = %v", err)
}
if err := os.WriteFile(filepath.Join(secretsDir, "handoff.key"), key, 0o600); err != nil {
t.Fatalf("WriteFile() error = %v", err)
}
handler := HandleHandoffExchange(configDir)
tenantID := "tenant-contract"
saveHandoffTestOrganization(t, configDir, &models.Organization{
ID: tenantID,
DisplayName: "Contract Tenant",
Status: models.OrgStatusActive,
CreatedAt: time.Now().UTC(),
OwnerUserID: "operator.owner+mixed@pulserelay.pro",
})
t.Setenv("PULSE_TENANT_ID", "")
token := signHandoffToken(t, key, cloudHandoffClaims{
AccountID: "acct-contract",
Email: "Operator.Owner+Mixed@PulseRelay.Pro",
Role: "owner",
RegisteredClaims: jwt.RegisteredClaims{
ID: "jti-contract",
Subject: "user-contract",
Issuer: cloudHandoffIssuer,
Audience: jwt.ClaimStrings{tenantID},
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour)),
},
})
req := httptest.NewRequest(http.MethodPost, "/api/cloud/handoff/exchange?token="+token+"&format=json", nil)
req.Host = tenantID + ".cloud.pulserelay.pro"
req.Header.Set("Accept", "application/json")
req.Header.Set("X-Forwarded-Host", req.Host)
req.Header.Set("X-Forwarded-For", "127.0.0.1")
req.RemoteAddr = "127.0.0.1:1234"
rec := httptest.NewRecorder()
handler(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status=%d, want %d: %s", rec.Code, http.StatusOK, rec.Body.String())
}
const want = `{
"account_id":"acct-contract",
"email":"operator.owner+mixed@pulserelay.pro",
"exp":"placeholder",
"jti":"jti-contract",
"ok":true,
"role":"owner",
"tenant_id":"tenant-contract",
"user_id":"user-contract"
}`
var payload map[string]any
if err := json.Unmarshal(rec.Body.Bytes(), &payload); err != nil {
t.Fatalf("decode handoff payload: %v", err)
}
if _, ok := payload["exp"].(string); !ok {
t.Fatalf("exp missing or not a string: %+v", payload)
}
payload["exp"] = "placeholder"
got, err := json.Marshal(payload)
if err != nil {
t.Fatalf("marshal normalized handoff payload: %v", err)
}
assertJSONSnapshot(t, got, want)
}
func TestContract_TenantAIServiceAvoidsSnapshotProviderBridge(t *testing.T) {
tmp := t.TempDir()
mtp := config.NewMultiTenantPersistence(tmp)
defaultMonitor, _, _ := newTestMonitor(t)
tenantAdapter := unifiedresources.NewMonitorAdapter(unifiedresources.NewRegistry(nil))
tenantMonitor := &monitoring.Monitor{}
tenantMonitor.SetResourceStore(tenantAdapter)
mtm := &monitoring.MultiTenantMonitor{}
setUnexportedField(t, mtm, "monitors", map[string]*monitoring.Monitor{
"default": defaultMonitor,
"tenant-1": tenantMonitor,
})
handler := NewAISettingsHandler(mtp, mtm, nil)
handler.SetStateProvider(defaultMonitor)
ctx := context.WithValue(context.Background(), OrgIDContextKey, "tenant-1")
svc := handler.GetAIService(ctx)
if svc == nil {
t.Fatal("expected tenant AI service")
}
if svc.GetStateProvider() != nil {
t.Fatal("expected tenant AI service to avoid snapshot provider bridge")
}
if svc.GetPatrolService() == nil {
t.Fatal("expected tenant patrol service to initialize from canonical providers")
}
}
func TestContract_HostedCloudHandoffUsesExistingTenantOrganizationMembership(t *testing.T) {
key := []byte("test-handoff-key")
configDir := t.TempDir()
resetSessionStoreForTests()
t.Cleanup(resetSessionStoreForTests)
resetCSRFStoreForTests()
t.Cleanup(resetCSRFStoreForTests)
InitSessionStore(configDir)
InitCSRFStore(configDir)
secretsDir := filepath.Join(configDir, "secrets")
if err := os.MkdirAll(secretsDir, 0o755); err != nil {
t.Fatalf("mkdir secrets dir: %v", err)
}
if err := os.WriteFile(filepath.Join(secretsDir, "handoff.key"), key, 0o600); err != nil {
t.Fatalf("write handoff key: %v", err)
}
tenantID := "tenant-contract-membership"
mtp := saveHandoffTestOrganization(t, configDir, &models.Organization{
ID: tenantID,
DisplayName: "Contract Membership",
Status: models.OrgStatusActive,
CreatedAt: time.Now().UTC(),
OwnerUserID: "legacy-owner@example.com",
Members: []models.OrganizationMember{
{UserID: "legacy-owner@example.com", Role: models.OrgRoleOwner, AddedAt: time.Now().UTC()},
{UserID: "courtmanr@gmail.com", Role: models.OrgRoleOwner, AddedAt: time.Now().UTC()},
},
})
token := signHandoffToken(t, key, cloudHandoffClaims{
AccountID: "acct-contract-membership",
Email: "courtmanr@gmail.com",
Role: "owner",
RegisteredClaims: jwt.RegisteredClaims{
ID: "jti-contract-membership",
Subject: "user-contract-membership",
Issuer: cloudHandoffIssuer,
Audience: jwt.ClaimStrings{tenantID},
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour)),
},
})
form := url.Values{}
form.Set("token", token)
t.Setenv("PULSE_HOSTED_MODE", "true")
t.Setenv("PULSE_TENANT_ID", "")
t.Setenv("PULSE_PUBLIC_URL", "")
req := httptest.NewRequest(http.MethodPost, "/api/cloud/handoff/exchange?format=json", strings.NewReader(form.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.Header.Set("Accept", "application/json")
req.Host = tenantID + ".cloud.pulserelay.pro"
req.RemoteAddr = "198.51.100.20:1234"
rec := httptest.NewRecorder()
HandleHandoffExchange(configDir).ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status=%d, want %d: %s", rec.Code, http.StatusOK, rec.Body.String())
}
org, err := mtp.LoadOrganization(tenantID)
if err != nil {
t.Fatalf("load organization: %v", err)
}
if org.OwnerUserID != "legacy-owner@example.com" {
t.Fatalf("ownerUserID=%q, want %q", org.OwnerUserID, "legacy-owner@example.com")
}
if got := org.GetMemberRole("user-contract-membership"); got != models.OrgRoleOwner {
t.Fatalf("stable member role=%q, want %q", got, models.OrgRoleOwner)
}
if got := org.Members[1].Email; got != "courtmanr@gmail.com" {
t.Fatalf("canonicalized member email=%q, want %q", got, "courtmanr@gmail.com")
}
}
func TestContract_HostedDirectCloudHandoffUsesExistingTenantMembership(t *testing.T) {
key := []byte("test-direct-handoff-key")
configDir := t.TempDir()
resetPersistentAuthStoresForTests()
t.Cleanup(resetPersistentAuthStoresForTests)
if err := os.WriteFile(filepath.Join(configDir, cloudauth.HandoffKeyFile), key, 0o600); err != nil {
t.Fatalf("write direct handoff key: %v", err)
}
tenantID := "tenant-direct-contract"
mtp := saveHandoffTestOrganization(t, configDir, &models.Organization{
ID: tenantID,
DisplayName: "Direct Contract Membership",
Status: models.OrgStatusActive,
CreatedAt: time.Now().UTC(),
OwnerUserID: "legacy-owner@example.com",
Members: []models.OrganizationMember{
{UserID: "legacy-owner@example.com", Role: models.OrgRoleOwner, AddedAt: time.Now().UTC()},
{UserID: "courtmanr@gmail.com", Role: models.OrgRoleOwner, AddedAt: time.Now().UTC()},
},
})
token, err := cloudauth.SignWithClaims(key, cloudauth.Claims{
Email: "courtmanr@gmail.com",
TenantID: tenantID,
AccountID: "acct-direct-contract",
UserID: "user-direct-contract",
Role: "owner",
}, time.Hour)
if err != nil {
t.Fatalf("sign direct handoff claims: %v", err)
}
req := httptest.NewRequest(http.MethodGet, "/auth/cloud-handoff?token="+url.QueryEscape(token), nil)
rec := httptest.NewRecorder()
HandleCloudHandoff(configDir).ServeHTTP(rec, req)
if rec.Code != http.StatusTemporaryRedirect {
t.Fatalf("status=%d, want %d: %s", rec.Code, http.StatusTemporaryRedirect, rec.Body.String())
}
if got := rec.Header().Get("Location"); got != "/" {
t.Fatalf("redirect=%q, want %q", got, "/")
}
org, err := mtp.LoadOrganization(tenantID)
if err != nil {
t.Fatalf("load organization: %v", err)
}
if org.OwnerUserID != "legacy-owner@example.com" {
t.Fatalf("ownerUserID=%q, want %q", org.OwnerUserID, "legacy-owner@example.com")
}
if got := org.GetMemberRole("user-direct-contract"); got != models.OrgRoleOwner {
t.Fatalf("stable member role=%q, want %q", got, models.OrgRoleOwner)
}
if got := org.Members[1].Email; got != "courtmanr@gmail.com" {
t.Fatalf("canonicalized member email=%q, want %q", got, "courtmanr@gmail.com")
}
}
func TestContract_APITokenDeleteRejectsScopeEscalation(t *testing.T) {
router := &Router{
config: &config.Config{
APITokens: []config.APITokenRecord{
{
ID: "broad-token",
Name: "broad",
Hash: "hash-broad",
CreatedAt: time.Now().Add(-time.Hour),
Scopes: []string{config.ScopeWildcard},
OrgID: "default",
},
},
},
}
caller, err := config.NewAPITokenRecord(
"limited-caller-token-123.12345678",
"limited",
[]string{config.ScopeSettingsWrite},
)
if err != nil {
t.Fatalf("NewAPITokenRecord: %v", err)
}
req := httptest.NewRequest(http.MethodDelete, "/api/security/tokens/broad-token", nil)
req = req.WithContext(authpkg.WithAPIToken(req.Context(), caller))
rec := httptest.NewRecorder()
router.handleDeleteAPIToken(rec, req)
if rec.Code != http.StatusForbidden {
t.Fatalf("status = %d, want %d", rec.Code, http.StatusForbidden)
}
if !strings.Contains(rec.Body.String(), `Cannot delete token with scope "*"`) {
t.Fatalf("expected delete scope-escalation contract message, got %q", rec.Body.String())
}
if len(router.config.APITokens) != 1 || router.config.APITokens[0].ID != "broad-token" {
t.Fatalf("expected broader token to remain configured, got %+v", router.config.APITokens)
}
}
func TestContract_OrganizationInvitationTransportJSONSnapshot(t *testing.T) {
t.Setenv("PULSE_DEV", "true")
defer SetMultiTenantEnabled(false)
SetMultiTenantEnabled(true)
persistence := config.NewMultiTenantPersistence(t.TempDir())
h := NewOrgHandlers(persistence, nil)
createReq := withUser(
httptest.NewRequest(http.MethodPost, "/api/orgs", bytes.NewBufferString(`{"id":"acme","displayName":"Acme"}`)),
"alice",
)
createRec := httptest.NewRecorder()
h.HandleCreateOrg(createRec, createReq)
if createRec.Code != http.StatusCreated {
t.Fatalf("create org: status=%d body=%s", createRec.Code, createRec.Body.String())
}
inviteReq := withUser(
httptest.NewRequest(http.MethodPost, "/api/orgs/acme/members", bytes.NewBufferString(`{"userId":"bob","role":"viewer"}`)),
"alice",
)
inviteReq.SetPathValue("id", "acme")
inviteRec := httptest.NewRecorder()
h.HandleInviteMember(inviteRec, inviteReq)
if inviteRec.Code != http.StatusAccepted {
t.Fatalf("invite member: status=%d body=%s", inviteRec.Code, inviteRec.Body.String())
}
var invitePayload organizationAccessMutationResponse
if err := json.Unmarshal(inviteRec.Body.Bytes(), &invitePayload); err != nil {
t.Fatalf("decode invite payload: %v", err)
}
if invitePayload.Kind != "invitation" || invitePayload.Invitation == nil {
t.Fatalf("unexpected invite payload: %+v", invitePayload)
}
if invitePayload.Invitation.InvitedAt.IsZero() {
t.Fatalf("expected invite payload to include invitedAt")
}
inboxReq := withUser(httptest.NewRequest(http.MethodGet, "/api/org-invitations", nil), "bob")
inboxRec := httptest.NewRecorder()
h.HandleListMyInvitations(inboxRec, inboxReq)
if inboxRec.Code != http.StatusOK {
t.Fatalf("list my invitations: status=%d body=%s", inboxRec.Code, inboxRec.Body.String())
}
var inboxPayload []organizationUserInvitationResponse
if err := json.Unmarshal(inboxRec.Body.Bytes(), &inboxPayload); err != nil {
t.Fatalf("decode invitation inbox: %v", err)
}
if len(inboxPayload) != 1 {
t.Fatalf("expected 1 invitation, got %d", len(inboxPayload))
}
if inboxPayload[0].InvitedAt.IsZero() {
t.Fatalf("expected inbox payload to include invitedAt")
}
acceptReq := withUser(httptest.NewRequest(http.MethodPost, "/api/org-invitations/acme/accept", nil), "bob")
acceptReq.SetPathValue("id", "acme")
acceptRec := httptest.NewRecorder()
h.HandleAcceptMyInvitation(acceptRec, acceptReq)
if acceptRec.Code != http.StatusOK {
t.Fatalf("accept invitation: status=%d body=%s", acceptRec.Code, acceptRec.Body.String())
}
var acceptPayload organizationAccessMutationResponse
if err := json.Unmarshal(acceptRec.Body.Bytes(), &acceptPayload); err != nil {
t.Fatalf("decode accept payload: %v", err)
}
if acceptPayload.Kind != "member" || acceptPayload.Member == nil {
t.Fatalf("unexpected accept payload: %+v", acceptPayload)
}
if acceptPayload.Member.AddedAt.IsZero() {
t.Fatalf("expected accept payload to include addedAt")
}
got, err := json.Marshal(struct {
Invite struct {
Kind string `json:"kind"`
Invitation struct {
UserID string `json:"userId"`
Role string `json:"role"`
InvitedAt string `json:"invitedAt"`
InvitedBy string `json:"invitedBy"`
} `json:"invitation"`
} `json:"invite"`
Inbox []struct {
UserID string `json:"userId"`
Role string `json:"role"`
InvitedAt string `json:"invitedAt"`
InvitedBy string `json:"invitedBy"`
OrgID string `json:"orgId"`
OrgDisplayName string `json:"orgDisplayName"`
} `json:"inbox"`
Accept struct {
Kind string `json:"kind"`
Member struct {
UserID string `json:"userId"`
Role string `json:"role"`
AddedAt string `json:"addedAt"`
AddedBy string `json:"addedBy"`
} `json:"member"`
} `json:"accept"`
}{
Invite: struct {
Kind string `json:"kind"`
Invitation struct {
UserID string `json:"userId"`
Role string `json:"role"`
InvitedAt string `json:"invitedAt"`
InvitedBy string `json:"invitedBy"`
} `json:"invitation"`
}{
Kind: invitePayload.Kind,
Invitation: struct {
UserID string `json:"userId"`
Role string `json:"role"`
InvitedAt string `json:"invitedAt"`
InvitedBy string `json:"invitedBy"`
}{
UserID: invitePayload.Invitation.UserID,
Role: string(invitePayload.Invitation.Role),
InvitedAt: "placeholder",
InvitedBy: invitePayload.Invitation.InvitedBy,
},
},
Inbox: []struct {
UserID string `json:"userId"`
Role string `json:"role"`
InvitedAt string `json:"invitedAt"`
InvitedBy string `json:"invitedBy"`
OrgID string `json:"orgId"`
OrgDisplayName string `json:"orgDisplayName"`
}{
{
UserID: inboxPayload[0].UserID,
Role: string(inboxPayload[0].Role),
InvitedAt: "placeholder",
InvitedBy: inboxPayload[0].InvitedBy,
OrgID: inboxPayload[0].OrgID,
OrgDisplayName: inboxPayload[0].OrgDisplayName,
},
},
Accept: struct {
Kind string `json:"kind"`
Member struct {
UserID string `json:"userId"`
Role string `json:"role"`
AddedAt string `json:"addedAt"`
AddedBy string `json:"addedBy"`
} `json:"member"`
}{
Kind: acceptPayload.Kind,
Member: struct {
UserID string `json:"userId"`
Role string `json:"role"`
AddedAt string `json:"addedAt"`
AddedBy string `json:"addedBy"`
}{
UserID: acceptPayload.Member.UserID,
Role: string(acceptPayload.Member.Role),
AddedAt: "placeholder",
AddedBy: acceptPayload.Member.AddedBy,
},
},
})
if err != nil {
t.Fatalf("marshal normalized invitation transport snapshot: %v", err)
}
const want = `{
"invite":{
"kind":"invitation",
"invitation":{"userId":"bob","role":"viewer","invitedAt":"placeholder","invitedBy":"alice"}
},
"inbox":[
{"userId":"bob","role":"viewer","invitedAt":"placeholder","invitedBy":"alice","orgId":"acme","orgDisplayName":"Acme"}
],
"accept":{
"kind":"member",
"member":{"userId":"bob","role":"viewer","addedAt":"placeholder","addedBy":"alice"}
}
}`
assertJSONSnapshot(t, got, want)
}
func TestContract_OwnerTransferRequiresFreshSession(t *testing.T) {
t.Setenv("PULSE_DEV", "true")
defer SetMultiTenantEnabled(false)
SetMultiTenantEnabled(true)
configDir := t.TempDir()
resetSessionStoreForTests()
t.Cleanup(resetSessionStoreForTests)
InitSessionStore(configDir)
persistence := config.NewMultiTenantPersistence(t.TempDir())
h := NewOrgHandlers(persistence, nil)
createReq := withUser(
httptest.NewRequest(http.MethodPost, "/api/orgs", bytes.NewBufferString(`{"id":"acme","displayName":"Acme"}`)),
"alice",
)
createRec := httptest.NewRecorder()
h.HandleCreateOrg(createRec, createReq)
if createRec.Code != http.StatusCreated {
t.Fatalf("create org: status=%d body=%s", createRec.Code, createRec.Body.String())
}
inviteMemberForTest(t, h, "acme", "alice", "bob", "viewer", http.StatusAccepted)
acceptInvitationForTest(t, h, "acme", "bob")
sessionToken := generateSessionToken()
store := GetSessionStore()
store.CreateSession(sessionToken, time.Hour, "browser", "127.0.0.1", "alice")
store.mu.Lock()
store.sessions[sessionHash(sessionToken)].CreatedAt = time.Now().Add(-privilegedBrowserSessionMaxAge - time.Minute)
store.mu.Unlock()
transferReq := withUser(
httptest.NewRequest(http.MethodPost, "/api/orgs/acme/members", bytes.NewBufferString(`{"userId":"bob","role":"owner"}`)),
"alice",
)
transferReq.AddCookie(&http.Cookie{Name: cookieNameSession, Value: sessionToken})
transferReq.SetPathValue("id", "acme")
transferRec := httptest.NewRecorder()
h.HandleInviteMember(transferRec, transferReq)
if transferRec.Code != http.StatusUnauthorized {
t.Fatalf("status=%d, want %d: %s", transferRec.Code, http.StatusUnauthorized, transferRec.Body.String())
}
var payload APIError
if err := json.Unmarshal(transferRec.Body.Bytes(), &payload); err != nil {
t.Fatalf("decode error payload: %v", err)
}
if payload.Code != "fresh_session_required" {
t.Fatalf("code=%q, want fresh_session_required", payload.Code)
}
if payload.ErrorMessage != "Sign in again to transfer ownership" {
t.Fatalf("error=%q, want %q", payload.ErrorMessage, "Sign in again to transfer ownership")
}
if payload.StatusCode != http.StatusUnauthorized {
t.Fatalf("status_code=%d, want %d", payload.StatusCode, http.StatusUnauthorized)
}
}
func TestContract_OnboardingQRResponseJSONSnapshot(t *testing.T) {
payload := onboardingQRResponse{
Schema: onboardingSchemaVersion,
InstanceURL: "https://pulse.example.test",
InstanceID: "relay_abc123",
Relay: onboardingRelayDetails{
Enabled: true,
URL: "wss://relay.example.test/ws/app",
IdentityFingerprint: "AA:BB:CC",
IdentityPublicKey: "base64-key",
},
AuthToken: "token-123",
DeepLink: "pulse://connect?schema=pulse-mobile-onboarding-v1&instance_url=https%3A%2F%2Fpulse.example.test&instance_id=relay_abc123&relay_url=wss%3A%2F%2Frelay.example.test%2Fws%2Fapp&auth_token=token-123&identity_fingerprint=AA%3ABB%3ACC&identity_public_key=base64-key",
}.normalizeCollections()
got, err := json.Marshal(payload)
if err != nil {
t.Fatalf("marshal onboarding qr response: %v", err)
}
const want = `{
"schema":"pulse-mobile-onboarding-v1",
"instance_url":"https://pulse.example.test",
"instance_id":"relay_abc123",
"relay":{"enabled":true,"url":"wss://relay.example.test/ws/app","identity_fingerprint":"AA:BB:CC","identity_public_key":"base64-key"},
"auth_token":"token-123",
"deep_link":"pulse://connect?schema=pulse-mobile-onboarding-v1\u0026instance_url=https%3A%2F%2Fpulse.example.test\u0026instance_id=relay_abc123\u0026relay_url=wss%3A%2F%2Frelay.example.test%2Fws%2Fapp\u0026auth_token=token-123\u0026identity_fingerprint=AA%3ABB%3ACC\u0026identity_public_key=base64-key",
"diagnostics":[]
}`
assertJSONSnapshot(t, got, want)
}
func TestContract_HostedRelayConfigResponseJSONSnapshot(t *testing.T) {
router, _, instanceHost := newHostedRelayRuntimeTestRouter(t)
req := httptest.NewRequest(http.MethodGet, "/api/settings/relay", nil)
rec := httptest.NewRecorder()
router.handleGetRelayConfig(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status=%d, want %d: %s", rec.Code, http.StatusOK, rec.Body.String())
}
cfg, err := router.loadRelayConfigForRuntime(context.Background())
if err != nil {
t.Fatalf("loadRelayConfigForRuntime() error = %v", err)
}
body := rec.Body.String()
if strings.Contains(body, instanceHost) {
t.Fatalf("relay config response leaked hosted instance secret %q: %s", instanceHost, body)
}
if strings.Contains(body, cfg.IdentityPrivateKey) {
t.Fatalf("relay config response leaked identity private key: %s", body)
}
want := fmt.Sprintf(`{
"enabled":true,
"server_url":"%s",
"identity_public_key":"%s",
"identity_fingerprint":"%s"
}`, relay.DefaultServerURL, cfg.IdentityPublicKey, cfg.IdentityFingerprint)
assertJSONSnapshot(t, rec.Body.Bytes(), want)
}
func TestContract_DiagnosticsInfoJSONSnapshot(t *testing.T) {
payload := EmptyDiagnosticsInfo()
got, err := json.Marshal(payload.NormalizeCollections())
if err != nil {
t.Fatalf("marshal diagnostics info: %v", err)
}
const want = `{
"version":"",
"runtime":"",
"uptime":0,
"nodes":[],
"pbs":[],
"system":{"os":"","arch":"","goVersion":"","numCPU":0,"numGoroutine":0,"memoryMB":0},
"errors":[],
"nodeSnapshots":[],
"guestSnapshots":[],
"memorySources":[],
"memorySourceBreakdown":[]
}`
assertJSONSnapshot(t, got, want)
}
func TestContract_UpdatePlanManualFallbackJSONSnapshot(t *testing.T) {
payload := updates.UpdatePlan{
CanAutoUpdate: false,
RequiresRoot: false,
RollbackSupport: true,
EstimatedTime: "5-10 minutes",
Instructions: []string{
"Check out or build Pulse 6.0.0-rc.1 in your development workspace.",
"Stop the current development instance.",
"Restart Pulse with the rebuilt binary or release artifact against the existing data directory.",
},
Prerequisites: []string{
"A local development workspace for Pulse",
"Build tooling for the target version",
"A backup of the active data directory before replacing the binary",
},
}
got, err := json.Marshal(payload)
if err != nil {
t.Fatalf("marshal update plan: %v", err)
}
const want = `{
"canAutoUpdate":false,
"instructions":["Check out or build Pulse 6.0.0-rc.1 in your development workspace.","Stop the current development instance.","Restart Pulse with the rebuilt binary or release artifact against the existing data directory."],
"prerequisites":["A local development workspace for Pulse","Build tooling for the target version","A backup of the active data directory before replacing the binary"],
"estimatedTime":"5-10 minutes",
"requiresRoot":false,
"rollbackSupport":true
}`
assertJSONSnapshot(t, got, want)
}
func TestContract_EmptyUpdatePlanJSONSnapshot(t *testing.T) {
payload := updates.EmptyUpdatePlan()
got, err := json.Marshal(payload)
if err != nil {
t.Fatalf("marshal empty update plan: %v", err)
}
const want = `{
"canAutoUpdate":false,
"instructions":[],
"prerequisites":[],
"requiresRoot":false,
"rollbackSupport":false
}`
assertJSONSnapshot(t, got, want)
}
func TestContract_APITokenDTOJSONSnapshot(t *testing.T) {
now := time.Date(2026, 2, 8, 13, 14, 15, 0, time.UTC)
lastUsed := now.Add(30 * time.Minute)
expires := now.Add(24 * time.Hour)
payload := apiTokenDTO{
ID: "token-1",
Name: "Deploy token",
Prefix: "pulse_",
Suffix: "1234",
CreatedAt: now,
LastUsedAt: &lastUsed,
ExpiresAt: &expires,
Scopes: []string{"monitoring:read", "settings:write"},
OwnerUserID: "owner@example.com",
}
got, err := json.Marshal(payload)
if err != nil {
t.Fatalf("marshal API token dto: %v", err)
}
const want = `{
"id":"token-1",
"name":"Deploy token",
"prefix":"pulse_",
"suffix":"1234",
"createdAt":"2026-02-08T13:14:15Z",
"lastUsedAt":"2026-02-08T13:44:15Z",
"expiresAt":"2026-02-09T13:14:15Z",
"scopes":["monitoring:read","settings:write"],
"ownerUserId":"owner@example.com"
}`
assertJSONSnapshot(t, got, want)
}
func TestContract_APITokenScopeAliasNormalization(t *testing.T) {
raw := []string{"host-agent:report", "host-agent:config:read", "host-agent:manage", "host-agent:enroll"}
got, err := normalizeRequestedScopes(&raw)
if err != nil {
t.Fatalf("normalize requested scopes: %v", err)
}
want := []string{
config.ScopeAgentConfigRead,
config.ScopeAgentEnroll,
config.ScopeAgentManage,
config.ScopeAgentReport,
}
if !reflect.DeepEqual(got, want) {
t.Fatalf("normalized scopes = %#v, want %#v", got, want)
}
for _, legacy := range raw {
if strings.HasPrefix(legacy, "agent:") {
t.Fatalf("expected legacy alias input, got canonical scope %q", legacy)
}
}
}
func TestContract_HostedSubscriptionRequiredErrorJSONSnapshot(t *testing.T) {
rec := httptest.NewRecorder()
writeHostedSubscriptionRequiredError(rec)
if rec.Code != http.StatusPaymentRequired {
t.Fatalf("status = %d, want %d", rec.Code, http.StatusPaymentRequired)
}
const want = `{
"error":"subscription_required",
"message":"Your Cloud subscription is not active. Please check your billing status."
}`
assertJSONSnapshot(t, rec.Body.Bytes(), want)
}
func TestContract_InstallScriptReleaseAssetURL(t *testing.T) {
router := &Router{serverVersion: "v6.0.0-rc.1"}
got, err := router.installScriptReleaseAssetURL("install.sh")
if err != nil {
t.Fatalf("install script release asset URL: %v", err)
}
const want = "https://github.com/rcourtman/Pulse/releases/download/v6.0.0-rc.1/install.sh"
if got != want {
t.Fatalf("install script release asset URL = %q, want %q", got, want)
}
}
func TestContract_InstallScriptReleaseAssetURLUsesConfiguredRepo(t *testing.T) {
t.Setenv("PULSE_GITHUB_REPO", "example/pulse-fork")
router := &Router{serverVersion: "v6.0.0-rc.1"}
got, err := router.installScriptReleaseAssetURL("install.sh")
if err != nil {
t.Fatalf("install script release asset URL: %v", err)
}
const want = "https://github.com/example/pulse-fork/releases/download/v6.0.0-rc.1/install.sh"
if got != want {
t.Fatalf("install script release asset URL = %q, want %q", got, want)
}
}
func TestContract_InstallScriptReleaseAssetURLRejectsUnreleasedBuild(t *testing.T) {
router := &Router{serverVersion: "dev"}
if _, err := router.installScriptReleaseAssetURL("install.sh"); err == nil {
t.Fatalf("expected development build to reject release asset lookup")
}
}
func TestContract_InstallScriptReleaseAssetURLRejectsDevPrereleaseBuild(t *testing.T) {
router := &Router{serverVersion: "v6.0.0-dev"}
if _, err := router.installScriptReleaseAssetURL("install.sh"); err == nil {
t.Fatalf("expected dev prerelease build to reject release asset lookup")
}
}
func TestContract_AgentBinaryReleaseAssetURL(t *testing.T) {
router := &Router{serverVersion: "v6.0.0-rc.1"}
got, err := router.agentBinaryReleaseAssetURL("linux-arm64")
if err != nil {
t.Fatalf("agent binary release asset URL: %v", err)
}
const want = "https://github.com/rcourtman/Pulse/releases/download/v6.0.0-rc.1/pulse-agent-linux-arm64"
if got != want {
t.Fatalf("agent binary release asset URL = %q, want %q", got, want)
}
}
func TestContract_AgentBinaryReleaseAssetURLRejectsDevPrereleaseBuild(t *testing.T) {
router := &Router{serverVersion: "v6.0.0-dev"}
if _, err := router.agentBinaryReleaseAssetURL("linux-arm64"); err == nil {
t.Fatalf("expected dev prerelease build to reject release asset lookup")
}
}
func TestContract_ProxmoxInstallCommandIncludesInsecureForPlainHTTP(t *testing.T) {
got := buildProxmoxAgentInstallCommand(agentInstallCommandOptions{
BaseURL: "http://pulse.example.com:7655/",
Token: "token-123",
InstallType: "pve",
IncludeInstallType: true,
})
if !strings.Contains(got, "--url "+posixShellQuote("http://pulse.example.com:7655")) {
t.Fatalf("install command missing canonical base URL: %s", got)
}
if !strings.Contains(got, "--insecure") {
t.Fatalf("install command missing insecure flag for plain HTTP Pulse URL: %s", got)
}
if !strings.Contains(got, `--token-file "$token_file"`) {
t.Fatalf("install command missing token-file transport: %s", got)
}
}
func TestContract_ProxmoxInstallCommandUsesPrivilegeEscalationWrapper(t *testing.T) {
got := buildProxmoxAgentInstallCommand(agentInstallCommandOptions{
BaseURL: "https://pulse.example.com/",
Token: "token-123",
InstallType: "pve",
IncludeInstallType: true,
})
if !strings.Contains(got, `| { if [ "$(id -u)" -eq 0 ]; then bash -s --`) {
t.Fatalf("install command missing root-or-sudo wrapper: %s", got)
}
if !strings.Contains(got, `sudo bash -s --`) {
t.Fatalf("install command missing sudo fallback: %s", got)
}
if strings.Contains(got, "| bash -s -- --url") {
t.Fatalf("install command preserved raw bash pipe instead of governed wrapper: %s", got)
}
if !strings.Contains(got, `rm -f "$token_file"`) {
t.Fatalf("install command missing ephemeral token cleanup: %s", got)
}
}
func TestContract_OptionalAuthProxmoxInstallCommandOmitsToken(t *testing.T) {
got := buildProxmoxAgentInstallCommand(agentInstallCommandOptions{
BaseURL: "https://pulse.example.com/",
Token: "",
InstallType: "pve",
IncludeInstallType: true,
})
if strings.Contains(got, "--token") {
t.Fatalf("optional-auth install command preserved token flag: %s", got)
}
if !strings.Contains(got, "--url "+posixShellQuote("https://pulse.example.com")) {
t.Fatalf("optional-auth install command missing canonical base URL: %s", got)
}
}
func TestContract_ProxmoxInstallCommandNormalizesTrailingSlashBaseURL(t *testing.T) {
got := buildProxmoxAgentInstallCommand(agentInstallCommandOptions{
BaseURL: "https://pulse.example.com/base///",
Token: "token-123",
InstallType: "pbs",
IncludeInstallType: true,
})
if !strings.Contains(got, posixShellQuote("https://pulse.example.com/base/install.sh")) {
t.Fatalf("install command missing normalized install script URL: %s", got)
}
if !strings.Contains(got, "--url "+posixShellQuote("https://pulse.example.com/base")) {
t.Fatalf("install command missing normalized base URL: %s", got)
}
if !strings.Contains(got, `--token-file "$token_file"`) {
t.Fatalf("install command missing token-file transport: %s", got)
}
if strings.Contains(got, "//install.sh") {
t.Fatalf("install command preserved double-slash install path: %s", got)
}
}
func TestContract_DownloadUnifiedAgentReleaseAssetIncludesSignatureHeader(t *testing.T) {
router, tempDir := setupUnifiedAgentRouter(t)
router.serverVersion = "v6.0.0"
binContent := validTestUnifiedAgentBinary("linux-amd64")
binPath := filepath.Join(tempDir, "bin", "pulse-agent-linux-amd64")
if err := os.WriteFile(binPath, binContent, 0o755); err != nil {
t.Fatalf("write binary: %v", err)
}
if err := os.WriteFile(binPath+".sig", []byte("signed-agent"), 0o644); err != nil {
t.Fatalf("write signature: %v", err)
}
if err := os.WriteFile(binPath+".sshsig", []byte("signed-agent-ssh"), 0o644); err != nil {
t.Fatalf("write ssh signature: %v", err)
}
req := httptest.NewRequest(http.MethodGet, "/api/install/agent?arch=linux-amd64", nil)
w := httptest.NewRecorder()
router.handleDownloadUnifiedAgent(w, req)
if got := w.Header().Get(signatureHeaderName); got != "signed-agent" {
t.Fatalf("agent download signature header = %q, want %q", got, "signed-agent")
}
if got := w.Header().Get(sshSignatureHeaderName); got != encodedTestSSHSignature("signed-agent-ssh") {
t.Fatalf("agent download SSH signature header = %q, want %q", got, encodedTestSSHSignature("signed-agent-ssh"))
}
}
func TestContract_DownloadInstallScriptReleaseAssetIncludesSignatureHeader(t *testing.T) {
router, tempDir := setupUnifiedAgentRouter(t)
router.serverVersion = "v6.0.0"
scriptPath := filepath.Join(tempDir, "scripts", "install.sh")
if err := os.WriteFile(scriptPath, []byte("#!/bin/sh\necho ok\n"), 0o755); err != nil {
t.Fatalf("write install script: %v", err)
}
if err := os.WriteFile(scriptPath+".sig", []byte("signed-install-script"), 0o644); err != nil {
t.Fatalf("write install signature: %v", err)
}
if err := os.WriteFile(scriptPath+".sshsig", []byte("signed-install-script-ssh"), 0o644); err != nil {
t.Fatalf("write install SSH signature: %v", err)
}
req := httptest.NewRequest(http.MethodGet, "/install.sh", nil)
w := httptest.NewRecorder()
router.handleDownloadUnifiedInstallScript(w, req)
if got := w.Header().Get(signatureHeaderName); got != "signed-install-script" {
t.Fatalf("install script signature header = %q, want %q", got, "signed-install-script")
}
if got := w.Header().Get(sshSignatureHeaderName); got != encodedTestSSHSignature("signed-install-script-ssh") {
t.Fatalf("install script SSH signature header = %q, want %q", got, encodedTestSSHSignature("signed-install-script-ssh"))
}
}
func TestContract_SystemSettingsResponseJSONSnapshot(t *testing.T) {
payload := EmptySystemSettingsResponse()
payload.SystemSettings = config.SystemSettings{
PVEPollingInterval: 30,
PBSPollingInterval: 60,
PMGPollingInterval: 60,
BackupPollingInterval: 3600,
UpdateChannel: "rc",
AutoUpdateEnabled: false,
AutoUpdateCheckInterval: 24,
AutoUpdateTime: "03:00",
DiscoveryEnabled: true,
DiscoverySubnet: "10.0.0.0/24",
DiscoveryConfig: config.DefaultDiscoveryConfig(),
Theme: "dark",
TemperatureMonitoringEnabled: true,
DisableDockerUpdateActions: true,
}
payload.EnvOverrides = map[string]bool{
"PULSE_TELEMETRY": true,
}
got, err := json.Marshal(payload)
if err != nil {
t.Fatalf("marshal system settings response: %v", err)
}
const want = `{
"pvePollingInterval":30,
"pbsPollingInterval":60,
"pmgPollingInterval":60,
"backupPollingInterval":3600,
"updateChannel":"rc",
"autoUpdateEnabled":false,
"autoUpdateCheckInterval":24,
"autoUpdateTime":"03:00",
"discoveryEnabled":true,
"discoverySubnet":"10.0.0.0/24",
"discoveryConfig":{
"environment_override":"auto",
"subnet_blocklist":["169.254.0.0/16"],
"max_hosts_per_scan":1024,
"max_concurrent":50,
"enable_reverse_dns":true,
"scan_gateways":true,
"dial_timeout_ms":1000,
"http_timeout_ms":2000
},
"theme":"dark",
"fullWidthMode":false,
"allowEmbedding":false,
"temperatureMonitoringEnabled":true,
"hideLocalLogin":false,
"disableDockerUpdateActions":true,
"envOverrides":{"PULSE_TELEMETRY":true}
}`
assertJSONSnapshot(t, got, want)
}
func TestContract_CachedDiscoveryResponseJSONSnapshot(t *testing.T) {
response := map[string]interface{}{
"servers": []map[string]interface{}{
{
"ip": "10.0.0.1",
"port": 8006,
"type": "pve",
},
},
"errors": []string{
"Docker bridge network [10.0.0.2:8007]: request timed out",
},
"structured_errors": []map[string]interface{}{
{
"ip": "10.0.0.2",
"port": 8007,
"phase": "docker_bridge_network",
"error_type": "timeout",
"message": "request timed out",
"timestamp": "2023-11-14T22:13:20Z",
},
},
"environment": nil,
"cached": true,
"updated": int64(1700000010),
"age": float64(0),
}
got, err := json.Marshal(response)
if err != nil {
t.Fatalf("marshal cached discovery response: %v", err)
}
const want = `{
"age":0,
"cached":true,
"environment":null,
"errors":["Docker bridge network [10.0.0.2:8007]: request timed out"],
"servers":[{"ip":"10.0.0.1","port":8006,"type":"pve"}],
"structured_errors":[{"error_type":"timeout","ip":"10.0.0.2","message":"request timed out","phase":"docker_bridge_network","port":8007,"timestamp":"2023-11-14T22:13:20Z"}],
"updated":1700000010
}`
var wantValue interface{}
if err := json.Unmarshal([]byte(want), &wantValue); err != nil {
t.Fatalf("unmarshal wanted discovery response: %v", err)
}
var gotValue interface{}
if err := json.Unmarshal(got, &gotValue); err != nil {
t.Fatalf("unmarshal got discovery response: %v", err)
}
if !reflect.DeepEqual(gotValue, wantValue) {
t.Fatalf("cached discovery response mismatch\nwant: %s\ngot: %s", want, string(got))
}
}
func TestContract_DiscoveryCommandScanRoutesRequireSettingsWrite(t *testing.T) {
monitoringToken := "discovery-command-monitoring-token-123.12345678"
settingsToken := "discovery-command-settings-token-123.12345678"
monitoringRecord := newTokenRecord(t, monitoringToken, []string{config.ScopeMonitoringWrite}, nil)
settingsRecord := newTokenRecord(t, settingsToken, []string{config.ScopeSettingsWrite}, nil)
cfg := newTestConfigWithTokens(t, monitoringRecord, settingsRecord)
router := NewRouter(cfg, nil, nil, nil, nil, "1.0.0")
paths := []string{
"/api/discovery/run",
"/api/discovery/agent/host-1/resource-1",
"/api/discovery/vm/host-1/resource-1",
}
for _, path := range paths {
req := httptest.NewRequest(http.MethodPost, path, strings.NewReader(`{}`))
req.Header.Set("X-API-Token", monitoringToken)
rec := httptest.NewRecorder()
router.Handler().ServeHTTP(rec, req)
if rec.Code != http.StatusForbidden {
t.Fatalf("expected monitoring:write token to be forbidden for command-scan route %s, got %d", path, rec.Code)
}
if !strings.Contains(rec.Body.String(), config.ScopeSettingsWrite) {
t.Fatalf("expected command-scan route %s to require %q, got %q", path, config.ScopeSettingsWrite, rec.Body.String())
}
req = httptest.NewRequest(http.MethodPost, path, strings.NewReader(`{}`))
req.Header.Set("X-API-Token", settingsToken)
rec = httptest.NewRecorder()
router.Handler().ServeHTTP(rec, req)
if rec.Code == http.StatusForbidden && strings.Contains(rec.Body.String(), config.ScopeSettingsWrite) {
t.Fatalf("expected settings:write token to pass route scope for command-scan route %s, got %d: %s", path, rec.Code, rec.Body.String())
}
}
}
func TestContract_AutoRegisterRequestJSONSnapshot(t *testing.T) {
payload := AutoRegisterRequest{
Type: "pve",
Host: "https://pve.local:8006",
TokenID: "pulse-monitor@pve!pulse-homelab",
TokenValue: "secret-token",
ServerName: "pve-node-1",
AuthToken: "setup-token-123",
Source: "agent",
}
got, err := json.Marshal(payload)
if err != nil {
t.Fatalf("marshal auto-register request: %v", err)
}
const want = `{
"type":"pve",
"host":"https://pve.local:8006",
"tokenId":"pulse-monitor@pve!pulse-homelab",
"tokenValue":"secret-token",
"serverName":"pve-node-1",
"authToken":"setup-token-123",
"source":"agent"
}`
assertJSONSnapshot(t, got, want)
}
func TestContract_AutoRegisterScriptRequestJSONSnapshot(t *testing.T) {
payload := AutoRegisterRequest{
Type: "pve",
Host: "https://pve.local:8006",
TokenID: "pulse-monitor@pve!pulse-homelab",
TokenValue: "secret-token",
ServerName: "pve-node-1",
AuthToken: "setup-token-123",
Source: "script",
}
got, err := json.Marshal(payload)
if err != nil {
t.Fatalf("marshal script auto-register request: %v", err)
}
const want = `{
"type":"pve",
"host":"https://pve.local:8006",
"tokenId":"pulse-monitor@pve!pulse-homelab",
"tokenValue":"secret-token",
"serverName":"pve-node-1",
"authToken":"setup-token-123",
"source":"script"
}`
assertJSONSnapshot(t, got, want)
}
func TestContract_AutoRegisterCheckRequestJSONSnapshot(t *testing.T) {
payload := AutoRegisterRequest{
Type: "pve",
Host: "https://pve.local:8006",
CandidateHosts: []string{"https://pve.local:8006", "https://10.0.0.5:8006"},
ServerName: "pve-node-1",
AuthToken: "setup-token-123",
Source: "agent",
CheckRegistration: true,
}
got, err := json.Marshal(payload)
if err != nil {
t.Fatalf("marshal auto-register check request: %v", err)
}
const want = `{
"type":"pve",
"host":"https://pve.local:8006",
"candidateHosts":["https://pve.local:8006","https://10.0.0.5:8006"],
"serverName":"pve-node-1",
"authToken":"setup-token-123",
"source":"agent",
"checkRegistration":true
}`
assertJSONSnapshot(t, got, want)
}
func TestContract_AutoUnregisterRequestJSONSnapshot(t *testing.T) {
payload := AutoUnregisterRequest{
Type: "pve",
Host: "https://pve.local:8006",
TokenID: "pulse-monitor@pve!pulse-homelab",
ServerName: "pve-node-1",
AuthToken: "setup-token-123",
Source: "script",
}
got, err := json.Marshal(payload)
if err != nil {
t.Fatalf("marshal auto-unregister request: %v", err)
}
const want = `{
"type":"pve",
"host":"https://pve.local:8006",
"tokenId":"pulse-monitor@pve!pulse-homelab",
"serverName":"pve-node-1",
"authToken":"setup-token-123",
"source":"script"
}`
assertJSONSnapshot(t, got, want)
}
func TestContract_AutoRegisterScriptRequestRequiresExplicitSourceMarker(t *testing.T) {
payload := AutoRegisterRequest{
Type: "pve",
Host: "https://pve.local:8006",
TokenID: "pulse-monitor@pve!pulse-homelab",
TokenValue: "secret-token",
ServerName: "pve-node-1",
AuthToken: "setup-token-123",
Source: "script",
}
got, err := json.Marshal(payload)
if err != nil {
t.Fatalf("marshal script auto-register request: %v", err)
}
var decoded map[string]any
if err := json.Unmarshal(got, &decoded); err != nil {
t.Fatalf("decode script auto-register request: %v", err)
}
if decoded["source"] != "script" {
t.Fatalf("source = %#v, want explicit script marker", decoded["source"])
}
}
func TestContract_CanonicalAutoRegisterRequiresExplicitServerName(t *testing.T) {
payload := AutoRegisterRequest{
Type: "pve",
Host: "https://pve.local:8006",
TokenID: "pulse-monitor@pve!pulse-homelab",
TokenValue: "secret-token",
AuthToken: "setup-token-123",
Source: "script",
}
got, err := json.Marshal(payload)
if err != nil {
t.Fatalf("marshal serverName-free auto-register request: %v", err)
}
var decoded map[string]any
if err := json.Unmarshal(got, &decoded); err != nil {
t.Fatalf("decode serverName-free auto-register request: %v", err)
}
if _, ok := decoded["serverName"]; ok {
t.Fatalf("serverName = %#v, want omitted when caller does not send it", decoded["serverName"])
}
}
func TestContract_CanonicalAutoRegisterSetupTokenAuthFailureText(t *testing.T) {
tempDir := t.TempDir()
t.Setenv("PULSE_DATA_DIR", tempDir)
cfg := &config.Config{
DataPath: tempDir,
ConfigPath: tempDir,
}
handler := newTestConfigHandlers(t, cfg)
requestBody := AutoRegisterRequest{
Type: "pve",
Host: "https://pve.local:8006",
TokenID: "pulse-monitor@pve!pulse-homelab",
TokenValue: "secret-token",
ServerName: "pve-node-1",
Source: "script",
}
missingAuthJSON, err := json.Marshal(requestBody)
if err != nil {
t.Fatalf("marshal missing-auth auto-register request: %v", err)
}
missingReq := httptest.NewRequest(http.MethodPost, "/api/auto-register", bytes.NewReader(missingAuthJSON))
missingRec := httptest.NewRecorder()
handler.HandleAutoRegister(missingRec, missingReq)
if missingRec.Code != http.StatusUnauthorized {
t.Fatalf("missing-auth status = %d, want 401", missingRec.Code)
}
if got := missingRec.Body.String(); got != "Pulse setup token required\n" {
t.Fatalf("missing-auth body = %q, want canonical missing-setup-token guidance", got)
}
requestBody.AuthToken = "invalid-setup-token"
invalidAuthJSON, err := json.Marshal(requestBody)
if err != nil {
t.Fatalf("marshal invalid-auth auto-register request: %v", err)
}
invalidReq := httptest.NewRequest(http.MethodPost, "/api/auto-register", bytes.NewReader(invalidAuthJSON))
invalidRec := httptest.NewRecorder()
handler.HandleAutoRegister(invalidRec, invalidReq)
if invalidRec.Code != http.StatusUnauthorized {
t.Fatalf("invalid-auth status = %d, want 401", invalidRec.Code)
}
if got := invalidRec.Body.String(); got != "Invalid or expired setup token\n" {
t.Fatalf("invalid-auth body = %q, want canonical setup-token auth failure text", got)
}
const validSetupToken = "setup-token-123"
tokenHash := authpkg.HashAPIToken(validSetupToken)
handler.codeMutex.Lock()
handler.setupTokens[tokenHash] = &SetupTokenRecord{
ExpiresAt: time.Now().Add(5 * time.Minute),
NodeType: "pve",
}
handler.codeMutex.Unlock()
requestBody.AuthToken = validSetupToken
requestBody.TokenValue = ""
mismatchedJSON, err := json.Marshal(requestBody)
if err != nil {
t.Fatalf("marshal mismatched-completion auto-register request: %v", err)
}
mismatchedReq := httptest.NewRequest(http.MethodPost, "/api/auto-register", bytes.NewReader(mismatchedJSON))
mismatchedRec := httptest.NewRecorder()
handler.HandleAutoRegister(mismatchedRec, mismatchedReq)
if mismatchedRec.Code != http.StatusBadRequest {
t.Fatalf("mismatched-completion status = %d, want 400", mismatchedRec.Code)
}
if got := mismatchedRec.Body.String(); got != "tokenId and tokenValue must be provided together\n" {
t.Fatalf("mismatched-completion body = %q, want canonical token-pair guidance", got)
}
}
func TestContract_BootstrapTokenPersistenceJSONSnapshot(t *testing.T) {
tempDir := t.TempDir()
token, created, path, err := loadOrCreateBootstrapToken(tempDir)
if err != nil {
t.Fatalf("loadOrCreateBootstrapToken() error = %v", err)
}
if !created {
t.Fatal("expected bootstrap token to be created")
}
data, err := os.ReadFile(path)
if err != nil {
t.Fatalf("read persisted bootstrap token: %v", err)
}
snapshot := string(data)
if !strings.Contains(snapshot, `"version":2`) {
t.Fatalf("bootstrap token snapshot missing version: %s", snapshot)
}
if !strings.Contains(snapshot, `"token_ciphertext":"`) {
t.Fatalf("bootstrap token snapshot missing ciphertext field: %s", snapshot)
}
if !strings.Contains(snapshot, `"token_hash":"`) {
t.Fatalf("bootstrap token snapshot missing token hash field: %s", snapshot)
}
if strings.Contains(snapshot, token) {
t.Fatalf("bootstrap token snapshot leaked raw token: %s", snapshot)
}
}
func TestContract_InitializeBootstrapTokenLogsPathNotSecret(t *testing.T) {
tempDir := t.TempDir()
cfg := &config.Config{DataPath: tempDir}
var logBuf bytes.Buffer
logger := zerolog.New(&logBuf)
router := &Router{config: cfg, eventLogger: &logger}
router.initializeBootstrapToken()
token, created, tokenPath, err := loadOrCreateBootstrapToken(tempDir)
if err != nil {
t.Fatalf("loadOrCreateBootstrapToken() error = %v", err)
}
if created {
t.Fatal("expected bootstrap token to already exist after initialization")
}
logOutput := logBuf.String()
if !strings.Contains(logOutput, tokenPath) {
t.Fatalf("bootstrap token log missing path %q in %q", tokenPath, logOutput)
}
if !strings.Contains(logOutput, "pulse bootstrap-token") {
t.Fatalf("bootstrap token log missing local reveal guidance: %q", logOutput)
}
if strings.Contains(logOutput, token) {
t.Fatalf("bootstrap token leaked into logs: %q", logOutput)
}
}
func TestContract_QuickSecuritySetupBootstrapRetrievalGuidance(t *testing.T) {
resetPersistentAuthStoresForTests()
t.Cleanup(resetPersistentAuthStoresForTests)
tempDir := t.TempDir()
cfg := &config.Config{
DataPath: tempDir,
ConfigPath: tempDir,
}
router := &Router{
config: cfg,
persistence: config.NewConfigPersistence(cfg.DataPath),
}
router.initializeBootstrapToken()
InitPersistentAuthStores(tempDir)
handler := handleQuickSecuritySetupFixed(router)
body := `{"username":"bootstrap","password":"StrongPass!1","apiToken":"` + strings.Repeat("aa", 32) + `"}`
req := httptest.NewRequest(http.MethodPost, "/api/security/quick-setup", strings.NewReader(body))
req.RemoteAddr = "127.0.0.1:54321"
rec := httptest.NewRecorder()
authLimiter.Reset("127.0.0.1")
handler(rec, req)
if rec.Code != http.StatusUnauthorized {
t.Fatalf("quick setup status = %d, want 401 (%s)", rec.Code, rec.Body.String())
}
if got := rec.Body.String(); !strings.Contains(got, "pulse bootstrap-token") {
t.Fatalf("quick setup guidance = %q, want pulse bootstrap-token retrieval guidance", got)
}
if got := rec.Body.String(); strings.Contains(got, ".bootstrap_token") {
t.Fatalf("quick setup guidance = %q, want no raw .bootstrap_token scraping guidance", got)
}
}
// A valid bootstrap token authorizes quick setup from any origin — the token
// is the security boundary, and it's only readable by callers with filesystem
// access to the Pulse data directory. The loopback-only path remains for the
// no-token fallback (legacy console flow).
func TestContract_QuickSecuritySetupValidBootstrapTokenAuthorizesAnyOrigin(t *testing.T) {
body := `{"username":"bootstrap","password":"StrongPass!1","apiToken":"` + strings.Repeat("aa", 32) + `"}`
newHandler := func(t *testing.T) (http.HandlerFunc, string) {
t.Helper()
tempDir := t.TempDir()
cfg := &config.Config{
DataPath: tempDir,
ConfigPath: tempDir,
}
router := &Router{
config: cfg,
persistence: config.NewConfigPersistence(cfg.DataPath),
}
router.initializeBootstrapToken()
InitPersistentAuthStores(tempDir)
token, _, _, err := loadOrCreateBootstrapToken(tempDir)
if err != nil {
t.Fatalf("loadOrCreateBootstrapToken: %v", err)
}
return handleQuickSecuritySetupFixed(router), token
}
t.Run("remote IP with valid bootstrap token succeeds", func(t *testing.T) {
handler, token := newHandler(t)
req := httptest.NewRequest(http.MethodPost, "/api/security/quick-setup", strings.NewReader(body))
req.RemoteAddr = "198.51.100.41:54321"
req.Header.Set(bootstrapTokenHeader, token)
rec := httptest.NewRecorder()
authLimiter.Reset("198.51.100.41")
handler(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("remote quick setup with bootstrap token status = %d, want 200 (%s)", rec.Code, rec.Body.String())
}
foundSessionCookie := false
for _, cookie := range rec.Result().Cookies() {
if cookie.Name == cookieNameSession || cookie.Name == cookieNameSessionSecure {
foundSessionCookie = true
break
}
}
if !foundSessionCookie {
t.Fatal("expected remote quick setup with bootstrap token to issue a session cookie")
}
})
t.Run("remote IP without bootstrap token is rejected with bootstrap guidance", func(t *testing.T) {
handler, _ := newHandler(t)
req := httptest.NewRequest(http.MethodPost, "/api/security/quick-setup", strings.NewReader(body))
req.RemoteAddr = "198.51.100.41:54321"
rec := httptest.NewRecorder()
authLimiter.Reset("198.51.100.41")
handler(rec, req)
if rec.Code != http.StatusForbidden {
t.Fatalf("remote quick setup without bootstrap token status = %d, want 403 (%s)", rec.Code, rec.Body.String())
}
if got := rec.Body.String(); !strings.Contains(strings.ToLower(got), "bootstrap") {
t.Fatalf("remote no-token rejection guidance = %q, want bootstrap-token message", got)
}
})
t.Run("loopback IP with valid bootstrap token succeeds", func(t *testing.T) {
handler, token := newHandler(t)
req := httptest.NewRequest(http.MethodPost, "/api/security/quick-setup", strings.NewReader(body))
req.RemoteAddr = "127.0.0.1:54321"
req.Header.Set(bootstrapTokenHeader, token)
rec := httptest.NewRecorder()
authLimiter.Reset("127.0.0.1")
handler(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("loopback quick setup status = %d, want 200 (%s)", rec.Code, rec.Body.String())
}
})
}
func TestContract_ResetFirstRunSecurityResponseJSONSnapshot(t *testing.T) {
t.Setenv("PULSE_DEV", "true")
t.Setenv("NODE_ENV", "")
record := newTokenRecord(t, "contract-reset-first-run-token-123.12345678", []string{config.ScopeSettingsWrite}, nil)
cfg := newTestConfigWithTokens(t, record)
cfg.AuthUser = "admin"
cfg.AuthPass = "hashed-password"
envPath, err := writeAuthEnvFile(cfg.ConfigPath, cfg.DataPath, []byte("PULSE_AUTH_USER='admin'\n"))
if err != nil {
t.Fatalf("writeAuthEnvFile: %v", err)
}
router := NewRouter(cfg, nil, nil, nil, nil, "1.0.0")
req := httptest.NewRequest(http.MethodPost, "/api/security/dev/reset-first-run", nil)
req.Header.Set("X-API-Token", "contract-reset-first-run-token-123.12345678")
rec := httptest.NewRecorder()
router.Handler().ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("reset-first-run status = %d, want 200 (%s)", rec.Code, rec.Body.String())
}
var payload firstRunResetResponse
if err := json.Unmarshal(rec.Body.Bytes(), &payload); err != nil {
t.Fatalf("decode reset-first-run response: %v", err)
}
if strings.TrimSpace(payload.BootstrapToken) == "" {
t.Fatal("reset-first-run response missing bootstrapToken")
}
if got, want := payload.BootstrapTokenPath, filepath.Join(cfg.DataPath, bootstrapTokenFilename); got != want {
t.Fatalf("reset-first-run bootstrapTokenPath = %q, want %q", got, want)
}
if _, err := os.Stat(envPath); !os.IsNotExist(err) {
t.Fatalf("reset-first-run should remove auth env file, stat err = %v", err)
}
got, err := json.Marshal(firstRunResetResponse{
BootstrapToken: "bootstrap-token-placeholder",
BootstrapTokenPath: "bootstrap-token-path-placeholder",
})
if err != nil {
t.Fatalf("marshal reset-first-run response snapshot: %v", err)
}
const want = `{
"bootstrapToken":"bootstrap-token-placeholder",
"bootstrapTokenPath":"bootstrap-token-path-placeholder"
}`
assertJSONSnapshot(t, got, want)
}
func TestContract_ResetFirstRunSecurityClearsEnvBackedStatus(t *testing.T) {
t.Setenv("PULSE_DEV", "true")
t.Setenv("NODE_ENV", "")
t.Setenv("PULSE_AUTH_USER", "admin")
t.Setenv("PULSE_AUTH_PASS", "hashed-password")
record := newTokenRecord(t, "contract-reset-first-run-token-456.12345678", []string{config.ScopeSettingsWrite}, nil)
cfg := newTestConfigWithTokens(t, record)
cfg.AuthUser = "admin"
cfg.AuthPass = "hashed-password"
router := NewRouter(cfg, nil, nil, nil, nil, "1.0.0")
resetReq := httptest.NewRequest(http.MethodPost, "/api/security/dev/reset-first-run", nil)
resetReq.Header.Set("X-API-Token", "contract-reset-first-run-token-456.12345678")
resetRec := httptest.NewRecorder()
router.Handler().ServeHTTP(resetRec, resetReq)
if resetRec.Code != http.StatusOK {
t.Fatalf("reset-first-run status = %d, want 200 (%s)", resetRec.Code, resetRec.Body.String())
}
statusReq := httptest.NewRequest(http.MethodGet, "/api/security/status", nil)
statusRec := httptest.NewRecorder()
router.Handler().ServeHTTP(statusRec, statusReq)
if statusRec.Code != http.StatusOK {
t.Fatalf("security status = %d, want 200 (%s)", statusRec.Code, statusRec.Body.String())
}
var payload map[string]any
if err := json.Unmarshal(statusRec.Body.Bytes(), &payload); err != nil {
t.Fatalf("decode security status payload: %v", err)
}
if got, _ := payload["hasAuthentication"].(bool); got {
t.Fatalf("hasAuthentication = %v, want false", payload["hasAuthentication"])
}
if got, _ := payload["bootstrapTokenPath"].(string); strings.TrimSpace(got) == "" {
t.Fatalf("bootstrapTokenPath = %v, want non-empty", payload["bootstrapTokenPath"])
}
}
func TestContract_SetupScriptURLResponseJSONSnapshot(t *testing.T) {
payload := map[string]any{
"type": "pve",
"host": "https://pve.local:8006",
"url": "https://pulse.example/api/setup-script?host=https%3A%2F%2Fpve.local%3A8006&pulse_url=https%3A%2F%2Fpulse.example&type=pve",
"downloadURL": "https://pulse.example/api/setup-script?host=https%3A%2F%2Fpve.local%3A8006&pulse_url=https%3A%2F%2Fpulse.example&setup_token=setup-token-123&type=pve",
"scriptFileName": "pulse-setup-pve.sh",
"command": "curl -fsSL 'https://pulse.example/api/setup-script?host=https%3A%2F%2Fpve.local%3A8006&pulse_url=https%3A%2F%2Fpulse.example&type=pve' | { if [ \"$(id -u)\" -eq 0 ]; then PULSE_SETUP_TOKEN='setup-token-123' bash; elif command -v sudo >/dev/null 2>&1; then sudo env PULSE_SETUP_TOKEN='setup-token-123' bash; else echo \"Root privileges required. Run as root (su -) and retry.\" >&2; exit 1; fi; }",
"commandWithEnv": "curl -fsSL 'https://pulse.example/api/setup-script?host=https%3A%2F%2Fpve.local%3A8006&pulse_url=https%3A%2F%2Fpulse.example&type=pve' | { if [ \"$(id -u)\" -eq 0 ]; then PULSE_SETUP_TOKEN='setup-token-123' bash; elif command -v sudo >/dev/null 2>&1; then sudo env PULSE_SETUP_TOKEN='setup-token-123' bash; else echo \"Root privileges required. Run as root (su -) and retry.\" >&2; exit 1; fi; }",
"commandWithoutEnv": "curl -fsSL 'https://pulse.example/api/setup-script?host=https%3A%2F%2Fpve.local%3A8006&pulse_url=https%3A%2F%2Fpulse.example&type=pve' | { if [ \"$(id -u)\" -eq 0 ]; then bash; elif command -v sudo >/dev/null 2>&1; then sudo bash; else echo \"Root privileges required. Run as root (su -) and retry.\" >&2; exit 1; fi; }",
"expires": int64(1900000000),
"setupToken": "setup-token-123",
"tokenHint": "set…123",
}
got, err := json.Marshal(payload)
if err != nil {
t.Fatalf("marshal setup-script-url response: %v", err)
}
const want = `{
"command":"curl -fsSL 'https://pulse.example/api/setup-script?host=https%3A%2F%2Fpve.local%3A8006\u0026pulse_url=https%3A%2F%2Fpulse.example\u0026type=pve' | { if [ \"$(id -u)\" -eq 0 ]; then PULSE_SETUP_TOKEN='setup-token-123' bash; elif command -v sudo \u003e/dev/null 2\u003e\u00261; then sudo env PULSE_SETUP_TOKEN='setup-token-123' bash; else echo \"Root privileges required. Run as root (su -) and retry.\" \u003e\u00262; exit 1; fi; }",
"commandWithEnv":"curl -fsSL 'https://pulse.example/api/setup-script?host=https%3A%2F%2Fpve.local%3A8006\u0026pulse_url=https%3A%2F%2Fpulse.example\u0026type=pve' | { if [ \"$(id -u)\" -eq 0 ]; then PULSE_SETUP_TOKEN='setup-token-123' bash; elif command -v sudo \u003e/dev/null 2\u003e\u00261; then sudo env PULSE_SETUP_TOKEN='setup-token-123' bash; else echo \"Root privileges required. Run as root (su -) and retry.\" \u003e\u00262; exit 1; fi; }",
"commandWithoutEnv":"curl -fsSL 'https://pulse.example/api/setup-script?host=https%3A%2F%2Fpve.local%3A8006\u0026pulse_url=https%3A%2F%2Fpulse.example\u0026type=pve' | { if [ \"$(id -u)\" -eq 0 ]; then bash; elif command -v sudo \u003e/dev/null 2\u003e\u00261; then sudo bash; else echo \"Root privileges required. Run as root (su -) and retry.\" \u003e\u00262; exit 1; fi; }",
"downloadURL":"https://pulse.example/api/setup-script?host=https%3A%2F%2Fpve.local%3A8006\u0026pulse_url=https%3A%2F%2Fpulse.example\u0026setup_token=setup-token-123\u0026type=pve",
"expires":1900000000,
"host":"https://pve.local:8006",
"scriptFileName":"pulse-setup-pve.sh",
"setupToken":"setup-token-123",
"tokenHint":"set…123",
"type":"pve",
"url":"https://pulse.example/api/setup-script?host=https%3A%2F%2Fpve.local%3A8006\u0026pulse_url=https%3A%2F%2Fpulse.example\u0026type=pve"
}`
assertJSONSnapshot(t, got, want)
}
func TestContract_SecurityStatusIncludesSessionCapabilitiesDemoMode(t *testing.T) {
cfg := newTestConfigWithTokens(t)
cfg.DemoMode = true
router := NewRouter(cfg, nil, nil, nil, nil, "1.0.0")
req := httptest.NewRequest(http.MethodGet, "/api/security/status", nil)
rec := httptest.NewRecorder()
router.Handler().ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("security status = %d, want 200 (%s)", rec.Code, rec.Body.String())
}
var payload map[string]any
if err := json.Unmarshal(rec.Body.Bytes(), &payload); err != nil {
t.Fatalf("decode security status payload: %v", err)
}
sessionCapabilities, ok := payload["sessionCapabilities"].(map[string]any)
if !ok {
t.Fatalf("sessionCapabilities = %#v, want object", payload["sessionCapabilities"])
}
if got, _ := sessionCapabilities["demoMode"].(bool); !got {
t.Fatalf("sessionCapabilities.demoMode = %v, want true", sessionCapabilities["demoMode"])
}
presentationPolicy, ok := payload["presentationPolicy"].(map[string]any)
if !ok {
t.Fatalf("presentationPolicy = %#v, want object", payload["presentationPolicy"])
}
for key, want := range map[string]bool{
"demoMode": true,
"readOnly": true,
"hideCommercial": true,
"hideUpgrade": true,
} {
if got, _ := presentationPolicy[key].(bool); got != want {
t.Fatalf("presentationPolicy.%s = %v, want %v", key, presentationPolicy[key], want)
}
}
}
func TestContract_SecurityStatusPresentationPolicyDefaultsHideUpgradeOutsideHosted(t *testing.T) {
cfg := newTestConfigWithTokens(t)
cfg.DemoMode = false
router := NewRouter(cfg, nil, nil, nil, nil, "1.0.0")
req := httptest.NewRequest(http.MethodGet, "/api/security/status", nil)
rec := httptest.NewRecorder()
router.Handler().ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("security status = %d, want 200 (%s)", rec.Code, rec.Body.String())
}
var payload map[string]any
if err := json.Unmarshal(rec.Body.Bytes(), &payload); err != nil {
t.Fatalf("decode security status payload: %v", err)
}
presentationPolicy, ok := payload["presentationPolicy"].(map[string]any)
if !ok {
t.Fatalf("presentationPolicy = %#v, want object", payload["presentationPolicy"])
}
for key, want := range map[string]bool{
"demoMode": false,
"readOnly": false,
"hideCommercial": false,
"hideUpgrade": true,
} {
if got, _ := presentationPolicy[key].(bool); got != want {
t.Fatalf("presentationPolicy.%s = %v, want %v", key, presentationPolicy[key], want)
}
}
}
func TestContract_SecurityStatusSplitsAuditLogCapabilityFromSettingsRead(t *testing.T) {
prevAuthorizer := authpkg.GetAuthorizer()
authpkg.SetAuthorizer(&allowRulesAuthorizer{
rules: map[string]bool{
"read:audit_logs": true,
"admin:audit_logs": true,
},
})
defer authpkg.SetAuthorizer(prevAuthorizer)
cases := []struct {
name string
scopes []string
wantAuditLog bool
wantAuditWebhooksRead bool
}{
{
name: "settings read stays limited to audit webhooks",
scopes: []string{config.ScopeSettingsRead},
wantAuditLog: false,
wantAuditWebhooksRead: true,
},
{
name: "audit read unlocks audit log capability",
scopes: []string{config.ScopeSettingsRead, config.ScopeAuditRead},
wantAuditLog: true,
wantAuditWebhooksRead: true,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
rawToken := fmt.Sprintf("contract-audit-scope-%s.12345678", strings.ReplaceAll(tc.name, " ", "-"))
record := newTokenRecord(t, rawToken, tc.scopes, nil)
cfg := newTestConfigWithTokens(t, record)
router := NewRouter(cfg, nil, nil, nil, nil, "1.0.0")
req := httptest.NewRequest(http.MethodGet, "/api/security/status", nil)
req.Header.Set("X-API-Token", rawToken)
rec := httptest.NewRecorder()
router.Handler().ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("security status = %d, want 200 (%s)", rec.Code, rec.Body.String())
}
var payload struct {
SettingsCapabilities securityStatusSettingsCapabilities `json:"settingsCapabilities"`
TokenScopes []string `json:"tokenScopes"`
}
if err := json.Unmarshal(rec.Body.Bytes(), &payload); err != nil {
t.Fatalf("decode security status payload: %v", err)
}
if payload.SettingsCapabilities.AuditLog != tc.wantAuditLog {
t.Fatalf("settingsCapabilities.auditLog = %v, want %v", payload.SettingsCapabilities.AuditLog, tc.wantAuditLog)
}
if payload.SettingsCapabilities.AuditWebhooksRead != tc.wantAuditWebhooksRead {
t.Fatalf(
"settingsCapabilities.auditWebhooksRead = %v, want %v",
payload.SettingsCapabilities.AuditWebhooksRead,
tc.wantAuditWebhooksRead,
)
}
if !reflect.DeepEqual(payload.TokenScopes, tc.scopes) {
t.Fatalf("tokenScopes = %#v, want %#v", payload.TokenScopes, tc.scopes)
}
})
}
}
func TestContract_SetupScriptURLRejectsNonCanonicalRequestJSON(t *testing.T) {
handler := newTestConfigHandlers(t, &config.Config{DataPath: t.TempDir()})
req := httptest.NewRequest(
http.MethodPost,
"/api/setup-script-url",
bytes.NewBufferString(`{"type":"pve","host":"pve.local","setupToken":"unexpected"}`),
)
rec := httptest.NewRecorder()
handler.HandleSetupScriptURL(rec, req)
if rec.Code != http.StatusBadRequest {
t.Fatalf("status = %d, want 400", rec.Code)
}
if got := rec.Body.String(); got != "Invalid request\n" {
t.Fatalf("body = %q, want canonical invalid-request guidance", got)
}
}
func TestContract_SetupBootstrapRejectsPBSBackupPerms(t *testing.T) {
handler := newTestConfigHandlers(t, &config.Config{DataPath: t.TempDir()})
setupScriptReq := httptest.NewRequest(
http.MethodGet,
"/api/setup-script?type=pbs&host=https://pbs.local:8007&pulse_url=https://pulse.example&backup_perms=true",
nil,
)
setupScriptRec := httptest.NewRecorder()
handler.HandleSetupScript(setupScriptRec, setupScriptReq)
if setupScriptRec.Code != http.StatusBadRequest {
t.Fatalf("setup-script status = %d, want 400", setupScriptRec.Code)
}
if got := setupScriptRec.Body.String(); got != "backup_perms is only supported for type 'pve'\n" {
t.Fatalf("setup-script body = %q, want canonical backup-perms guidance", got)
}
setupURLReq := httptest.NewRequest(
http.MethodPost,
"/api/setup-script-url",
bytes.NewBufferString(`{"type":"pbs","host":"pbs.local","backupPerms":true}`),
)
setupURLRec := httptest.NewRecorder()
handler.HandleSetupScriptURL(setupURLRec, setupURLReq)
if setupURLRec.Code != http.StatusBadRequest {
t.Fatalf("setup-script-url status = %d, want 400", setupURLRec.Code)
}
if got := setupURLRec.Body.String(); got != "backupPerms is only supported for type 'pve'\n" {
t.Fatalf("setup-script-url body = %q, want canonical backup-perms guidance", got)
}
}
func TestContract_CanonicalAutoRegisterSourceContract(t *testing.T) {
if !isCanonicalAutoRegisterSource("agent") {
t.Fatalf("agent source should be accepted")
}
if !isCanonicalAutoRegisterSource("script") {
t.Fatalf("script source should be accepted")
}
if isCanonicalAutoRegisterSource("manual") {
t.Fatalf("manual source should be rejected")
}
}
func TestContract_CanonicalAutoRegisterTypeContract(t *testing.T) {
if !isCanonicalAutoRegisterType("pve") {
t.Fatalf("pve type should be accepted")
}
if !isCanonicalAutoRegisterType("pbs") {
t.Fatalf("pbs type should be accepted")
}
if isCanonicalAutoRegisterType("pmg") {
t.Fatalf("pmg type should be rejected")
}
}
func TestContract_CanonicalAutoRegisterTokenIDContract(t *testing.T) {
if !isCanonicalAutoRegisterTokenID("pve", "pulse-monitor@pve!pulse-homelab") {
t.Fatalf("canonical pve token id should be accepted")
}
if !isCanonicalAutoRegisterTokenID("pbs", "pulse-monitor@pbs!pulse-backup") {
t.Fatalf("canonical pbs token id should be accepted")
}
if isCanonicalAutoRegisterTokenID("pve", "pulse-monitor@pve!token") {
t.Fatalf("non-pulse-managed pve token suffix should be rejected")
}
if isCanonicalAutoRegisterTokenID("pve", "pulse@pve!token") {
t.Fatalf("non-canonical token id should be rejected")
}
if isCanonicalAutoRegisterTokenID("pve", "pulse-monitor@pbs!pulse-backup") {
t.Fatalf("cross-type token id should be rejected")
}
if isCanonicalAutoRegisterTokenID("pve", "pulse-monitor@pve!") {
t.Fatalf("empty canonical token suffix should be rejected")
}
if isCanonicalAutoRegisterTokenID("pve", "pulse-monitor@pve!pulse-") {
t.Fatalf("empty pulse-managed token slug should be rejected")
}
}
func TestContract_CanonicalAutoRegisterMatchMessageContract(t *testing.T) {
if got := canonicalAutoRegisterMatchMessage("resolved host identity"); got != "Canonical auto-register matched existing node by resolved host identity" {
t.Fatalf("resolved-host message = %q", got)
}
if got := canonicalAutoRegisterMatchMessage("DHCP continuity token identity"); got != "Canonical auto-register matched existing node by DHCP continuity token identity" {
t.Fatalf("dhcp message = %q", got)
}
if got := canonicalAutoRegisterMatchMessage("host; updated token in-place"); got != "Canonical auto-register matched existing node by host; updated token in-place" {
t.Fatalf("host-update message = %q", got)
}
if strings.Contains(canonicalAutoRegisterMatchMessage("resolved host identity"), "Secure auto-register") {
t.Fatalf("canonical match message must not preserve secure auto-register wording")
}
}
func TestContract_CanonicalAutoRegisterCompletionPayloadMessageContract(t *testing.T) {
if got := canonicalAutoRegisterCompletionPayloadMessage(); got != "Incomplete canonical auto-register token completion payload" {
t.Fatalf("completion-payload message = %q", got)
}
if strings.Contains(canonicalAutoRegisterCompletionPayloadMessage(), "secure token completion") {
t.Fatalf("canonical completion-payload message must not preserve secure wording")
}
}
func TestContract_CanonicalAutoRegisterMissingFieldsMessageContract(t *testing.T) {
if got := canonicalAutoRegisterMissingFieldsMessage("", "", false, ""); got != "Missing required canonical auto-register fields: type, host, tokenId/tokenValue, serverName" {
t.Fatalf("all-missing message = %q", got)
}
if got := canonicalAutoRegisterMissingFieldsMessage("pve", "https://pve.local:8006", true, ""); got != "Missing required canonical auto-register fields: serverName" {
t.Fatalf("serverName-only message = %q", got)
}
}
func TestContract_CanonicalAutoUnregisterMissingFieldsMessageContract(t *testing.T) {
if got := canonicalAutoUnregisterMissingFieldsMessage("", "", ""); got != "Missing required canonical auto-unregister fields: type, host, serverName" {
t.Fatalf("all-missing auto-unregister message = %q", got)
}
if got := canonicalAutoUnregisterMissingFieldsMessage("pve", "https://pve.local:8006", ""); got != "Missing required canonical auto-unregister fields: serverName" {
t.Fatalf("serverName-only auto-unregister message = %q", got)
}
}
func TestContract_CanonicalAutoRegisterDirectValidationContract(t *testing.T) {
tempDir := t.TempDir()
t.Setenv("PULSE_DATA_DIR", tempDir)
cfg := &config.Config{
DataPath: tempDir,
ConfigPath: tempDir,
}
handler := newTestConfigHandlers(t, cfg)
reqBody := AutoRegisterRequest{
Type: "pve",
Host: "https://pve.local:8006",
Source: "script",
TokenID: "pulse-monitor@pve!pulse-homelab",
TokenValue: "secret-token",
}
missingServerJSON, err := json.Marshal(reqBody)
if err != nil {
t.Fatalf("marshal missing-serverName canonical request: %v", err)
}
missingServerReq := httptest.NewRequest(http.MethodPost, "/api/auto-register", bytes.NewReader(missingServerJSON))
missingServerRec := httptest.NewRecorder()
handler.handleCanonicalAutoRegister(missingServerRec, missingServerReq, &reqBody, "127.0.0.1")
if missingServerRec.Code != http.StatusBadRequest {
t.Fatalf("missing-serverName status = %d, want 400", missingServerRec.Code)
}
if got := missingServerRec.Body.String(); got != "Missing required canonical auto-register fields: serverName\n" {
t.Fatalf("missing-serverName body = %q, want canonical missing-field guidance", got)
}
reqBody.ServerName = "pve-node-1"
reqBody.TokenValue = ""
mismatchedReq := httptest.NewRequest(http.MethodPost, "/api/auto-register", nil)
mismatchedRec := httptest.NewRecorder()
handler.handleCanonicalAutoRegister(mismatchedRec, mismatchedReq, &reqBody, "127.0.0.1")
if mismatchedRec.Code != http.StatusBadRequest {
t.Fatalf("mismatched-completion status = %d, want 400", mismatchedRec.Code)
}
if got := mismatchedRec.Body.String(); got != "tokenId and tokenValue must be provided together\n" {
t.Fatalf("mismatched-completion body = %q, want canonical token-pair guidance", got)
}
}
func TestContract_AutoRegisterResponseJSONSnapshot(t *testing.T) {
payload := map[string]any{
"status": "success",
"message": "Node pve-node-1 registered successfully at https://pve.local:8006",
"action": "use_token",
"type": "pve",
"source": "script",
"host": "https://pve.local:8006",
"nodeId": "pve-node-1",
"nodeName": "pve-node-1",
"tokenId": "pulse-monitor@pve!pulse-homelab",
"tokenValue": "secret-token",
}
got, err := json.Marshal(payload)
if err != nil {
t.Fatalf("marshal auto-register response: %v", err)
}
const want = `{
"action":"use_token",
"host":"https://pve.local:8006",
"message":"Node pve-node-1 registered successfully at https://pve.local:8006",
"nodeId":"pve-node-1",
"nodeName":"pve-node-1",
"source":"script",
"status":"success",
"tokenId":"pulse-monitor@pve!pulse-homelab",
"tokenValue":"secret-token",
"type":"pve"
}`
assertJSONSnapshot(t, got, want)
}
func TestContract_AutoUnregisterResponseJSONSnapshot(t *testing.T) {
payload := map[string]any{
"status": "success",
"message": "Node pve-node-1 removed successfully from https://pve.local:8006",
"action": "remove_connection",
"type": "pve",
"source": "script",
"host": "https://pve.local:8006",
"nodeId": "pve-node-1",
"nodeName": "pve-node-1",
"removed": true,
}
got, err := json.Marshal(payload)
if err != nil {
t.Fatalf("marshal auto-unregister response: %v", err)
}
const want = `{
"action":"remove_connection",
"host":"https://pve.local:8006",
"message":"Node pve-node-1 removed successfully from https://pve.local:8006",
"nodeId":"pve-node-1",
"nodeName":"pve-node-1",
"removed":true,
"source":"script",
"status":"success",
"type":"pve"
}`
assertJSONSnapshot(t, got, want)
}
func TestContract_AutoUnregisterNoopResponseJSONSnapshot(t *testing.T) {
payload := map[string]any{
"status": "success",
"message": "No matching node is currently configured for https://pve.local:8006",
"action": "noop",
"type": "pve",
"source": "script",
"host": "https://pve.local:8006",
"nodeId": "pve-node-1",
"nodeName": "pve-node-1",
"removed": false,
}
got, err := json.Marshal(payload)
if err != nil {
t.Fatalf("marshal auto-unregister noop response: %v", err)
}
const want = `{
"action":"noop",
"host":"https://pve.local:8006",
"message":"No matching node is currently configured for https://pve.local:8006",
"nodeId":"pve-node-1",
"nodeName":"pve-node-1",
"removed":false,
"source":"script",
"status":"success",
"type":"pve"
}`
assertJSONSnapshot(t, got, want)
}
func TestContract_AutoRegisterWebSocketEventJSONSnapshot(t *testing.T) {
payload := map[string]any{
"type": "pve",
"source": "script",
"host": "https://pve.local:8006",
"name": "pve-node-1",
"nodeId": "pve-node-1",
"nodeName": "pve-node-1",
"tokenId": "pulse-monitor@pve!pulse-homelab",
"hasToken": true,
"verifySSL": true,
"status": "connected",
}
got, err := json.Marshal(payload)
if err != nil {
t.Fatalf("marshal auto-register websocket event: %v", err)
}
const want = `{
"hasToken":true,
"host":"https://pve.local:8006",
"name":"pve-node-1",
"nodeId":"pve-node-1",
"nodeName":"pve-node-1",
"source":"script",
"status":"connected",
"tokenId":"pulse-monitor@pve!pulse-homelab",
"type":"pve",
"verifySSL":true
}`
assertJSONSnapshot(t, got, want)
}
func TestContract_CanonicalAutoRegisterEventJSONSnapshot(t *testing.T) {
payload := map[string]any{
"type": "pbs",
"source": "agent",
"host": "https://pbs.local:8007",
"name": "backup-node (2)",
"nodeId": "backup-node (2)",
"nodeName": "backup-node (2)",
"tokenId": "pulse-monitor@pbs!pulse-backup",
"hasToken": true,
"verifySSL": true,
"status": "connected",
}
got, err := json.Marshal(payload)
if err != nil {
t.Fatalf("marshal canonical /api/auto-register websocket event: %v", err)
}
const want = `{
"hasToken":true,
"host":"https://pbs.local:8007",
"name":"backup-node (2)",
"nodeId":"backup-node (2)",
"nodeName":"backup-node (2)",
"source":"agent",
"status":"connected",
"tokenId":"pulse-monitor@pbs!pulse-backup",
"type":"pbs",
"verifySSL":true
}`
assertJSONSnapshot(t, got, want)
}
func TestContract_CanonicalAutoRegisterReusedTokenResponseJSONSnapshot(t *testing.T) {
payload := map[string]any{
"status": "success",
"message": "Node pve-node-1 registered successfully at https://pve.local:8006",
"action": "use_token",
"type": "pve",
"source": "script",
"host": "https://pve.local:8006",
"nodeId": "pve-node-1",
"nodeName": "pve-node-1",
"tokenId": "pulse-monitor@pve!pulse-existing-node",
"tokenValue": "existing-token",
}
got, err := json.Marshal(payload)
if err != nil {
t.Fatalf("marshal canonical /api/auto-register response: %v", err)
}
const want = `{
"action":"use_token",
"host":"https://pve.local:8006",
"message":"Node pve-node-1 registered successfully at https://pve.local:8006",
"nodeId":"pve-node-1",
"nodeName":"pve-node-1",
"source":"script",
"status":"success",
"tokenId":"pulse-monitor@pve!pulse-existing-node",
"tokenValue":"existing-token",
"type":"pve"
}`
assertJSONSnapshot(t, got, want)
}
func TestContract_CanonicalAutoRegisterCallerProvidedTokenResponseJSONSnapshot(t *testing.T) {
payload := map[string]any{
"status": "success",
"message": "Node pve-node-1 registered successfully at https://pve.local:8006",
"action": "use_token",
"type": "pve",
"source": "agent",
"host": "https://pve.local:8006",
"nodeId": "pve-node-1",
"nodeName": "pve-node-1",
"tokenId": "pulse-monitor@pve!pulse-server",
"tokenValue": "created-locally",
}
got, err := json.Marshal(payload)
if err != nil {
t.Fatalf("marshal canonical /api/auto-register caller-provided token response: %v", err)
}
const want = `{
"action":"use_token",
"host":"https://pve.local:8006",
"message":"Node pve-node-1 registered successfully at https://pve.local:8006",
"nodeId":"pve-node-1",
"nodeName":"pve-node-1",
"source":"agent",
"status":"success",
"tokenId":"pulse-monitor@pve!pulse-server",
"tokenValue":"created-locally",
"type":"pve"
}`
assertJSONSnapshot(t, got, want)
}
func TestContract_CanonicalAutoRegisterRotatedTokenResponseJSONSnapshot(t *testing.T) {
payload := map[string]any{
"status": "success",
"message": "Node pve-node-1 registered successfully at https://pve.local:8006",
"action": "use_token",
"type": "pve",
"source": "agent",
"host": "https://pve.local:8006",
"nodeId": "pve-node-1",
"nodeName": "pve-node-1",
"tokenId": "pulse-monitor@pve!pulse-existing-node",
"tokenValue": "rotated-token",
}
got, err := json.Marshal(payload)
if err != nil {
t.Fatalf("marshal canonical /api/auto-register rotated token response: %v", err)
}
const want = `{
"action":"use_token",
"host":"https://pve.local:8006",
"message":"Node pve-node-1 registered successfully at https://pve.local:8006",
"nodeId":"pve-node-1",
"nodeName":"pve-node-1",
"source":"agent",
"status":"success",
"tokenId":"pulse-monitor@pve!pulse-existing-node",
"tokenValue":"rotated-token",
"type":"pve"
}`
assertJSONSnapshot(t, got, want)
}
func TestContract_CanonicalAutoRegisterResponseUsesCanonicalStoredNodeIdentity(t *testing.T) {
payload := map[string]any{
"status": "success",
"message": "Node pve-node-1 (2) registered successfully at https://pve.local:8006",
"action": "use_token",
"type": "pve",
"source": "agent",
"host": "https://pve.local:8006",
"nodeId": "pve-node-1 (2)",
"nodeName": "pve-node-1 (2)",
"tokenId": "pulse-monitor@pve!pulse-existing-node",
"tokenValue": "existing-token",
}
got, err := json.Marshal(payload)
if err != nil {
t.Fatalf("marshal canonical /api/auto-register disambiguated response: %v", err)
}
const want = `{
"action":"use_token",
"host":"https://pve.local:8006",
"message":"Node pve-node-1 (2) registered successfully at https://pve.local:8006",
"nodeId":"pve-node-1 (2)",
"nodeName":"pve-node-1 (2)",
"source":"agent",
"status":"success",
"tokenId":"pulse-monitor@pve!pulse-existing-node",
"tokenValue":"existing-token",
"type":"pve"
}`
assertJSONSnapshot(t, got, want)
}
func TestContract_MetricsHistoryLiveFallbackJSONSnapshot(t *testing.T) {
state := models.NewState()
state.UpdateVMsForInstance("pve1", []models.VM{{
ID: "pve1:node1:101",
VMID: 101,
Name: "vm-101",
Node: "node1",
Instance: "pve1",
Status: "running",
Type: "qemu",
CPU: 0.42,
Memory: models.Memory{
Usage: 55.0,
},
Disk: models.Disk{
Usage: 33.0,
},
}})
monitor := &monitoring.Monitor{}
setUnexportedField(t, monitor, "state", state)
setUnexportedField(t, monitor, "metricsHistory", monitoring.NewMetricsHistory(10, time.Hour))
tempDir := t.TempDir()
mtp := config.NewMultiTenantPersistence(tempDir)
if _, err := mtp.GetPersistence("default"); err != nil {
t.Fatalf("failed to init persistence: %v", err)
}
router := &Router{
monitor: monitor,
licenseHandlers: NewLicenseHandlers(mtp, false),
}
req := httptest.NewRequest(
http.MethodGet,
"/api/metrics-store/history?resourceType=vm&resourceId=pve1:node1:101&metric=cpu&start=2026-03-11T00:00:00Z&end=2026-03-12T00:00:00Z",
nil,
)
rec := httptest.NewRecorder()
router.handleMetricsHistory(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status = %d, body=%s", rec.Code, rec.Body.String())
}
var payload map[string]any
if err := json.Unmarshal(rec.Body.Bytes(), &payload); err != nil {
t.Fatalf("unmarshal metrics history response: %v", err)
}
points, ok := payload["points"].([]any)
if !ok || len(points) != 1 {
t.Fatalf("unexpected points payload: %#v", payload["points"])
}
point, ok := points[0].(map[string]any)
if !ok {
t.Fatalf("unexpected point payload: %#v", points[0])
}
point["timestamp"] = float64(1700000000000)
payload["range"] = "24h"
payload["start"] = float64(1741651200000)
payload["end"] = float64(1741737600000)
got, err := json.Marshal(payload)
if err != nil {
t.Fatalf("marshal normalized metrics history response: %v", err)
}
const want = `{
"end":1741737600000,
"metric":"cpu",
"points":[
{
"max":42,
"min":42,
"timestamp":1700000000000,
"value":42
}
],
"range":"24h",
"resourceId":"pve1:node1:101",
"resourceType":"vm",
"source":"live",
"start":1741651200000
}`
assertJSONSnapshot(t, got, want)
}
func TestContract_MetricsHistoryAgentTemperatureLiveFallback(t *testing.T) {
state := models.NewState()
state.UpsertHost(models.Host{
ID: "agent-pve-node-1",
Hostname: "pve-node-1",
Status: "online",
Sensors: models.HostSensorSummary{
TemperatureCelsius: map[string]float64{
"cpu_package": 62.5,
},
},
})
monitor := &monitoring.Monitor{}
setUnexportedField(t, monitor, "state", state)
setUnexportedField(t, monitor, "metricsHistory", monitoring.NewMetricsHistory(10, time.Hour))
router := &Router{monitor: monitor}
req := httptest.NewRequest(
http.MethodGet,
"/api/metrics-store/history?resourceType=agent&resourceId=agent-pve-node-1&metric=temperature&range=5m",
nil,
)
rec := httptest.NewRecorder()
router.handleMetricsHistory(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status = %d, body=%s", rec.Code, rec.Body.String())
}
var resp metricsHistoryResponse
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
t.Fatalf("failed to unmarshal response: %v", err)
}
if resp.ResourceType != "agent" {
t.Fatalf("expected resourceType agent, got %q", resp.ResourceType)
}
if resp.ResourceId != "agent-pve-node-1" {
t.Fatalf("expected resource id agent-pve-node-1, got %q", resp.ResourceId)
}
if resp.Metric != "temperature" {
t.Fatalf("expected metric temperature, got %q", resp.Metric)
}
if resp.Source != "live" {
t.Fatalf("expected source live, got %q", resp.Source)
}
if len(resp.Points) != 1 || resp.Points[0].Value != 62.5 {
t.Fatalf("expected one live temperature point 62.5, got %+v", resp.Points)
}
}
func TestContract_MetricsHistoryCanonicalizesLegacyKubernetesPodIDs(t *testing.T) {
mh := monitoring.NewMetricsHistory(1000, time.Hour)
now := time.Now().UTC().Truncate(time.Second)
legacyID := "cluster-1:pod:pod-1"
canonicalID := "k8s:" + legacyID
for i, value := range []float64{21, 34, 29} {
mh.AddGuestMetric(canonicalID, "cpu", value, now.Add(time.Duration(i-2)*10*time.Minute))
}
router := &Router{
monitor: &monitoring.Monitor{},
}
setUnexportedField(t, router.monitor, "metricsHistory", mh)
req := httptest.NewRequest(
http.MethodGet,
"/api/metrics-store/history?resourceType=k8s&resourceId="+legacyID+"&metric=cpu&range=30m",
nil,
)
rec := httptest.NewRecorder()
router.handleMetricsHistory(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status = %d, body=%s", rec.Code, rec.Body.String())
}
var resp metricsHistoryResponse
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
t.Fatalf("failed to unmarshal response: %v", err)
}
if resp.ResourceType != "k8s" {
t.Fatalf("expected resourceType k8s, got %q", resp.ResourceType)
}
if resp.ResourceId != canonicalID {
t.Fatalf("expected canonical pod resource id %q, got %q", canonicalID, resp.ResourceId)
}
if resp.Metric != "cpu" {
t.Fatalf("expected metric cpu, got %q", resp.Metric)
}
if resp.Range != "30m" {
t.Fatalf("expected range 30m, got %q", resp.Range)
}
if resp.Source != "memory" {
t.Fatalf("expected source memory, got %q", resp.Source)
}
if len(resp.Points) != 3 {
t.Fatalf("expected 3 cpu points, got %d", len(resp.Points))
}
}
func TestContract_MetricsHistoryPhysicalDiskIOLiveWindowUsesCanonicalDiskTarget(t *testing.T) {
mh := monitoring.NewMetricsHistory(1000, time.Hour)
now := time.Now().UTC().Truncate(time.Second)
resourceID := "SERIAL884006359727"
for i, value := range []float64{1.5, 2.25, 3.0} {
mh.AddDiskMetric(resourceID, "diskread", value*1024*1024, now.Add(time.Duration(i-2)*10*time.Minute))
}
router := &Router{
monitor: &monitoring.Monitor{},
}
setUnexportedField(t, router.monitor, "metricsHistory", mh)
req := httptest.NewRequest(
http.MethodGet,
"/api/metrics-store/history?resourceType=disk&resourceId="+resourceID+"&metric=diskread&range=30m",
nil,
)
rec := httptest.NewRecorder()
router.handleMetricsHistory(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status = %d, body=%s", rec.Code, rec.Body.String())
}
var resp metricsHistoryResponse
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
t.Fatalf("failed to unmarshal response: %v", err)
}
if resp.ResourceType != "disk" {
t.Fatalf("expected resourceType disk, got %q", resp.ResourceType)
}
if resp.ResourceId != resourceID {
t.Fatalf("expected canonical disk resource id %q, got %q", resourceID, resp.ResourceId)
}
if resp.Metric != "diskread" {
t.Fatalf("expected metric diskread, got %q", resp.Metric)
}
if resp.Range != "30m" {
t.Fatalf("expected range 30m, got %q", resp.Range)
}
if resp.Source != "memory" {
t.Fatalf("expected source memory, got %q", resp.Source)
}
if len(resp.Points) != 3 {
t.Fatalf("expected 3 diskread points, got %d", len(resp.Points))
}
}
func TestContract_PatrolStatusResponseJSONSnapshot(t *testing.T) {
lastPatrolAt := time.Date(2026, 3, 12, 9, 30, 0, 0, time.UTC)
lastActivityAt := lastPatrolAt.Add(5 * time.Minute)
nextPatrolAt := lastPatrolAt.Add(6 * time.Hour)
blockedAt := lastPatrolAt.Add(15 * time.Minute)
payload := PatrolStatusResponse{
RuntimeState: ai.PatrolRuntimeStateBlocked,
Running: false,
Enabled: true,
LastPatrolAt: &lastPatrolAt,
LastActivityAt: &lastActivityAt,
TriggerStatus: &ai.TriggerStatus{
Running: true,
PendingTriggers: 3,
CurrentInterval: 300000,
RecentEvents: 6,
IsBusyMode: true,
AlertTriggersEnabled: true,
AnomalyTriggersEnabled: false,
},
NextPatrolAt: &nextPatrolAt,
LastDurationMs: 12345,
ResourcesChecked: 18,
FindingsCount: 3,
ErrorCount: 1,
Healthy: false,
IntervalMs: 21600000,
FixedCount: 2,
BlockedReason: "Awaiting AI provider configuration",
BlockedCause: string(ai.PatrolFailureCauseProviderNotConfigured),
BlockedAt: &blockedAt,
LicenseRequired: true,
LicenseStatus: "none",
UpgradeURL: "https://pulserelay.pro/upgrade?feature=ai_autofix",
Readiness: &PatrolReadinessResponse{
Status: patrolReadinessNotReady,
Ready: false,
Cause: string(ai.PatrolFailureCauseModelUnsupportedTools),
Summary: "The selected Patrol model is a reasoning-only model family that commonly does not emit tool calls.",
Provider: "ollama",
Model: "ollama:deepseek-r1:7b-llama-distill-q4_K_M",
Checks: []PatrolReadinessCheck{
{
ID: "tools",
Status: patrolReadinessNotReady,
Cause: string(ai.PatrolFailureCauseModelUnsupportedTools),
Label: "Patrol tools",
Message: "The selected Patrol model is a reasoning-only model family that commonly does not emit tool calls.",
Action: "open_provider_settings",
},
},
},
}
payload.Summary.Critical = 1
payload.Summary.Warning = 2
payload.Summary.Watch = 0
payload.Summary.Info = 4
got, err := json.Marshal(payload)
if err != nil {
t.Fatalf("marshal patrol status response: %v", err)
}
const want = `{
"runtime_state":"blocked",
"running":false,
"enabled":true,
"last_patrol_at":"2026-03-12T09:30:00Z",
"last_activity_at":"2026-03-12T09:35:00Z",
"trigger_status":{"running":true,"pending_triggers":3,"current_interval_ms":300000,"recent_events":6,"is_busy_mode":true,"alert_triggers_enabled":true,"anomaly_triggers_enabled":false},
"next_patrol_at":"2026-03-12T15:30:00Z",
"last_duration_ms":12345,
"resources_checked":18,
"findings_count":3,
"error_count":1,
"healthy":false,
"interval_ms":21600000,
"fixed_count":2,
"blocked_reason":"Awaiting AI provider configuration",
"blocked_cause":"provider_not_configured",
"blocked_at":"2026-03-12T09:45:00Z",
"license_required":true,
"license_status":"none",
"upgrade_url":"https://pulserelay.pro/upgrade?feature=ai_autofix",
"summary":{"critical":1,"warning":2,"watch":0,"info":4},
"readiness":{"status":"not_ready","ready":false,"cause":"model_unsupported_tools","summary":"The selected Patrol model is a reasoning-only model family that commonly does not emit tool calls.","provider":"ollama","model":"ollama:deepseek-r1:7b-llama-distill-q4_K_M","checks":[{"id":"tools","status":"not_ready","cause":"model_unsupported_tools","label":"Patrol tools","message":"The selected Patrol model is a reasoning-only model family that commonly does not emit tool calls.","action":"open_provider_settings"}]}
}`
assertJSONSnapshot(t, got, want)
}
func TestContract_PatrolRunRecordJSONSnapshot(t *testing.T) {
startedAt := time.Date(2026, 3, 12, 10, 0, 0, 0, time.UTC)
completedAt := startedAt.Add(90 * time.Second)
payload := ai.PatrolRunRecord{
ID: "run-1",
StartedAt: startedAt,
CompletedAt: completedAt,
DurationMs: 90000,
Type: "scoped",
TriggerReason: "alert_fired",
ScopeResourceIDs: []string{"seed-resource"},
EffectiveScopeResourceIDs: []string{},
ScopeResourceTypes: []string{"vm"},
ResourcesChecked: 4,
NodesChecked: 0,
GuestsChecked: 2,
DockerChecked: 0,
StorageChecked: 0,
HostsChecked: 0,
TrueNASChecked: 1,
PBSChecked: 0,
PMGChecked: 1,
KubernetesChecked: 1,
NewFindings: 0,
ExistingFindings: 2,
RejectedFindings: 1,
ResolvedFindings: 1,
AutoFixCount: 0,
FindingsSummary: "All clear",
FindingIDs: []string{},
ErrorCount: 0,
Status: "healthy",
TriageFlags: 3,
TriageSkippedLLM: true,
ToolCallCount: 0,
}
got, err := json.Marshal(payload)
if err != nil {
t.Fatalf("marshal patrol run record: %v", err)
}
const want = `{
"id":"run-1",
"started_at":"2026-03-12T10:00:00Z",
"completed_at":"2026-03-12T10:01:30Z",
"duration_ms":90000,
"type":"scoped",
"trigger_reason":"alert_fired",
"scope_resource_ids":["seed-resource"],
"effective_scope_resource_ids":[],
"scope_resource_types":["vm"],
"resources_checked":4,
"nodes_checked":0,
"guests_checked":2,
"docker_checked":0,
"storage_checked":0,
"hosts_checked":0,
"truenas_checked":1,
"pbs_checked":0,
"pmg_checked":1,
"kubernetes_checked":1,
"new_findings":0,
"existing_findings":2,
"rejected_findings":1,
"resolved_findings":1,
"findings_summary":"All clear",
"finding_ids":[],
"error_count":0,
"status":"healthy",
"triage_flags":3,
"triage_skipped_llm":true,
"tool_call_count":0
}`
assertJSONSnapshot(t, got, want)
}
func TestContract_ChatStreamEventJSONSnapshots(t *testing.T) {
cases := []struct {
name string
event chat.StreamEvent
want string
}{
{
name: "content",
event: mustStreamEvent(t, "content", chat.ContentData{
Text: "hello",
}),
want: `{"type":"content","data":{"text":"hello"}}`,
},
{
name: "workflow_state",
event: mustStreamEvent(t, "workflow_state", chat.WorkflowStateData{
Phase: "plan",
Message: "Planning governed action and safety checks before execution.",
State: "READING",
Tool: "pulse_exec",
}),
want: `{"type":"workflow_state","data":{"phase":"plan","message":"Planning governed action and safety checks before execution.","state":"READING","tool":"pulse_exec"}}`,
},
{
name: "tool_start",
event: mustStreamEvent(t, "tool_start", chat.ToolStartData{
ID: "tool-1",
Name: "pulse_read",
Input: `{"path":"/tmp/x.log"}`,
RawInput: `{"path":"/tmp/x.log"}`,
}),
want: `{"type":"tool_start","data":{"id":"tool-1","name":"pulse_read","input":"{\"path\":\"/tmp/x.log\"}","raw_input":"{\"path\":\"/tmp/x.log\"}"}}`,
},
{
name: "tool_end",
event: mustStreamEvent(t, "tool_end", chat.ToolEndData{
ID: "tool-1",
Name: "pulse_read",
Input: `{"path":"/tmp/x.log"}`,
RawInput: `{"path":"/tmp/x.log"}`,
Output: "ok",
Success: true,
}),
want: `{"type":"tool_end","data":{"id":"tool-1","name":"pulse_read","input":"{\"path\":\"/tmp/x.log\"}","raw_input":"{\"path\":\"/tmp/x.log\"}","output":"ok","success":true}}`,
},
{
name: "approval_needed",
event: mustStreamEvent(t, "approval_needed", chat.ApprovalNeededData{
ApprovalID: "approval-1",
ToolID: "tool-2",
ToolName: "pulse_exec",
Command: "systemctl restart nginx",
RunOnHost: true,
TargetHost: "node-1",
TargetType: "agent",
TargetID: "agent-1",
Risk: "high",
Description: "Restart web service",
AuditID: "action-1",
Plan: &chat.ApprovalPlanData{
ActionID: "action-1",
RequestID: "approval-1",
Summary: "Restart web service",
RequiresApproval: true,
ApprovalPolicy: "admin",
BlastRadius: "service interruption on target",
RollbackAvailable: true,
PlanHash: "hash-1",
ExpiresAt: "2026-04-23T12:30:00Z",
},
ContextConfidence: &chat.ApprovalContextConfidenceData{
Level: "verified",
Summary: "Target was resolved to a concrete resource before approval.",
Evidence: []string{"Target identifier bound to agent-1."},
},
Preflight: &chat.ApprovalPreflightData{
Target: "agent:node-1 (agent-1)",
CurrentState: "Resolved approval target: agent:node-1 (agent-1).",
IntendedChange: "Restart web service",
DryRunAvailable: false,
DryRunSummary: "No provider-supported dry run is available for this action.",
SafetyChecks: []string{"Approval is scoped to this organization."},
VerificationSteps: []string{"Read back the target state after execution."},
GeneratedAt: "2026-04-23T12:29:00Z",
},
}),
want: `{"type":"approval_needed","data":{"approval_id":"approval-1","tool_id":"tool-2","tool_name":"pulse_exec","command":"systemctl restart nginx","run_on_host":true,"target_host":"node-1","target_type":"agent","target_id":"agent-1","risk":"high","description":"Restart web service","audit_id":"action-1","plan":{"action_id":"action-1","request_id":"approval-1","summary":"Restart web service","requires_approval":true,"approval_policy":"admin","blast_radius":"service interruption on target","rollback_available":true,"plan_hash":"hash-1","expires_at":"2026-04-23T12:30:00Z"},"context_confidence":{"level":"verified","summary":"Target was resolved to a concrete resource before approval.","evidence":["Target identifier bound to agent-1."]},"preflight":{"target":"agent:node-1 (agent-1)","current_state":"Resolved approval target: agent:node-1 (agent-1).","intended_change":"Restart web service","dry_run_available":false,"dry_run_summary":"No provider-supported dry run is available for this action.","safety_checks":["Approval is scoped to this organization."],"verification_steps":["Read back the target state after execution."],"generated_at":"2026-04-23T12:29:00Z"}}}`,
},
{
name: "question",
event: mustStreamEvent(t, "question", chat.QuestionData{
SessionID: "session-1",
QuestionID: "question-1",
Questions: []chat.Question{
{
ID: "target",
Type: "select",
Question: "Which node should I inspect?",
Header: "Target",
Options: []chat.QuestionOption{
{Label: "Node A", Value: "node-a", Description: "Primary compute node"},
{Label: "Node B", Value: "node-b", Description: "Replica node"},
},
},
},
}),
want: `{"type":"question","data":{"session_id":"session-1","question_id":"question-1","questions":[{"id":"target","type":"select","question":"Which node should I inspect?","header":"Target","options":[{"label":"Node A","value":"node-a","description":"Primary compute node"},{"label":"Node B","value":"node-b","description":"Replica node"}]}]}}`,
},
{
name: "done",
event: mustStreamEvent(t, "done", chat.DoneData{
SessionID: "session-1",
InputTokens: 120,
OutputTokens: 80,
}),
want: `{"type":"done","data":{"session_id":"session-1","input_tokens":120,"output_tokens":80}}`,
},
{
name: "error",
event: mustStreamEvent(t, "error", chat.ErrorData{
Message: "request failed",
}),
want: `{"type":"error","data":{"message":"request failed"}}`,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got, err := json.Marshal(tc.event)
if err != nil {
t.Fatalf("marshal stream event: %v", err)
}
assertJSONSnapshot(t, got, tc.want)
})
}
}
func TestContract_PushNotificationJSONSnapshots(t *testing.T) {
cases := []struct {
name string
payload relay.PushNotificationPayload
want string
}{
{
name: "patrol_finding",
payload: relay.NewPatrolFindingNotification("finding-1", "warning", "capacity", "Disk pressure detected"),
want: `{"type":"patrol_finding","priority":"normal","title":"Disk pressure detected","body":"New warning capacity finding detected","action_type":"view_finding","action_id":"finding-1","category":"capacity","severity":"warning"}`,
},
{
name: "patrol_critical",
payload: relay.NewPatrolFindingNotification("finding-2", "critical", "performance", "CPU saturation detected"),
want: `{"type":"patrol_critical","priority":"high","title":"CPU saturation detected","body":"New critical performance finding detected","action_type":"view_finding","action_id":"finding-2","category":"performance","severity":"critical"}`,
},
{
name: "approval_request",
payload: relay.NewApprovalRequestNotification("approval-1", "Fix queued", "high"),
want: `{"type":"approval_request","priority":"high","title":"Fix queued","body":"A high-risk fix requires your approval","action_type":"approve_fix","action_id":"approval-1"}`,
},
{
name: "fix_completed_success",
payload: relay.NewFixCompletedNotification("finding-3", "CPU saturation detected", true),
want: `{"type":"fix_completed","priority":"normal","title":"CPU saturation detected","body":"Fix applied successfully","action_type":"view_fix_result","action_id":"finding-3"}`,
},
{
name: "fix_completed_failed",
payload: relay.NewFixCompletedNotification("finding-4", "Disk pressure detected", false),
want: `{"type":"fix_completed","priority":"normal","title":"Disk pressure detected","body":"Fix attempt failed — review needed","action_type":"view_fix_result","action_id":"finding-4"}`,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got, err := json.Marshal(tc.payload)
if err != nil {
t.Fatalf("marshal push payload: %v", err)
}
assertJSONSnapshot(t, got, tc.want)
})
}
}
func TestContract_AlertJSONSnapshot(t *testing.T) {
start := time.Date(2026, 2, 8, 13, 14, 15, 0, time.UTC)
lastSeen := start.Add(3 * time.Minute)
payload := alerts.Alert{
ID: "cluster/qemu/100-cpu",
Type: "cpu",
Level: alerts.AlertLevelWarning,
ResourceID: "cluster/qemu/100",
ResourceName: "test-vm",
Node: "pve-1",
Instance: "cpu0",
Message: "VM cpu at 95%",
Value: 95.0,
Threshold: 90.0,
StartTime: start,
LastSeen: lastSeen,
Acknowledged: false,
Metadata: map[string]interface{}{
"resourceType": "VM",
"clearThreshold": 70.0,
"unit": "%",
"monitorOnly": true,
},
}
got, err := json.Marshal(payload)
if err != nil {
t.Fatalf("marshal alert: %v", err)
}
const want = `{
"id":"cluster/qemu/100-cpu",
"type":"cpu",
"level":"warning",
"resourceId":"cluster/qemu/100",
"resourceName":"test-vm",
"node":"pve-1",
"instance":"cpu0",
"message":"VM cpu at 95%",
"value":95,
"threshold":90,
"startTime":"2026-02-08T13:14:15Z",
"lastSeen":"2026-02-08T13:17:15Z",
"acknowledged":false,
"metadata":{"clearThreshold":70,"monitorOnly":true,"resourceType":"VM","unit":"%"}
}`
assertJSONSnapshot(t, got, want)
}
func TestContract_AlertAllFieldsJSONSnapshot(t *testing.T) {
start := time.Date(2026, 2, 8, 13, 14, 15, 0, time.UTC)
lastSeen := start.Add(3 * time.Minute)
ackTime := start.Add(5 * time.Minute)
lastNotified := start.Add(2 * time.Minute)
escalationTimes := []time.Time{start.Add(1 * time.Minute), start.Add(3 * time.Minute)}
payload := alerts.Alert{
ID: "cluster/qemu/100-cpu",
Type: "cpu",
Level: alerts.AlertLevelWarning,
ResourceID: "cluster/qemu/100",
ResourceName: "test-vm",
Node: "pve-1",
NodeDisplayName: "Proxmox Node 1",
Instance: "cpu0",
Message: "VM cpu at 95%",
Value: 95.0,
Threshold: 90.0,
StartTime: start,
LastSeen: lastSeen,
Acknowledged: true,
AckTime: &ackTime,
AckUser: "admin",
Metadata: map[string]interface{}{
"resourceType": "VM",
"clearThreshold": 70.0,
"unit": "%",
"monitorOnly": true,
},
LastNotified: &lastNotified,
LastEscalation: 2,
EscalationTimes: escalationTimes,
}
got, err := json.Marshal(payload)
if err != nil {
t.Fatalf("marshal alert with all fields: %v", err)
}
const want = `{
"id":"cluster/qemu/100-cpu",
"type":"cpu",
"level":"warning",
"resourceId":"cluster/qemu/100",
"resourceName":"test-vm",
"node":"pve-1",
"nodeDisplayName":"Proxmox Node 1",
"instance":"cpu0",
"message":"VM cpu at 95%",
"value":95,
"threshold":90,
"startTime":"2026-02-08T13:14:15Z",
"lastSeen":"2026-02-08T13:17:15Z",
"acknowledged":true,
"ackTime":"2026-02-08T13:19:15Z",
"ackUser":"admin",
"metadata":{"clearThreshold":70,"monitorOnly":true,"resourceType":"VM","unit":"%"},
"lastNotified":"2026-02-08T13:16:15Z",
"lastEscalation":2,
"escalationTimes":["2026-02-08T13:15:15Z","2026-02-08T13:17:15Z"]
}`
assertJSONSnapshot(t, got, want)
}
func TestContract_ModelAlertJSONSnapshot(t *testing.T) {
start := time.Date(2026, 2, 8, 13, 14, 15, 0, time.UTC)
ackTime := start.Add(5 * time.Minute)
resolvedTime := start.Add(10 * time.Minute)
t.Run("alert", func(t *testing.T) {
payload := models.Alert{
ID: "cluster/qemu/100-cpu",
Type: "cpu",
Level: "warning",
ResourceID: "cluster/qemu/100",
ResourceName: "test-vm",
Node: "pve-1",
NodeDisplayName: "Proxmox Node 1",
Instance: "cpu0",
Message: "VM cpu at 95%",
Value: 95.0,
Threshold: 90.0,
StartTime: start,
Acknowledged: true,
AckTime: &ackTime,
AckUser: "admin",
}
got, err := json.Marshal(payload)
if err != nil {
t.Fatalf("marshal model alert: %v", err)
}
forbidden := []string{`"lastSeen"`, `"metadata"`, `"lastNotified"`, `"lastEscalation"`, `"escalationTimes"`}
for _, field := range forbidden {
if strings.Contains(string(got), field) {
t.Fatalf("model alert json unexpectedly contains %s: %s", field, string(got))
}
}
const want = `{
"id":"cluster/qemu/100-cpu",
"type":"cpu",
"level":"warning",
"resourceId":"cluster/qemu/100",
"resourceName":"test-vm",
"node":"pve-1",
"nodeDisplayName":"Proxmox Node 1",
"instance":"cpu0",
"message":"VM cpu at 95%",
"value":95,
"threshold":90,
"startTime":"2026-02-08T13:14:15Z",
"acknowledged":true,
"ackTime":"2026-02-08T13:19:15Z",
"ackUser":"admin"
}`
assertJSONSnapshot(t, got, want)
})
t.Run("resolved_alert", func(t *testing.T) {
payload := models.ResolvedAlert{
Alert: models.Alert{
ID: "cluster/qemu/100-cpu",
Type: "cpu",
Level: "warning",
ResourceID: "cluster/qemu/100",
ResourceName: "test-vm",
Node: "pve-1",
NodeDisplayName: "Proxmox Node 1",
Instance: "cpu0",
Message: "VM cpu at 95%",
Value: 95.0,
Threshold: 90.0,
StartTime: start,
Acknowledged: true,
AckTime: &ackTime,
AckUser: "admin",
},
ResolvedTime: resolvedTime,
}
got, err := json.Marshal(payload)
if err != nil {
t.Fatalf("marshal model resolved alert: %v", err)
}
forbidden := []string{`"lastSeen"`, `"metadata"`, `"lastNotified"`, `"lastEscalation"`, `"escalationTimes"`}
for _, field := range forbidden {
if strings.Contains(string(got), field) {
t.Fatalf("model resolved alert json unexpectedly contains %s: %s", field, string(got))
}
}
const want = `{
"id":"cluster/qemu/100-cpu",
"type":"cpu",
"level":"warning",
"resourceId":"cluster/qemu/100",
"resourceName":"test-vm",
"node":"pve-1",
"nodeDisplayName":"Proxmox Node 1",
"instance":"cpu0",
"message":"VM cpu at 95%",
"value":95,
"threshold":90,
"startTime":"2026-02-08T13:14:15Z",
"acknowledged":true,
"ackTime":"2026-02-08T13:19:15Z",
"ackUser":"admin",
"resolvedTime":"2026-02-08T13:24:15Z"
}`
assertJSONSnapshot(t, got, want)
})
}
func TestContract_IncidentJSONSnapshot(t *testing.T) {
start := time.Date(2026, 2, 8, 13, 14, 15, 0, time.UTC)
ackTime := start.Add(5 * time.Minute)
closedAt := start.Add(10 * time.Minute)
t.Run("open", func(t *testing.T) {
payload := memory.Incident{
ID: "incident-1",
AlertIdentifier: "cluster/qemu/100-cpu",
AlertType: "cpu",
Level: "warning",
ResourceID: "cluster/qemu/100",
ResourceName: "test-vm",
ResourceType: "guest",
Node: "pve-1",
Instance: "cpu0",
Message: "VM cpu at 95%",
Status: memory.IncidentStatusOpen,
OpenedAt: start,
Acknowledged: true,
AckUser: "admin",
AckTime: &ackTime,
Events: []memory.IncidentEvent{
{
ID: "evt-1",
Type: memory.IncidentEventAlertFired,
Timestamp: start.Add(1 * time.Minute),
Summary: "CPU alert fired",
Details: map[string]interface{}{
"type": "cpu",
"level": "warning",
"value": 95,
"threshold": 90,
},
},
{
ID: "evt-2",
Type: memory.IncidentEventAlertAcknowledged,
Timestamp: start.Add(5 * time.Minute),
Summary: "Alert acknowledged",
Details: map[string]interface{}{
"user": "admin",
},
},
},
}
got, err := json.Marshal(payload)
if err != nil {
t.Fatalf("marshal open incident: %v", err)
}
const want = `{
"id":"incident-1",
"alertIdentifier":"cluster/qemu/100-cpu",
"alertType":"cpu",
"level":"warning",
"resourceId":"cluster/qemu/100",
"resourceName":"test-vm",
"resourceType":"guest",
"node":"pve-1",
"instance":"cpu0",
"message":"VM cpu at 95%",
"status":"open",
"openedAt":"2026-02-08T13:14:15Z",
"acknowledged":true,
"ackUser":"admin",
"ackTime":"2026-02-08T13:19:15Z",
"events":[
{"id":"evt-1","type":"alert_fired","timestamp":"2026-02-08T13:15:15Z","summary":"CPU alert fired","details":{"level":"warning","threshold":90,"type":"cpu","value":95}},
{"id":"evt-2","type":"alert_acknowledged","timestamp":"2026-02-08T13:19:15Z","summary":"Alert acknowledged","details":{"user":"admin"}}
]
}`
assertJSONSnapshot(t, got, want)
})
t.Run("resolved", func(t *testing.T) {
payload := memory.Incident{
ID: "incident-1",
AlertIdentifier: "cluster/qemu/100-cpu",
AlertType: "cpu",
Level: "warning",
ResourceID: "cluster/qemu/100",
ResourceName: "test-vm",
ResourceType: "guest",
Node: "pve-1",
Instance: "cpu0",
Message: "VM cpu at 95%",
Status: memory.IncidentStatusResolved,
OpenedAt: start,
ClosedAt: &closedAt,
Acknowledged: true,
AckUser: "admin",
AckTime: &ackTime,
Events: []memory.IncidentEvent{
{
ID: "evt-1",
Type: memory.IncidentEventAlertFired,
Timestamp: start.Add(1 * time.Minute),
Summary: "CPU alert fired",
Details: map[string]interface{}{
"type": "cpu",
"level": "warning",
"value": 95,
"threshold": 90,
},
},
{
ID: "evt-2",
Type: memory.IncidentEventAlertAcknowledged,
Timestamp: start.Add(5 * time.Minute),
Summary: "Alert acknowledged",
Details: map[string]interface{}{
"user": "admin",
},
},
},
}
got, err := json.Marshal(payload)
if err != nil {
t.Fatalf("marshal resolved incident: %v", err)
}
const want = `{
"id":"incident-1",
"alertIdentifier":"cluster/qemu/100-cpu",
"alertType":"cpu",
"level":"warning",
"resourceId":"cluster/qemu/100",
"resourceName":"test-vm",
"resourceType":"guest",
"node":"pve-1",
"instance":"cpu0",
"message":"VM cpu at 95%",
"status":"resolved",
"openedAt":"2026-02-08T13:14:15Z",
"closedAt":"2026-02-08T13:24:15Z",
"acknowledged":true,
"ackUser":"admin",
"ackTime":"2026-02-08T13:19:15Z",
"events":[
{"id":"evt-1","type":"alert_fired","timestamp":"2026-02-08T13:15:15Z","summary":"CPU alert fired","details":{"level":"warning","threshold":90,"type":"cpu","value":95}},
{"id":"evt-2","type":"alert_acknowledged","timestamp":"2026-02-08T13:19:15Z","summary":"Alert acknowledged","details":{"user":"admin"}}
]
}`
assertJSONSnapshot(t, got, want)
})
}
func TestContract_IncidentEventTypeEnumSnapshot(t *testing.T) {
type envelope struct {
Type memory.IncidentEventType `json:"type"`
}
cases := []struct {
name string
typ memory.IncidentEventType
want string
}{
{name: "alert_fired", typ: memory.IncidentEventAlertFired, want: `{"type":"alert_fired"}`},
{name: "alert_acknowledged", typ: memory.IncidentEventAlertAcknowledged, want: `{"type":"alert_acknowledged"}`},
{name: "alert_unacknowledged", typ: memory.IncidentEventAlertUnacknowledged, want: `{"type":"alert_unacknowledged"}`},
{name: "alert_resolved", typ: memory.IncidentEventAlertResolved, want: `{"type":"alert_resolved"}`},
{name: "ai_analysis", typ: memory.IncidentEventAnalysis, want: `{"type":"ai_analysis"}`},
{name: "command", typ: memory.IncidentEventCommand, want: `{"type":"command"}`},
{name: "runbook", typ: memory.IncidentEventRunbook, want: `{"type":"runbook"}`},
{name: "note", typ: memory.IncidentEventNote, want: `{"type":"note"}`},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got, err := json.Marshal(envelope{Type: tc.typ})
if err != nil {
t.Fatalf("marshal incident event type %q: %v", tc.name, err)
}
assertJSONSnapshot(t, got, tc.want)
})
}
}
func TestContract_AlertFieldNamingConsistency(t *testing.T) {
cases := []struct {
name string
typ reflect.Type
}{
{name: "alerts.Alert", typ: reflect.TypeOf(alerts.Alert{})},
{name: "memory.Incident", typ: reflect.TypeOf(memory.Incident{})},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
for i := 0; i < tc.typ.NumField(); i++ {
field := tc.typ.Field(i)
if !field.IsExported() {
continue
}
jsonTag := field.Tag.Get("json")
if jsonTag == "" || jsonTag == "-" {
continue
}
tagName := strings.Split(jsonTag, ",")[0]
if strings.Contains(tagName, "_") {
t.Fatalf("field %s on %s uses snake_case json tag %q", field.Name, tc.name, tagName)
}
}
})
}
}
func TestContract_AlertResourceTypeConsistency(t *testing.T) {
cases := []struct {
resourceType string
want []string
}{
{resourceType: "VM", want: []string{"vm", "guest"}},
{resourceType: "Container", want: []string{}},
{resourceType: "Node", want: []string{"node"}},
{resourceType: "Agent", want: []string{"agent", "node"}},
{resourceType: "Agent Disk", want: []string{}},
{resourceType: "PBS", want: []string{"pbs", "node"}},
{resourceType: "Docker Container", want: []string{}},
{resourceType: "DockerHost", want: []string{}},
{resourceType: "Docker Service", want: []string{}},
{resourceType: "Storage", want: []string{"storage"}},
{resourceType: "PMG", want: []string{"pmg", "node"}},
{resourceType: "K8s", want: []string{}},
}
for _, tc := range cases {
t.Run(tc.resourceType, func(t *testing.T) {
got := alerts.CanonicalResourceTypeKeys(tc.resourceType)
if len(tc.want) > 0 && len(got) == 0 {
t.Fatalf("resource type %q returned no canonical keys", tc.resourceType)
}
if len(tc.want) == 0 && len(got) == 0 {
return
}
if !reflect.DeepEqual(got, tc.want) {
t.Fatalf("canonical keys mismatch for %q: got %v want %v", tc.resourceType, got, tc.want)
}
})
}
}
func TestContract_TenantResourcesDoNotFallbackToRawSnapshotSeeding(t *testing.T) {
now := time.Date(2026, 3, 17, 9, 0, 0, 0, time.UTC)
h := NewResourceHandlers(&config.Config{DataPath: t.TempDir()})
h.SetStateProvider(resourceStateProvider{snapshot: models.StateSnapshot{
Hosts: []models.Host{{ID: "host-default", Hostname: "default", Status: "online", LastSeen: now}},
}})
h.SetTenantStateProvider(tenantResourceStateProvider{snapshots: map[string]models.StateSnapshot{
"acme": {
Hosts: []models.Host{{ID: "host-tenant-snapshot", Hostname: "tenant-snapshot", Status: "online", LastSeen: now}},
LastUpdate: time.Time{},
},
}})
req := httptest.NewRequest(http.MethodGet, "/api/resources?type=agent", nil)
req = req.WithContext(context.WithValue(req.Context(), OrgIDContextKey, "acme"))
rec := httptest.NewRecorder()
h.HandleListResources(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status = %d, body=%s", rec.Code, rec.Body.String())
}
const want = `{"data":[],"meta":{"page":1,"limit":50,"total":0,"totalPages":0},"aggregations":{"total":0,"byType":{},"byStatus":{},"bySource":{},"policyPosture":{"totalResources":0,"sensitivityCounts":{},"routingCounts":{},"redactionCounts":{}}}}`
if got := strings.TrimSpace(rec.Body.String()); got != want {
t.Fatalf("tenant resource fallback contract = %s, want %s", got, want)
}
}
func TestContract_ResourceListPolicyMetadata(t *testing.T) {
now := time.Date(2026, 3, 17, 10, 0, 0, 0, time.UTC)
h := NewResourceHandlers(&config.Config{DataPath: t.TempDir()})
h.SetStateProvider(resourceUnifiedSeedProvider{
snapshot: models.StateSnapshot{LastUpdate: now},
resources: []unifiedresources.Resource{
{
ID: "vm-sensitive",
Type: unifiedresources.ResourceTypeVM,
Name: "payments-vm",
Status: unifiedresources.StatusOnline,
LastSeen: now,
Sources: []unifiedresources.DataSource{unifiedresources.SourceProxmox},
Tags: []string{"customer-data"},
Identity: unifiedresources.ResourceIdentity{
Hostnames: []string{"payments.internal"},
IPAddresses: []string{"10.0.0.44"},
},
},
},
})
req := httptest.NewRequest(http.MethodGet, "/api/resources?type=vm", nil)
rec := httptest.NewRecorder()
h.HandleListResources(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status = %d, body=%s", rec.Code, rec.Body.String())
}
var resp ResourcesResponse
if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil {
t.Fatalf("decode response: %v", err)
}
if len(resp.Data) != 1 {
t.Fatalf("expected 1 resource, got %d", len(resp.Data))
}
resource := resp.Data[0]
if resource.Canonical == nil {
t.Fatal("expected canonical identity metadata in resource contract")
}
if strings.TrimSpace(resource.Canonical.DisplayName) == "" {
t.Fatal("expected canonical display name in resource contract")
}
if resource.Policy == nil {
t.Fatal("expected policy metadata in resource contract")
}
if got := resource.Policy.Sensitivity; got != unifiedresources.ResourceSensitivityRestricted {
t.Fatalf("policy sensitivity = %q, want %q", got, unifiedresources.ResourceSensitivityRestricted)
}
if got := resource.Policy.Routing.Scope; got != unifiedresources.ResourceRoutingScopeLocalOnly {
t.Fatalf("routing scope = %q, want %q", got, unifiedresources.ResourceRoutingScopeLocalOnly)
}
wantRedactions := []unifiedresources.ResourceRedactionHint{
unifiedresources.ResourceRedactionHostname,
unifiedresources.ResourceRedactionIPAddress,
unifiedresources.ResourceRedactionPlatformID,
unifiedresources.ResourceRedactionAlias,
}
if !reflect.DeepEqual(resource.Policy.Routing.Redact, wantRedactions) {
t.Fatalf("policy redact = %#v, want %#v", resource.Policy.Routing.Redact, wantRedactions)
}
if got := resource.AISafeSummary; !strings.Contains(got, "virtual machine resource;") || !strings.Contains(got, "local-only context") {
t.Fatalf("aiSafeSummary = %q", got)
}
if resp.Aggregations.PolicyPosture == nil {
t.Fatal("expected policy posture aggregation in resource list contract")
}
if got := resp.Aggregations.PolicyPosture.TotalResources; got != 1 {
t.Fatalf("policyPosture.totalResources = %d, want 1", got)
}
if got := resp.Aggregations.PolicyPosture.SensitivityCounts[unifiedresources.ResourceSensitivityRestricted]; got != 1 {
t.Fatalf("policyPosture.sensitivityCounts[restricted] = %d, want 1", got)
}
if got := resp.Aggregations.PolicyPosture.RoutingCounts[unifiedresources.ResourceRoutingScopeLocalOnly]; got != 1 {
t.Fatalf("policyPosture.routingCounts[local-only] = %d, want 1", got)
}
if got := resp.Aggregations.PolicyPosture.RedactionCounts[unifiedresources.ResourceRedactionHostname]; got != 1 {
t.Fatalf("policyPosture.redactionCounts[hostname] = %d, want 1", got)
}
}
func TestContract_ResourceListUsesDeterministicNameTieBreakers(t *testing.T) {
now := time.Date(2026, 4, 11, 0, 0, 0, 0, time.UTC)
h := NewResourceHandlers(&config.Config{DataPath: t.TempDir()})
h.SetStateProvider(resourceUnifiedSeedProvider{
snapshot: models.StateSnapshot{LastUpdate: now},
resources: []unifiedresources.Resource{
{ID: "storage-c", Type: unifiedresources.ResourceTypeStorage, Name: "backup-vault-a", Status: unifiedresources.StatusOnline, LastSeen: now},
{ID: "storage-a", Type: unifiedresources.ResourceTypeStorage, Name: "backup-vault-a", Status: unifiedresources.StatusOnline, LastSeen: now},
{ID: "storage-b", Type: unifiedresources.ResourceTypeStorage, Name: "backup-vault-a", Status: unifiedresources.StatusOnline, LastSeen: now},
},
})
for attempt := 0; attempt < 2; attempt++ {
rec := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/api/resources?type=storage&page=1&limit=100", nil)
h.HandleListResources(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("attempt %d: status = %d, body=%s", attempt+1, rec.Code, rec.Body.String())
}
var resp ResourcesResponse
if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil {
t.Fatalf("attempt %d: decode response: %v", attempt+1, err)
}
gotIDs := make([]string, 0, len(resp.Data))
for _, resource := range resp.Data {
gotIDs = append(gotIDs, resource.ID)
}
wantIDs := []string{"storage-a", "storage-b", "storage-c"}
if len(gotIDs) != len(wantIDs) {
t.Fatalf("attempt %d: expected %d resources, got %d (%v)", attempt+1, len(wantIDs), len(gotIDs), gotIDs)
}
for index := range wantIDs {
if gotIDs[index] != wantIDs[index] {
t.Fatalf("attempt %d: position %d = %q, want %q (got=%v)", attempt+1, index, gotIDs[index], wantIDs[index], gotIDs)
}
}
}
}
func TestContract_StateAndResourceListShareCanonicalMockResourceContract(t *testing.T) {
t.Setenv("PULSE_MOCK_MODE", "true")
mock.SetEnabled(true)
t.Cleanup(func() { mock.SetEnabled(false) })
dataPath := t.TempDir()
hashedPassword, err := authpkg.HashPassword("password")
if err != nil {
t.Fatalf("hash password: %v", err)
}
cfg := &config.Config{
DataPath: dataPath,
MultiTenantEnabled: true,
AuthUser: "admin",
AuthPass: hashedPassword,
}
InitSessionStore(dataPath)
InitCSRFStore(dataPath)
monitor, err := monitoring.New(cfg)
if err != nil {
t.Fatalf("new monitor: %v", err)
}
t.Cleanup(func() { monitor.Stop() })
router := &Router{
config: cfg,
monitor: monitor,
persistence: config.NewConfigPersistence(dataPath),
}
handlers := NewResourceHandlers(cfg)
handlers.SetStateProvider(monitor)
stateReq := httptest.NewRequest(http.MethodGet, "/api/state", nil)
stateReq.SetBasicAuth("admin", "password")
stateRec := httptest.NewRecorder()
router.handleState(stateRec, stateReq)
if stateRec.Code != http.StatusOK {
t.Fatalf("/api/state status = %d, body=%s", stateRec.Code, stateRec.Body.String())
}
var state models.StateFrontend
if err := json.NewDecoder(stateRec.Body).Decode(&state); err != nil {
t.Fatalf("decode /api/state: %v", err)
}
listReq := httptest.NewRequest(http.MethodGet, "/api/resources?type=agent,docker-host,pbs,pmg,k8s-cluster,k8s-node&page=1&limit=100", nil)
listRec := httptest.NewRecorder()
handlers.HandleListResources(listRec, listReq)
if listRec.Code != http.StatusOK {
t.Fatalf("/api/resources status = %d, body=%s", listRec.Code, listRec.Body.String())
}
var listResp ResourcesResponse
if err := json.NewDecoder(listRec.Body).Decode(&listResp); err != nil {
t.Fatalf("decode /api/resources: %v", err)
}
allowedTypes := map[string]bool{
"agent": true,
"docker-host": true,
"k8s-cluster": true,
"k8s-node": true,
"pbs": true,
"pmg": true,
}
stateContracts := make(map[string]resourceContractSnapshot)
for _, resource := range state.Resources {
if !allowedTypes[resource.Type] {
continue
}
if resource.Type == "node" {
t.Fatalf("/api/state published legacy resource type %#v", resource)
}
if resource.Name == "pve1" || resource.Name == "edge-apps-01" {
t.Fatalf("/api/state published legacy display name %#v", resource)
}
stateContracts[resource.ID] = resourceContractSnapshot{
ID: resource.ID,
Name: resource.Name,
Type: resource.Type,
}
}
listContracts := make(map[string]resourceContractSnapshot)
for _, resource := range listResp.Data {
if !allowedTypes[string(resource.Type)] {
continue
}
listContracts[resource.ID] = resourceContractSnapshot{
ID: resource.ID,
Name: resource.Name,
Type: string(resource.Type),
}
}
if !reflect.DeepEqual(stateContracts, listContracts) {
t.Fatalf("/api/state and /api/resources resource contracts diverged: state=%#v resources=%#v", stateContracts, listContracts)
}
foundCanonicalProxmoxName := false
for _, resource := range stateContracts {
if resource.Name == "West Production A" && resource.Type == "agent" {
foundCanonicalProxmoxName = true
break
}
}
if !foundCanonicalProxmoxName {
t.Fatalf("expected canonical proxmox resource contract in /api/state, got %#v", stateContracts)
}
}
func TestContract_ResourceListCarriesTimelineAndCapabilityContracts(t *testing.T) {
now := time.Date(2026, 3, 18, 12, 0, 0, 0, time.UTC)
occurredAt := now.Add(-2 * time.Minute)
payload := ResourcesResponse{
Data: []unifiedresources.Resource{
{
ID: "vm-42",
Type: unifiedresources.ResourceTypeVM,
Name: "web-42",
Status: unifiedresources.StatusOnline,
LastSeen: now,
Capabilities: []unifiedresources.ResourceCapability{
{
Name: "restart",
Type: unifiedresources.CapabilityTypeCommon,
Description: "Restart the VM",
MinimumApprovalLevel: unifiedresources.ApprovalAdmin,
Params: []unifiedresources.CapabilityParam{
{
Name: "force",
Type: "boolean",
Required: false,
Description: "Restart without graceful shutdown",
},
},
},
},
Relationships: []unifiedresources.ResourceRelationship{
{
SourceID: "vm-42",
TargetID: "node-1",
Type: unifiedresources.RelRunsOn,
Confidence: 1,
Active: true,
Discoverer: "proxmox_adapter",
ObservedAt: now,
LastSeenAt: now,
Metadata: map[string]any{
"source": "live",
"cluster": "pve-prod",
},
},
},
RecentChanges: []unifiedresources.ResourceChange{
{
ID: "chg-42",
ObservedAt: now,
OccurredAt: &occurredAt,
ResourceID: "vm-42",
Kind: unifiedresources.ChangeStateTransition,
From: "offline",
To: "online",
SourceType: unifiedresources.SourcePlatformEvent,
SourceAdapter: unifiedresources.AdapterProxmox,
Confidence: unifiedresources.ConfidenceHigh,
RelatedResources: []string{"node-1"},
Reason: "vm started",
Metadata: map[string]any{
"source": "snapshot",
"ticket": "INC-1234",
},
},
},
FacetCounts: unifiedresources.ResourceFacetCounts{
RecentChanges: 1,
},
},
},
Meta: ResourcesMeta{
Page: 1,
Limit: 50,
Total: 1,
TotalPages: 1,
},
Aggregations: unifiedresources.ResourceStats{
Total: 1,
ByType: map[unifiedresources.ResourceType]int{unifiedresources.ResourceTypeVM: 1},
ByStatus: map[unifiedresources.ResourceStatus]int{unifiedresources.StatusOnline: 1},
BySource: map[unifiedresources.DataSource]int{unifiedresources.SourceProxmox: 1},
PolicyPosture: unifiedresources.EmptyResourcePolicyPostureSummary(),
},
}
got, err := json.Marshal(payload)
if err != nil {
t.Fatalf("marshal resource response: %v", err)
}
const want = `{
"data":[
{
"id":"vm-42",
"type":"vm",
"name":"web-42",
"status":"online",
"lastSeen":"2026-03-18T12:00:00Z",
"updatedAt":"0001-01-01T00:00:00Z",
"sources":null,
"identity":{},
"capabilities":[
{
"name":"restart",
"type":"common",
"description":"Restart the VM",
"minimumApprovalLevel":"admin",
"params":[
{
"name":"force",
"type":"boolean",
"required":false,
"isSensitive":false,
"description":"Restart without graceful shutdown"
}
]
}
],
"relationships":[
{
"sourceId":"vm-42",
"targetId":"node-1",
"type":"runs_on",
"confidence":1,
"active":true,
"discoverer":"proxmox_adapter",
"observedAt":"2026-03-18T12:00:00Z",
"lastSeenAt":"2026-03-18T12:00:00Z",
"metadata":{"cluster":"pve-prod","source":"live"}
}
],
"recentChanges":[
{
"id":"chg-42",
"observedAt":"2026-03-18T12:00:00Z",
"occurredAt":"2026-03-18T11:58:00Z",
"resourceId":"vm-42",
"kind":"state_transition",
"from":"offline",
"to":"online",
"sourceType":"platform_event",
"sourceAdapter":"proxmox_adapter",
"confidence":"high",
"relatedResources":["node-1"],
"reason":"vm started",
"metadata":{"source":"snapshot","ticket":"INC-1234"}
}
],
"facetCounts":{
"recentChanges":1
}
}
],
"meta":{"page":1,"limit":50,"total":1,"totalPages":1},
"aggregations":{"total":1,"byType":{"vm":1},"byStatus":{"online":1},"bySource":{"proxmox":1},"policyPosture":{"totalResources":0,"sensitivityCounts":{},"routingCounts":{},"redactionCounts":{}}}
}`
assertJSONSnapshot(t, got, want)
}
func TestContract_ResourceListUsesTenantStateProviderAtStartup(t *testing.T) {
cfg := &config.Config{
DataPath: t.TempDir(),
ConfigPath: t.TempDir(),
}
router := NewRouter(cfg, nil, &monitoring.MultiTenantMonitor{}, nil, nil, "1.0.0")
rec := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/api/resources?page=1&limit=100", nil)
req = req.WithContext(context.WithValue(req.Context(), OrgIDContextKey, "tenant-a"))
router.resourceHandlers.HandleListResources(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status = %d, body=%s", rec.Code, rec.Body.String())
}
}
func TestContract_ResourceCapabilitiesJSONSnapshot(t *testing.T) {
payload := struct {
ResourceID string `json:"resourceId"`
Capabilities []unifiedresources.ResourceCapability `json:"capabilities"`
Count int `json:"count"`
}{
ResourceID: "vm:42",
Capabilities: []unifiedresources.ResourceCapability{
{
Name: "restart",
Type: unifiedresources.CapabilityTypeCommon,
Description: "Restart the VM",
MinimumApprovalLevel: unifiedresources.ApprovalAdmin,
},
},
Count: 1,
}
got, err := json.Marshal(payload)
if err != nil {
t.Fatalf("marshal resource capabilities response: %v", err)
}
const want = `{
"resourceId":"vm:42",
"capabilities":[
{
"name":"restart",
"type":"common",
"description":"Restart the VM",
"minimumApprovalLevel":"admin"
}
],
"count":1
}`
assertJSONSnapshot(t, got, want)
}
func TestContract_AgentExecWebSocketRejectsUnboundToken(t *testing.T) {
rawToken := "contract-agent-unbound-token.12345678"
record := newTokenRecord(t, rawToken, []string{config.ScopeAgentExec}, nil)
cfg := newTestConfigWithTokens(t, record)
router := NewRouter(cfg, nil, nil, nil, nil, "1.0.0")
ts := newIPv4HTTPServer(t, router.Handler())
defer ts.Close()
conn, _, err := websocket.DefaultDialer.Dial(wsURLForHTTP(ts.URL)+"/api/agent/ws", wsHeadersForHTTP(t, ts.URL))
if err != nil {
t.Fatalf("Dial: %v", err)
}
defer conn.Close()
regMsg, err := agentexec.NewMessage(agentexec.MsgTypeAgentRegister, "", agentexec.AgentRegisterPayload{
AgentID: "agent-node-1",
Hostname: "node-1",
Version: "1.0.0",
Platform: "linux",
Token: rawToken,
})
if err != nil {
t.Fatalf("NewMessage: %v", err)
}
if err := conn.WriteJSON(regMsg); err != nil {
t.Fatalf("WriteJSON: %v", err)
}
reg := readRegisteredPayload(t, conn)
if reg.Success {
t.Fatalf("expected unbound agent exec token registration to fail closed")
}
}
func TestContract_AgentExecWebSocketAcceptsLegacyHostnameBinding(t *testing.T) {
rawToken := "contract-agent-legacy-token.12345678"
record := newTokenRecord(t, rawToken, []string{config.ScopeAgentExec}, map[string]string{"bound_hostname": "node-1"})
cfg := newTestConfigWithTokens(t, record)
router := NewRouter(cfg, nil, nil, nil, nil, "1.0.0")
ts := newIPv4HTTPServer(t, router.Handler())
defer ts.Close()
conn, _, err := websocket.DefaultDialer.Dial(wsURLForHTTP(ts.URL)+"/api/agent/ws", wsHeadersForHTTP(t, ts.URL))
if err != nil {
t.Fatalf("Dial: %v", err)
}
defer conn.Close()
regMsg, err := agentexec.NewMessage(agentexec.MsgTypeAgentRegister, "", agentexec.AgentRegisterPayload{
AgentID: "agent-node-1",
Hostname: "node-1",
Version: "1.0.0",
Platform: "linux",
Token: rawToken,
})
if err != nil {
t.Fatalf("NewMessage: %v", err)
}
if err := conn.WriteJSON(regMsg); err != nil {
t.Fatalf("WriteJSON: %v", err)
}
reg := readRegisteredPayload(t, conn)
if !reg.Success {
t.Fatalf("expected legacy hostname binding to resolve to canonical agent binding, got %q", reg.Message)
}
}
func TestContract_AgentExecWebSocketBindsProxmoxInstallTokenOnFirstUse(t *testing.T) {
rawToken := "contract-agent-proxmox-install-token.12345678"
record := newTokenRecord(t, rawToken, []string{config.ScopeAgentExec, config.ScopeAgentReport}, map[string]string{
"install_type": "pve",
"issued_via": agentInstallIssuedViaConfig,
})
cfg := newTestConfigWithTokens(t, record)
router := NewRouter(cfg, nil, nil, nil, nil, "1.0.0")
ts := newIPv4HTTPServer(t, router.Handler())
defer ts.Close()
conn, _, err := websocket.DefaultDialer.Dial(wsURLForHTTP(ts.URL)+"/api/agent/ws", wsHeadersForHTTP(t, ts.URL))
if err != nil {
t.Fatalf("Dial: %v", err)
}
regMsg, err := agentexec.NewMessage(agentexec.MsgTypeAgentRegister, "", agentexec.AgentRegisterPayload{
AgentID: "agent-delly",
Hostname: "delly",
Version: "1.0.0",
Platform: "linux",
Token: rawToken,
})
if err != nil {
conn.Close()
t.Fatalf("NewMessage: %v", err)
}
if err := conn.WriteJSON(regMsg); err != nil {
conn.Close()
t.Fatalf("WriteJSON: %v", err)
}
reg := readRegisteredPayload(t, conn)
conn.Close()
if !reg.Success {
t.Fatalf("expected Pulse-minted Proxmox install token to bind on first command registration, got %q", reg.Message)
}
config.Mu.Lock()
if got := cfg.APITokens[0].Metadata["bound_hostname"]; got != "delly" {
config.Mu.Unlock()
t.Fatalf("bound_hostname = %q, want delly", got)
}
if got := cfg.APITokens[0].Metadata["bound_agent_id"]; got != "agent-delly" {
config.Mu.Unlock()
t.Fatalf("bound_agent_id = %q, want agent-delly", got)
}
config.Mu.Unlock()
conn, _, err = websocket.DefaultDialer.Dial(wsURLForHTTP(ts.URL)+"/api/agent/ws", wsHeadersForHTTP(t, ts.URL))
if err != nil {
t.Fatalf("Dial rebound: %v", err)
}
defer conn.Close()
regMsg, err = agentexec.NewMessage(agentexec.MsgTypeAgentRegister, "", agentexec.AgentRegisterPayload{
AgentID: "agent-other",
Hostname: "other-host",
Version: "1.0.0",
Platform: "linux",
Token: rawToken,
})
if err != nil {
t.Fatalf("NewMessage rebound: %v", err)
}
if err := conn.WriteJSON(regMsg); err != nil {
t.Fatalf("WriteJSON rebound: %v", err)
}
reg = readRegisteredPayload(t, conn)
if reg.Success {
t.Fatalf("expected first-use-bound Proxmox install token to reject a different command agent")
}
}
func TestContract_AdminBypassFailsClosedOutsideDevMode(t *testing.T) {
t.Setenv("ALLOW_ADMIN_BYPASS", "1")
t.Setenv("PULSE_DEV", "")
t.Setenv("NODE_ENV", "production")
resetAdminBypassState()
if adminBypassEnabled() {
t.Fatal("expected admin bypass to fail closed outside development mode")
}
}
func TestContract_ResourceRelationshipsJSONSnapshot(t *testing.T) {
now := time.Date(2026, 3, 18, 17, 0, 0, 0, time.UTC)
payload := struct {
ResourceID string `json:"resourceId"`
Relationships []unifiedresources.ResourceRelationship `json:"relationships"`
Count int `json:"count"`
}{
ResourceID: "vm:42",
Relationships: []unifiedresources.ResourceRelationship{
{
SourceID: "vm:42",
TargetID: "node-1",
Type: unifiedresources.RelRunsOn,
Confidence: 1,
Active: true,
Discoverer: "proxmox_adapter",
ObservedAt: now,
LastSeenAt: now,
Metadata: map[string]any{
"source": "live",
"cluster": "pve-prod",
},
},
},
Count: 1,
}
got, err := json.Marshal(payload)
if err != nil {
t.Fatalf("marshal resource relationships response: %v", err)
}
const want = `{
"resourceId":"vm:42",
"relationships":[
{
"sourceId":"vm:42",
"targetId":"node-1",
"type":"runs_on",
"confidence":1,
"active":true,
"discoverer":"proxmox_adapter",
"observedAt":"2026-03-18T17:00:00Z",
"lastSeenAt":"2026-03-18T17:00:00Z",
"metadata":{"cluster":"pve-prod","source":"live"}
}
],
"count":1
}`
assertJSONSnapshot(t, got, want)
}
func TestContract_ResourceTimelineJSONSnapshot(t *testing.T) {
now := time.Date(2026, 3, 18, 17, 0, 0, 0, time.UTC)
payload := struct {
ResourceID string `json:"resourceId"`
RecentChanges []unifiedresources.ResourceChange `json:"recentChanges"`
Count int `json:"count"`
}{
ResourceID: "vm:42",
RecentChanges: []unifiedresources.ResourceChange{
{
ID: "chg-42",
ResourceID: "vm:42",
ObservedAt: now,
OccurredAt: &now,
Kind: unifiedresources.ChangeStateTransition,
From: "offline",
To: "online",
SourceType: unifiedresources.SourcePlatformEvent,
SourceAdapter: unifiedresources.AdapterProxmox,
Confidence: unifiedresources.ConfidenceHigh,
RelatedResources: []string{"node-1"},
Reason: "vm started",
Metadata: map[string]any{"source": "snapshot", "ticket": "INC-1234"},
},
},
Count: 1,
}
got, err := json.Marshal(payload)
if err != nil {
t.Fatalf("marshal resource timeline response: %v", err)
}
const want = `{
"resourceId":"vm:42",
"recentChanges":[
{
"id":"chg-42",
"observedAt":"2026-03-18T17:00:00Z",
"occurredAt":"2026-03-18T17:00:00Z",
"resourceId":"vm:42",
"kind":"state_transition",
"from":"offline",
"to":"online",
"sourceType":"platform_event",
"sourceAdapter":"proxmox_adapter",
"confidence":"high",
"relatedResources":["node-1"],
"reason":"vm started",
"metadata":{"source":"snapshot","ticket":"INC-1234"}
}
],
"count":1
}`
assertJSONSnapshot(t, got, want)
}
func TestContract_ResourceTimelineRelationshipJSONSnapshot(t *testing.T) {
now := time.Date(2026, 3, 18, 17, 5, 0, 0, time.UTC)
payload := struct {
ResourceID string `json:"resourceId"`
RecentChanges []unifiedresources.ResourceChange `json:"recentChanges"`
Count int `json:"count"`
}{
ResourceID: "vm:42",
RecentChanges: []unifiedresources.ResourceChange{
{
ID: "chg-relationship-42",
ObservedAt: now,
OccurredAt: &now,
ResourceID: "vm:42",
Kind: unifiedresources.ChangeRelationship,
From: "node-1",
To: "node-2",
SourceType: unifiedresources.SourcePulseDiff,
SourceAdapter: unifiedresources.AdapterProxmox,
Confidence: unifiedresources.ConfidenceHigh,
RelatedResources: []string{"db:alpha", "service:beta"},
Reason: "relationship updated",
Metadata: map[string]any{
"edgeType": "depends_on",
"active": true,
},
},
},
Count: 1,
}
got, err := json.Marshal(payload)
if err != nil {
t.Fatalf("marshal resource timeline relationship response: %v", err)
}
const want = `{
"resourceId":"vm:42",
"recentChanges":[
{
"id":"chg-relationship-42",
"observedAt":"2026-03-18T17:05:00Z",
"occurredAt":"2026-03-18T17:05:00Z",
"resourceId":"vm:42",
"kind":"relationship_change",
"from":"node-1",
"to":"node-2",
"sourceType":"pulse_diff",
"sourceAdapter":"proxmox_adapter",
"confidence":"high",
"relatedResources":["db:alpha","service:beta"],
"reason":"relationship updated",
"metadata":{"active":true,"edgeType":"depends_on"}
}
],
"count":1
}`
assertJSONSnapshot(t, got, want)
}
func TestContract_ResourceTimelineRestartJSONSnapshot(t *testing.T) {
now := time.Date(2026, 3, 18, 17, 10, 0, 0, time.UTC)
payload := struct {
ResourceID string `json:"resourceId"`
RecentChanges []unifiedresources.ResourceChange `json:"recentChanges"`
Count int `json:"count"`
}{
ResourceID: "container:7",
RecentChanges: []unifiedresources.ResourceChange{
{
ID: "chg-restart-7",
ObservedAt: now,
OccurredAt: &now,
ResourceID: "container:7",
Kind: unifiedresources.ChangeRestart,
From: "online|docker.restartCount=1|docker.uptimeSeconds=3600",
To: "online|docker.restartCount=2|docker.uptimeSeconds=120",
SourceType: unifiedresources.SourcePlatformEvent,
SourceAdapter: unifiedresources.AdapterDocker,
Confidence: unifiedresources.ConfidenceHigh,
RelatedResources: []string{"node:1", "service:api"},
Reason: "resource restart detected",
Metadata: map[string]any{
"changedFields": []string{"docker.restartCount", "docker.uptimeSeconds"},
},
},
},
Count: 1,
}
got, err := json.Marshal(payload)
if err != nil {
t.Fatalf("marshal resource timeline restart response: %v", err)
}
const want = `{
"resourceId":"container:7",
"recentChanges":[
{
"id":"chg-restart-7",
"observedAt":"2026-03-18T17:10:00Z",
"occurredAt":"2026-03-18T17:10:00Z",
"resourceId":"container:7",
"kind":"restart",
"from":"online|docker.restartCount=1|docker.uptimeSeconds=3600",
"to":"online|docker.restartCount=2|docker.uptimeSeconds=120",
"sourceType":"platform_event",
"sourceAdapter":"docker_adapter",
"confidence":"high",
"relatedResources":["node:1","service:api"],
"reason":"resource restart detected",
"metadata":{"changedFields":["docker.restartCount","docker.uptimeSeconds"]}
}
],
"count":1
}`
assertJSONSnapshot(t, got, want)
}
func TestContract_ResourceTimelineAnomalyJSONSnapshot(t *testing.T) {
now := time.Date(2026, 3, 18, 17, 12, 0, 0, time.UTC)
payload := struct {
ResourceID string `json:"resourceId"`
RecentChanges []unifiedresources.ResourceChange `json:"recentChanges"`
Count int `json:"count"`
}{
ResourceID: "storage:1",
RecentChanges: []unifiedresources.ResourceChange{
{
ID: "chg-anomaly-1",
ObservedAt: now,
OccurredAt: &now,
ResourceID: "storage:1",
Kind: unifiedresources.ChangeAnomaly,
From: "none",
To: "capacity_runway_low[warning]:PBS datastore archive is READ_ONLY",
SourceType: unifiedresources.SourcePulseDiff,
SourceAdapter: unifiedresources.AdapterProxmox,
Confidence: unifiedresources.ConfidenceHigh,
RelatedResources: []string{"node-2", "service:db"},
Reason: "resource incident changed",
Metadata: map[string]any{
"changedFields": []string{"incidents"},
},
},
},
Count: 1,
}
got, err := json.Marshal(payload)
if err != nil {
t.Fatalf("marshal resource timeline anomaly response: %v", err)
}
const want = `{
"resourceId":"storage:1",
"recentChanges":[
{
"id":"chg-anomaly-1",
"observedAt":"2026-03-18T17:12:00Z",
"occurredAt":"2026-03-18T17:12:00Z",
"resourceId":"storage:1",
"kind":"metric_anomaly",
"from":"none",
"to":"capacity_runway_low[warning]:PBS datastore archive is READ_ONLY",
"sourceType":"pulse_diff",
"sourceAdapter":"proxmox_adapter",
"confidence":"high",
"relatedResources":["node-2","service:db"],
"reason":"resource incident changed",
"metadata":{"changedFields":["incidents"]}
}
],
"count":1
}`
assertJSONSnapshot(t, got, want)
}
func TestContract_ResourceFacetsJSONSnapshot(t *testing.T) {
now := time.Date(2026, 3, 18, 17, 0, 0, 0, time.UTC)
payload := struct {
ResourceID string `json:"resourceId"`
Capabilities []unifiedresources.ResourceCapability `json:"capabilities,omitempty"`
Relationships []unifiedresources.ResourceRelationship `json:"relationships,omitempty"`
RecentChanges []unifiedresources.ResourceChange `json:"recentChanges"`
Counts struct {
RecentChanges int `json:"recentChanges"`
RecentChangeKinds map[unifiedresources.ChangeKind]int `json:"recentChangeKinds"`
RecentChangeSourceTypes map[unifiedresources.ChangeSourceType]int `json:"recentChangeSourceTypes"`
RecentChangeSourceAdapters map[unifiedresources.ChangeSourceAdapter]int `json:"recentChangeSourceAdapters"`
} `json:"counts"`
}{
ResourceID: "vm:42",
Capabilities: []unifiedresources.ResourceCapability{
{
Name: "restart",
Type: unifiedresources.CapabilityTypeCommon,
Description: "Restart the VM",
MinimumApprovalLevel: unifiedresources.ApprovalAdmin,
},
},
Relationships: []unifiedresources.ResourceRelationship{
{
SourceID: "vm:42",
TargetID: "node-1",
Type: unifiedresources.RelRunsOn,
Confidence: 1,
Active: true,
Discoverer: "proxmox_adapter",
ObservedAt: now,
LastSeenAt: now,
Metadata: map[string]any{
"source": "live",
"cluster": "pve-prod",
},
},
},
RecentChanges: []unifiedresources.ResourceChange{
{
ID: "chg-42",
ResourceID: "vm:42",
ObservedAt: now,
OccurredAt: &now,
Kind: unifiedresources.ChangeStateTransition,
From: "offline",
To: "online",
SourceType: unifiedresources.SourcePlatformEvent,
SourceAdapter: unifiedresources.AdapterProxmox,
Confidence: unifiedresources.ConfidenceHigh,
RelatedResources: []string{"node-1"},
Metadata: map[string]any{
"source": "snapshot",
"ticket": "INC-1234",
},
},
},
Counts: struct {
RecentChanges int `json:"recentChanges"`
RecentChangeKinds map[unifiedresources.ChangeKind]int `json:"recentChangeKinds"`
RecentChangeSourceTypes map[unifiedresources.ChangeSourceType]int `json:"recentChangeSourceTypes"`
RecentChangeSourceAdapters map[unifiedresources.ChangeSourceAdapter]int `json:"recentChangeSourceAdapters"`
}{
RecentChanges: 3,
RecentChangeKinds: map[unifiedresources.ChangeKind]int{unifiedresources.ChangeRestart: 1, unifiedresources.ChangeAnomaly: 2},
RecentChangeSourceTypes: map[unifiedresources.ChangeSourceType]int{
unifiedresources.SourcePlatformEvent: 1,
unifiedresources.SourcePulseDiff: 2,
},
RecentChangeSourceAdapters: map[unifiedresources.ChangeSourceAdapter]int{
unifiedresources.AdapterProxmox: 1,
unifiedresources.AdapterDocker: 2,
},
},
}
got, err := json.Marshal(payload)
if err != nil {
t.Fatalf("marshal resource facets response: %v", err)
}
const want = `{
"resourceId":"vm:42",
"capabilities":[
{
"name":"restart",
"type":"common",
"description":"Restart the VM",
"minimumApprovalLevel":"admin"
}
],
"relationships":[
{
"sourceId":"vm:42",
"targetId":"node-1",
"type":"runs_on",
"confidence":1,
"active":true,
"discoverer":"proxmox_adapter",
"observedAt":"2026-03-18T17:00:00Z",
"lastSeenAt":"2026-03-18T17:00:00Z",
"metadata":{"cluster":"pve-prod","source":"live"}
}
],
"recentChanges":[
{
"id":"chg-42",
"observedAt":"2026-03-18T17:00:00Z",
"occurredAt":"2026-03-18T17:00:00Z",
"resourceId":"vm:42",
"kind":"state_transition",
"from":"offline",
"to":"online",
"sourceType":"platform_event",
"sourceAdapter":"proxmox_adapter",
"confidence":"high",
"relatedResources":["node-1"],
"metadata":{"source":"snapshot","ticket":"INC-1234"}
}
],
"counts":{
"recentChanges":3,
"recentChangeKinds":{
"metric_anomaly":2,
"restart":1
},
"recentChangeSourceTypes":{
"platform_event":1,
"pulse_diff":2
},
"recentChangeSourceAdapters":{
"docker_adapter":2,
"proxmox_adapter":1
}
}
}`
assertJSONSnapshot(t, got, want)
}
func TestContract_ResourceFacetsDeriveCanonicalParentRelationship(t *testing.T) {
now := time.Date(2026, 3, 18, 17, 0, 0, 0, time.UTC)
parentID := "k8s-cluster-1"
payload := struct {
ResourceID string `json:"resourceId"`
Relationships []unifiedresources.ResourceRelationship `json:"relationships,omitempty"`
}{
ResourceID: "agent-1",
Relationships: unifiedresources.ResourceRelationshipsWithCanonicalParent(unifiedresources.Resource{
ID: "agent-1",
Type: unifiedresources.ResourceTypeAgent,
ParentID: &parentID,
LastSeen: now,
}),
}
got, err := json.Marshal(payload)
if err != nil {
t.Fatalf("marshal derived parent relationship facets response: %v", err)
}
const want = `{
"resourceId":"agent-1",
"relationships":[
{
"sourceId":"agent-1",
"targetId":"k8s-cluster-1",
"type":"owned_by",
"confidence":1,
"active":true,
"discoverer":"resource_registry",
"observedAt":"2026-03-18T17:00:00Z",
"lastSeenAt":"2026-03-18T17:00:00Z",
"metadata":{"source":"parentId"}
}
]
}`
assertJSONSnapshot(t, got, want)
}
func TestContract_ActionPlanJSONSnapshot(t *testing.T) {
now := time.Date(2026, 5, 3, 10, 0, 0, 0, time.UTC)
resource := unifiedresources.Resource{
ID: "vm:42",
Type: unifiedresources.ResourceTypeVM,
Name: "web-42",
Status: unifiedresources.StatusWarning,
LastSeen: now,
UpdatedAt: now,
Sources: []unifiedresources.DataSource{unifiedresources.SourceProxmox},
Capabilities: []unifiedresources.ResourceCapability{
{
Name: "restart",
Type: unifiedresources.CapabilityTypeCommon,
Description: "Restart the VM",
MinimumApprovalLevel: unifiedresources.ApprovalAdmin,
InternalHandler: "proxmox.vm.restart",
Params: []unifiedresources.CapabilityParam{
{Name: "mode", Type: "string", Required: true, Enum: []string{"graceful", "force"}},
},
},
},
Relationships: []unifiedresources.ResourceRelationship{
{
SourceID: "vm:42",
TargetID: "node-1",
Type: unifiedresources.RelRunsOn,
Confidence: 1,
Active: true,
Discoverer: "proxmox_adapter",
ObservedAt: now,
LastSeenAt: now,
},
},
}
req := unifiedresources.ActionRequest{
RequestID: "agent-run-123",
ResourceID: "vm:42",
CapabilityName: "restart",
Params: map[string]any{"mode": "graceful"},
Reason: "Recover after confirmed outage",
RequestedBy: "agent:oncall-helper",
}
plan, err := (actionplanner.Planner{Now: func() time.Time { return now }}).Plan(req, resource)
if err != nil {
t.Fatalf("plan action: %v", err)
}
got, err := json.Marshal(plan)
if err != nil {
t.Fatalf("marshal action plan: %v", err)
}
const want = `{
"actionId":"act_371d0bcde73818752f83bb759e1fa523",
"requestId":"agent-run-123",
"allowed":true,
"requiresApproval":true,
"approvalPolicy":"admin",
"predictedBlastRadius":["vm:42","node-1"],
"rollbackAvailable":false,
"message":"Plan created for restart on web-42. Execution requires admin approval and is not performed by this endpoint.",
"plannedAt":"2026-05-03T10:00:00Z",
"expiresAt":"2026-05-03T10:05:00Z",
"resourceVersion":"resource:sha256:3a87c71cf83bd017736bcb25",
"policyVersion":"policy:sha256:0bce3cd2df181ace685598eb",
"planHash":"sha256:38e8a016794bae597cd6129e65506556048bcac88d6a5a1b59e1337a2acc5a05",
"preflight":{
"target":"vm:42",
"currentState":"web-42 is warning",
"intendedChange":"Restart the VM",
"dryRunAvailable":false,
"dryRunSummary":"No provider-supported dry run is advertised for this capability.",
"safetyChecks":[
"Resource was resolved from the unified resource registry.",
"Capability is advertised by the resource contract.",
"This endpoint plans only; it does not approve or execute the action.",
"Execution requires admin approval."
],
"verificationSteps":[
"Refresh the resource and confirm the expected state after execution.",
"Review /api/audit/actions/act_371d0bcde73818752f83bb759e1fa523/events for lifecycle evidence."
],
"generatedAt":"2026-05-03T10:00:00Z"
}
}`
assertJSONSnapshot(t, got, want)
}
func TestContract_ActionPlanAuditLifecycleSnapshot(t *testing.T) {
now := time.Date(2026, 5, 3, 10, 0, 0, 0, time.UTC)
resource := unifiedresources.Resource{
ID: "vm:42",
Type: unifiedresources.ResourceTypeVM,
Name: "web-42",
Status: unifiedresources.StatusWarning,
LastSeen: now,
UpdatedAt: now,
Sources: []unifiedresources.DataSource{unifiedresources.SourceProxmox},
Capabilities: []unifiedresources.ResourceCapability{
{
Name: "restart",
Type: unifiedresources.CapabilityTypeCommon,
Description: "Restart the VM",
MinimumApprovalLevel: unifiedresources.ApprovalAdmin,
InternalHandler: "proxmox.vm.restart",
Params: []unifiedresources.CapabilityParam{
{Name: "mode", Type: "string", Required: true, Enum: []string{"graceful", "force"}},
},
},
},
}
req := unifiedresources.ActionRequest{
RequestID: "agent-run-123",
ResourceID: "vm:42",
CapabilityName: "restart",
Params: map[string]any{"mode": "graceful"},
Reason: "Recover after confirmed outage",
RequestedBy: "agent:oncall-helper",
}
plan, err := (actionplanner.Planner{Now: func() time.Time { return now }}).Plan(req, resource)
if err != nil {
t.Fatalf("plan action: %v", err)
}
store := unifiedresources.NewMemoryStore()
if err := persistActionPlanAudit(store, req, plan); err != nil {
t.Fatalf("persist action plan audit: %v", err)
}
audits, err := store.GetActionAudits("vm:42", time.Time{}, 10)
if err != nil {
t.Fatalf("GetActionAudits: %v", err)
}
events, err := store.GetActionLifecycleEvents(plan.ActionID, time.Time{}, 10)
if err != nil {
t.Fatalf("GetActionLifecycleEvents: %v", err)
}
payload := struct {
Audit struct {
ID string `json:"id"`
State unifiedresources.ActionState `json:"state"`
ResourceID string `json:"resourceId"`
RequestID string `json:"requestId"`
RequestedBy string `json:"requestedBy"`
ApprovalPolicy unifiedresources.ActionApprovalLevel `json:"approvalPolicy"`
PlanHash string `json:"planHash"`
PreflightSummary string `json:"preflightSummary"`
} `json:"audit"`
Events []struct {
State unifiedresources.ActionState `json:"state"`
Actor string `json:"actor"`
Message string `json:"message"`
} `json:"events"`
}{}
if len(audits) != 1 {
t.Fatalf("audits len = %d, want 1", len(audits))
}
payload.Audit.ID = audits[0].ID
payload.Audit.State = audits[0].State
payload.Audit.ResourceID = audits[0].Request.ResourceID
payload.Audit.RequestID = audits[0].Request.RequestID
payload.Audit.RequestedBy = audits[0].Request.RequestedBy
payload.Audit.ApprovalPolicy = audits[0].Plan.ApprovalPolicy
payload.Audit.PlanHash = audits[0].Plan.PlanHash
if audits[0].Plan.Preflight != nil {
payload.Audit.PreflightSummary = audits[0].Plan.Preflight.DryRunSummary
}
for _, event := range events {
payload.Events = append(payload.Events, struct {
State unifiedresources.ActionState `json:"state"`
Actor string `json:"actor"`
Message string `json:"message"`
}{
State: event.State,
Actor: event.Actor,
Message: event.Message,
})
}
got, err := json.Marshal(payload)
if err != nil {
t.Fatalf("marshal persisted action audit contract: %v", err)
}
const want = `{
"audit":{
"id":"act_f464b1a69ce73d27a986b445104ad708",
"state":"pending_approval",
"resourceId":"vm:42",
"requestId":"agent-run-123",
"requestedBy":"agent:oncall-helper",
"approvalPolicy":"admin",
"planHash":"sha256:e2998e7fe1b9b97c8180102bd474ece7411146db70bbb6299cdc47582de7bd1d",
"preflightSummary":"No provider-supported dry run is advertised for this capability."
},
"events":[
{
"state":"pending_approval",
"actor":"agent:oncall-helper",
"message":"Action is waiting for approval before execution."
},
{
"state":"planned",
"actor":"agent:oncall-helper",
"message":"Action plan created."
}
]
}`
assertJSONSnapshot(t, got, want)
}
func TestContract_ActionDecisionJSONSnapshot(t *testing.T) {
now := time.Date(2026, 5, 4, 15, 0, 0, 0, time.UTC)
plannedAt := now.Add(-time.Minute)
record := unifiedresources.ActionAuditRecord{
ID: "act_decision_contract",
CreatedAt: plannedAt,
UpdatedAt: plannedAt,
State: unifiedresources.ActionStatePending,
Request: unifiedresources.ActionRequest{
RequestID: "agent-run-approve",
ResourceID: "vm:42",
CapabilityName: "restart",
Reason: "Recover after confirmed outage",
RequestedBy: "agent:oncall-helper",
},
Plan: unifiedresources.ActionPlan{
ActionID: "act_decision_contract",
RequestID: "agent-run-approve",
Allowed: true,
RequiresApproval: true,
ApprovalPolicy: unifiedresources.ApprovalAdmin,
RollbackAvailable: false,
PlannedAt: plannedAt,
ExpiresAt: now.Add(4 * time.Minute),
ResourceVersion: "resource:sha256:contract",
PolicyVersion: "policy:sha256:contract",
PlanHash: "sha256:contract",
Preflight: &unifiedresources.ActionPreflight{
Target: "vm:42",
CurrentState: "web-42 is warning",
IntendedChange: "Restart the VM",
DryRunAvailable: false,
DryRunSummary: "No provider-supported dry run is advertised for this capability.",
SafetyChecks: []string{
"Resource was resolved from the unified resource registry.",
"This endpoint plans only; it does not approve or execute the action.",
},
VerificationSteps: []string{
"Review /api/audit/actions/act_decision_contract/events for lifecycle evidence.",
},
GeneratedAt: plannedAt,
},
},
}
updated, event, err := unifiedresources.ApplyActionDecision(record, unifiedresources.ActionApprovalRecord{
Actor: "operator@example.com",
Outcome: unifiedresources.OutcomeApproved,
Reason: "inside maintenance window",
}, now)
if err != nil {
t.Fatalf("apply action decision: %v", err)
}
response := actionDecisionResponse{
ActionID: updated.ID,
State: updated.State,
Approval: updated.Approvals[len(updated.Approvals)-1],
Audit: updated,
}
payload := struct {
Response actionDecisionResponse `json:"response"`
Event struct {
ActionID string `json:"actionId"`
State unifiedresources.ActionState `json:"state"`
Actor string `json:"actor"`
Message string `json:"message"`
Result *unifiedresources.ExecutionResult `json:"result,omitempty"`
Method unifiedresources.ApprovalMethod `json:"method"`
Outcome unifiedresources.ApprovalOutcome `json:"outcome"`
Policy unifiedresources.ActionApprovalLevel `json:"policy"`
} `json:"event"`
}{Response: response}
payload.Event.ActionID = event.ActionID
payload.Event.State = event.State
payload.Event.Actor = event.Actor
payload.Event.Message = event.Message
payload.Event.Result = response.Audit.Result
payload.Event.Method = response.Approval.Method
payload.Event.Outcome = response.Approval.Outcome
payload.Event.Policy = response.Audit.Plan.ApprovalPolicy
got, err := json.Marshal(payload)
if err != nil {
t.Fatalf("marshal action decision contract: %v", err)
}
const want = `{
"response":{
"actionId":"act_decision_contract",
"state":"approved",
"approval":{
"actor":"operator@example.com",
"method":"api",
"timestamp":"2026-05-04T15:00:00Z",
"outcome":"approved",
"reason":"inside maintenance window"
},
"audit":{
"id":"act_decision_contract",
"createdAt":"2026-05-04T14:59:00Z",
"updatedAt":"2026-05-04T15:00:00Z",
"state":"approved",
"request":{
"requestId":"agent-run-approve",
"resourceId":"vm:42",
"capabilityName":"restart",
"reason":"Recover after confirmed outage",
"requestedBy":"agent:oncall-helper"
},
"plan":{
"actionId":"act_decision_contract",
"requestId":"agent-run-approve",
"allowed":true,
"requiresApproval":true,
"approvalPolicy":"admin",
"rollbackAvailable":false,
"plannedAt":"2026-05-04T14:59:00Z",
"expiresAt":"2026-05-04T15:04:00Z",
"resourceVersion":"resource:sha256:contract",
"policyVersion":"policy:sha256:contract",
"planHash":"sha256:contract",
"preflight":{
"target":"vm:42",
"currentState":"web-42 is warning",
"intendedChange":"Restart the VM",
"dryRunAvailable":false,
"dryRunSummary":"No provider-supported dry run is advertised for this capability.",
"safetyChecks":[
"Resource was resolved from the unified resource registry.",
"This endpoint plans only; it does not approve or execute the action."
],
"verificationSteps":[
"Review /api/audit/actions/act_decision_contract/events for lifecycle evidence."
],
"generatedAt":"2026-05-04T14:59:00Z"
}
},
"approvals":[
{
"actor":"operator@example.com",
"method":"api",
"timestamp":"2026-05-04T15:00:00Z",
"outcome":"approved",
"reason":"inside maintenance window"
}
],
"verificationOutcome":{
"status":"unknown"
}
}
},
"event":{
"actionId":"act_decision_contract",
"state":"approved",
"actor":"operator@example.com",
"message":"Action approved. Execution remains pending a separate execution contract.",
"method":"api",
"outcome":"approved",
"policy":"admin"
}
}`
assertJSONSnapshot(t, got, want)
}
func TestContract_ActionExecutionJSONSnapshot(t *testing.T) {
startedAt := time.Date(2026, 5, 4, 15, 30, 0, 0, time.UTC)
completedAt := startedAt.Add(30 * time.Second)
plannedAt := startedAt.Add(-2 * time.Minute)
approvedAt := startedAt.Add(-time.Minute)
record := unifiedresources.ActionAuditRecord{
ID: "act_execution_contract",
CreatedAt: plannedAt,
UpdatedAt: approvedAt,
State: unifiedresources.ActionStateApproved,
Request: unifiedresources.ActionRequest{
RequestID: "agent-run-execute",
ResourceID: "vm:42",
CapabilityName: "restart",
Reason: "Recover after confirmed outage",
RequestedBy: "agent:oncall-helper",
},
Plan: unifiedresources.ActionPlan{
ActionID: "act_execution_contract",
RequestID: "agent-run-execute",
Allowed: true,
RequiresApproval: true,
ApprovalPolicy: unifiedresources.ApprovalAdmin,
RollbackAvailable: false,
PlannedAt: plannedAt,
ExpiresAt: startedAt.Add(3 * time.Minute),
ResourceVersion: "resource:sha256:contract",
PolicyVersion: "policy:sha256:contract",
PlanHash: "sha256:contract",
Preflight: &unifiedresources.ActionPreflight{
Target: "vm:42",
CurrentState: "web-42 is warning",
IntendedChange: "Restart the VM",
DryRunAvailable: false,
DryRunSummary: "No provider-supported dry run is advertised for this capability.",
SafetyChecks: []string{
"Resource was resolved from the unified resource registry.",
"Execution must use POST /api/actions/{id}/execute after approval.",
},
VerificationSteps: []string{
"Review /api/audit/actions/act_execution_contract/events for lifecycle evidence.",
},
GeneratedAt: plannedAt,
},
},
Approvals: []unifiedresources.ActionApprovalRecord{
{
Actor: "operator@example.com",
Method: unifiedresources.MethodAPI,
Timestamp: approvedAt,
Outcome: unifiedresources.OutcomeApproved,
Reason: "inside maintenance window",
},
},
}
started, startEvent, err := unifiedresources.BeginActionExecution(record, "operator@example.com", startedAt)
if err != nil {
t.Fatalf("begin action execution: %v", err)
}
completed, doneEvent, err := unifiedresources.CompleteActionExecution(started, &unifiedresources.ExecutionResult{
Success: true,
Output: "restart dispatched",
}, "operator@example.com", completedAt)
if err != nil {
t.Fatalf("complete action execution: %v", err)
}
response := actionExecutionResponse{
ActionID: completed.ID,
State: completed.State,
Result: completed.Result,
Audit: completed,
}
payload := struct {
Response actionExecutionResponse `json:"response"`
Events []struct {
ActionID string `json:"actionId"`
State unifiedresources.ActionState `json:"state"`
Actor string `json:"actor"`
Message string `json:"message"`
} `json:"events"`
}{Response: response}
for _, event := range []unifiedresources.ActionLifecycleEvent{startEvent, doneEvent} {
payload.Events = append(payload.Events, struct {
ActionID string `json:"actionId"`
State unifiedresources.ActionState `json:"state"`
Actor string `json:"actor"`
Message string `json:"message"`
}{
ActionID: event.ActionID,
State: event.State,
Actor: event.Actor,
Message: event.Message,
})
}
got, err := json.Marshal(payload)
if err != nil {
t.Fatalf("marshal action execution contract: %v", err)
}
const want = `{
"response":{
"actionId":"act_execution_contract",
"state":"completed",
"result":{
"success":true,
"output":"restart dispatched"
},
"audit":{
"id":"act_execution_contract",
"createdAt":"2026-05-04T15:28:00Z",
"updatedAt":"2026-05-04T15:30:30Z",
"state":"completed",
"request":{
"requestId":"agent-run-execute",
"resourceId":"vm:42",
"capabilityName":"restart",
"reason":"Recover after confirmed outage",
"requestedBy":"agent:oncall-helper"
},
"plan":{
"actionId":"act_execution_contract",
"requestId":"agent-run-execute",
"allowed":true,
"requiresApproval":true,
"approvalPolicy":"admin",
"rollbackAvailable":false,
"plannedAt":"2026-05-04T15:28:00Z",
"expiresAt":"2026-05-04T15:33:00Z",
"resourceVersion":"resource:sha256:contract",
"policyVersion":"policy:sha256:contract",
"planHash":"sha256:contract",
"preflight":{
"target":"vm:42",
"currentState":"web-42 is warning",
"intendedChange":"Restart the VM",
"dryRunAvailable":false,
"dryRunSummary":"No provider-supported dry run is advertised for this capability.",
"safetyChecks":[
"Resource was resolved from the unified resource registry.",
"Execution must use POST /api/actions/{id}/execute after approval."
],
"verificationSteps":[
"Review /api/audit/actions/act_execution_contract/events for lifecycle evidence."
],
"generatedAt":"2026-05-04T15:28:00Z"
}
},
"approvals":[
{
"actor":"operator@example.com",
"method":"api",
"timestamp":"2026-05-04T15:29:00Z",
"outcome":"approved",
"reason":"inside maintenance window"
}
],
"result":{
"success":true,
"output":"restart dispatched"
},
"verificationOutcome":{
"status":"unknown"
}
}
},
"events":[
{
"actionId":"act_execution_contract",
"state":"executing",
"actor":"operator@example.com",
"message":"Action execution started."
},
{
"actionId":"act_execution_contract",
"state":"completed",
"actor":"operator@example.com",
"message":"Action execution completed."
}
]
}`
assertJSONSnapshot(t, got, want)
}
func TestContract_ActionDryRunOnlyExecutionErrorJSONSnapshot(t *testing.T) {
now := time.Now().UTC().Truncate(time.Second)
h := NewResourceHandlers(&config.Config{DataPath: t.TempDir()})
store, err := h.getStore("default")
if err != nil {
t.Fatalf("get store: %v", err)
}
record := unifiedresources.ActionAuditRecord{
ID: "act_dry_run_contract",
CreatedAt: now.Add(-time.Minute),
UpdatedAt: now.Add(-time.Minute),
State: unifiedresources.ActionStatePlanned,
Request: unifiedresources.ActionRequest{
RequestID: "agent-run-dry-run",
ResourceID: "vm:42",
CapabilityName: "restart",
Reason: "Inspect possible restart remediation",
RequestedBy: "agent:oncall-helper",
},
Plan: unifiedresources.ActionPlan{
ActionID: "act_dry_run_contract",
RequestID: "agent-run-dry-run",
Allowed: true,
RequiresApproval: false,
ApprovalPolicy: unifiedresources.ApprovalDryRun,
RollbackAvailable: false,
PlannedAt: now.Add(-time.Minute),
ExpiresAt: now.Add(4 * time.Minute),
ResourceVersion: "resource:sha256:dry-run-contract",
PolicyVersion: "policy:sha256:dry-run-contract",
PlanHash: "sha256:dry-run-contract",
Preflight: &unifiedresources.ActionPreflight{
Target: "vm:42",
CurrentState: "web-42 is warning",
IntendedChange: "Dry-run only restart inspection",
DryRunAvailable: true,
DryRunSummary: "Provider advertised dry-run only; no execution is allowed.",
SafetyChecks: []string{
"Dry-run-only plans are not executable.",
},
VerificationSteps: []string{
"Review /api/audit/actions/act_dry_run_contract/events for lifecycle evidence.",
},
GeneratedAt: now.Add(-time.Minute),
},
},
}
if err := store.RecordActionAudit(record); err != nil {
t.Fatalf("record action audit: %v", err)
}
req := httptest.NewRequest(http.MethodPost, "/api/actions/act_dry_run_contract/execute", bytes.NewBufferString(`{}`))
req.SetPathValue("id", "act_dry_run_contract")
rec := httptest.NewRecorder()
h.HandleExecuteAction(rec, req)
if rec.Code != http.StatusConflict {
t.Fatalf("status = %d, want %d; body=%s", rec.Code, http.StatusConflict, rec.Body.String())
}
// The action endpoints now emit the agent-stable error
// envelope (slice 58 refactor): the stable code lives under
// the top-level `error` field and the human message lives
// under `message`. The previous APIError shape (code under
// `code`, message under `error`) is gone for these handlers
// because they joined the agent surface.
var envelope struct {
Error string `json:"error"`
Message string `json:"message"`
}
if err := json.Unmarshal(rec.Body.Bytes(), &envelope); err != nil {
t.Fatalf("decode dry-run execution error: %v", err)
}
payload := struct {
Status int `json:"status"`
ErrorKey string `json:"error"`
Message string `json:"message"`
}{
Status: rec.Code,
ErrorKey: envelope.Error,
Message: envelope.Message,
}
got, err := json.Marshal(payload)
if err != nil {
t.Fatalf("marshal dry-run execution error contract: %v", err)
}
const want = `{
"status":409,
"error":"action_dry_run_only",
"message":"Action plan is dry-run only and cannot be executed"
}`
assertJSONSnapshot(t, got, want)
gotAudit, ok, err := store.GetActionAudit("act_dry_run_contract")
if err != nil {
t.Fatalf("GetActionAudit: %v", err)
}
if !ok || gotAudit.State != unifiedresources.ActionStateFailed || gotAudit.Result == nil || gotAudit.Result.Success || !strings.HasPrefix(gotAudit.Result.ErrorMessage, "action_dry_run_only:") {
t.Fatalf("dry-run audit mutated: ok=%v state=%q result=%#v", ok, gotAudit.State, gotAudit.Result)
}
events, err := store.GetActionLifecycleEvents("act_dry_run_contract", time.Time{}, 10)
if err != nil {
t.Fatalf("GetActionLifecycleEvents: %v", err)
}
if len(events) != 1 || events[0].State != unifiedresources.ActionStateFailed || !strings.HasPrefix(events[0].Message, "action_dry_run_only:") {
t.Fatalf("dry-run execution should append one failed lifecycle event, got %#v", events)
}
}
func TestContract_APIActionExecutionRevalidatesPlanFreshness(t *testing.T) {
source, err := os.ReadFile("actions.go")
if err != nil {
t.Fatalf("read actions.go: %v", err)
}
src := string(source)
for _, snippet := range []string{
"if unified.IsPermanentActionExecutionRefusal(err)",
"if err := h.validateActionPlanFresh(orgID, record); err != nil",
"errors.Is(err, unified.ErrActionPlanDrift)",
"recordRefusedActionExecution(store, record, actor, now, err)",
"unified.RefuseActionExecution(record, reason, actor, now)",
"writeJSONError(w, http.StatusConflict, \"action_plan_drift\"",
} {
if !strings.Contains(src, snippet) {
t.Fatalf("actions.go must pin API execute plan freshness guard snippet %q", snippet)
}
}
if strings.Index(src, "if err := h.validateActionPlanFresh(orgID, record); err != nil") >
strings.Index(src, "started, startEvent, err := unified.BeginActionExecution(record, actor, now)") {
t.Fatal("HandleExecuteAction must validate plan freshness before entering executing state or calling the executor")
}
}
func TestContract_ExecuteActionCapabilityDeclaresPlanExpired(t *testing.T) {
source, err := os.ReadFile("agent_capabilities.go")
if err != nil {
t.Fatalf("read agent_capabilities.go: %v", err)
}
src := string(source)
start := strings.Index(src, `Name: "execute_action"`)
if start < 0 {
t.Fatal("agent capabilities manifest must declare execute_action")
}
block := src[start:]
if end := strings.Index(block, "\n\t\t},"); end >= 0 {
block = block[:end]
}
if !strings.Contains(block, `"action_plan_expired"`) {
t.Error("execute_action manifest must declare action_plan_expired so agents can branch on permanent expired-plan refusals")
}
}
func TestContract_ResourceTimelineEndpointsIncludeRelatedChanges(t *testing.T) {
now := time.Date(2026, 4, 25, 22, 15, 0, 0, time.UTC)
h := NewResourceHandlers(&config.Config{DataPath: t.TempDir()})
h.SetStateProvider(resourceUnifiedSeedProvider{
snapshot: models.StateSnapshot{LastUpdate: now},
resources: []unifiedresources.Resource{
{
ID: "node-contract-relationship",
Type: unifiedresources.ResourceTypeAgent,
Name: "node-contract-relationship",
Status: unifiedresources.StatusOnline,
LastSeen: now,
},
{
ID: "vm-contract-relationship",
Type: unifiedresources.ResourceTypeVM,
Name: "vm-contract-relationship",
Status: unifiedresources.StatusOnline,
LastSeen: now,
},
},
})
store, err := h.getStore("default")
if err != nil {
t.Fatalf("get resource store: %v", err)
}
for _, change := range []unifiedresources.ResourceChange{
{
ID: "change-related-contract",
ResourceID: "vm-contract-relationship",
ObservedAt: now,
Kind: unifiedresources.ChangeRestart,
SourceType: unifiedresources.SourcePlatformEvent,
SourceAdapter: unifiedresources.AdapterProxmox,
Confidence: unifiedresources.ConfidenceHigh,
RelatedResources: []string{" node-contract-relationship "},
},
{
ID: "change-direct-contract",
ResourceID: "node-contract-relationship",
ObservedAt: now.Add(-time.Minute),
Kind: unifiedresources.ChangeStateTransition,
SourceType: unifiedresources.SourcePulseDiff,
SourceAdapter: unifiedresources.AdapterProxmox,
Confidence: unifiedresources.ConfidenceMedium,
},
} {
if err := store.RecordChange(change); err != nil {
t.Fatalf("record %s: %v", change.ID, err)
}
}
timelineRec := httptest.NewRecorder()
timelineReq := httptest.NewRequest(http.MethodGet, "/api/resources/node-contract-relationship/timeline?limit=10", nil)
h.HandleResourceRoutes(timelineRec, timelineReq)
if timelineRec.Code != http.StatusOK {
t.Fatalf("timeline status = %d, body=%s", timelineRec.Code, timelineRec.Body.String())
}
var timeline struct {
ResourceID string `json:"resourceId"`
RecentChanges []unifiedresources.ResourceChange `json:"recentChanges"`
Count int `json:"count"`
}
if err := json.NewDecoder(timelineRec.Body).Decode(&timeline); err != nil {
t.Fatalf("decode relationship-aware timeline: %v", err)
}
if timeline.ResourceID != "node-contract-relationship" || timeline.Count != 2 || len(timeline.RecentChanges) != 2 {
t.Fatalf("unexpected relationship-aware timeline: %#v", timeline)
}
if timeline.RecentChanges[0].ID != "change-related-contract" || timeline.RecentChanges[0].ResourceID != "vm-contract-relationship" {
t.Fatalf("timeline did not preserve related originating resource: %#v", timeline.RecentChanges)
}
facetsRec := httptest.NewRecorder()
facetsReq := httptest.NewRequest(http.MethodGet, "/api/resources/node-contract-relationship/facets?kind=restart&limit=10", nil)
h.HandleResourceRoutes(facetsRec, facetsReq)
if facetsRec.Code != http.StatusOK {
t.Fatalf("facets status = %d, body=%s", facetsRec.Code, facetsRec.Body.String())
}
var facets struct {
ResourceID string `json:"resourceId"`
RecentChanges []unifiedresources.ResourceChange `json:"recentChanges"`
Counts struct {
RecentChanges int `json:"recentChanges"`
RecentChangeKinds map[unifiedresources.ChangeKind]int `json:"recentChangeKinds"`
RecentAdapters map[unifiedresources.ChangeSourceAdapter]int `json:"recentChangeSourceAdapters"`
} `json:"counts"`
}
if err := json.NewDecoder(facetsRec.Body).Decode(&facets); err != nil {
t.Fatalf("decode relationship-aware facets: %v", err)
}
if facets.ResourceID != "node-contract-relationship" || facets.Counts.RecentChanges != 1 || len(facets.RecentChanges) != 1 {
t.Fatalf("unexpected relationship-aware facets: %#v", facets)
}
if facets.RecentChanges[0].ID != "change-related-contract" {
t.Fatalf("facets did not include related restart: %#v", facets.RecentChanges)
}
if got := facets.Counts.RecentChangeKinds[unifiedresources.ChangeRestart]; got != 1 {
t.Fatalf("restart facet count = %d, want 1", got)
}
if got := facets.Counts.RecentAdapters[unifiedresources.AdapterProxmox]; got != 1 {
t.Fatalf("adapter facet count = %d, want 1", got)
}
}
func TestContract_ResourceTimelineRejectsInvalidSourceAdapter(t *testing.T) {
_, err := unifiedresources.ParseResourceChangeFilters(nil, nil, []string{"unsupported_adapter"})
if err == nil {
t.Fatal("expected invalid sourceAdapter to be rejected")
}
}
func TestContract_UnifiedActionAuditsJSONSnapshot(t *testing.T) {
now := time.Date(2026, 3, 18, 16, 0, 0, 0, time.UTC)
payload := struct {
Audits []unifiedresources.ActionAuditRecord `json:"audits"`
Count int `json:"count"`
ResourceID string `json:"resourceId,omitempty"`
}{
Audits: []unifiedresources.ActionAuditRecord{
{
ID: "action-1",
CreatedAt: now,
UpdatedAt: now,
State: unifiedresources.ActionStateCompleted,
Request: unifiedresources.ActionRequest{
RequestID: "req-1",
ResourceID: "vm:42",
CapabilityName: "restart",
Reason: "maintenance",
RequestedBy: "agent:ops",
},
Plan: unifiedresources.ActionPlan{
ActionID: "action-1",
RequestID: "req-1",
Allowed: true,
RequiresApproval: false,
ApprovalPolicy: unifiedresources.ApprovalNone,
RollbackAvailable: false,
PlannedAt: now,
ExpiresAt: now.Add(5 * time.Minute),
ResourceVersion: "rv-1",
PolicyVersion: "pv-1",
PlanHash: "hash-1",
Preflight: &unifiedresources.ActionPreflight{
Target: "vm:42",
CurrentState: "online",
IntendedChange: "restart",
DryRunAvailable: false,
DryRunSummary: "No provider-supported dry run is available for this action.",
SafetyChecks: []string{"Approval and execution are scoped to vm:42."},
VerificationSteps: []string{"Read back VM status after execution."},
GeneratedAt: now,
},
},
Approvals: []unifiedresources.ActionApprovalRecord{
{
Actor: "admin@example.com",
Method: unifiedresources.MethodUI,
Timestamp: now.Add(time.Minute),
Outcome: unifiedresources.OutcomeApproved,
Reason: "approved",
},
},
Result: &unifiedresources.ExecutionResult{
Success: true,
Output: "done",
},
VerificationOutcome: unifiedresources.VerificationOutcome{
Status: unifiedresources.VerificationUnknown,
},
},
},
Count: 1,
ResourceID: "vm:42",
}
got, err := json.Marshal(payload)
if err != nil {
t.Fatalf("marshal action audits response: %v", err)
}
const want = `{
"audits":[
{
"id":"action-1",
"createdAt":"2026-03-18T16:00:00Z",
"updatedAt":"2026-03-18T16:00:00Z",
"state":"completed",
"request":{
"requestId":"req-1",
"resourceId":"vm:42",
"capabilityName":"restart",
"reason":"maintenance",
"requestedBy":"agent:ops"
},
"plan":{
"actionId":"action-1",
"requestId":"req-1",
"allowed":true,
"requiresApproval":false,
"approvalPolicy":"none",
"rollbackAvailable":false,
"plannedAt":"2026-03-18T16:00:00Z",
"expiresAt":"2026-03-18T16:05:00Z",
"resourceVersion":"rv-1",
"policyVersion":"pv-1",
"planHash":"hash-1",
"preflight":{
"target":"vm:42",
"currentState":"online",
"intendedChange":"restart",
"dryRunAvailable":false,
"dryRunSummary":"No provider-supported dry run is available for this action.",
"safetyChecks":["Approval and execution are scoped to vm:42."],
"verificationSteps":["Read back VM status after execution."],
"generatedAt":"2026-03-18T16:00:00Z"
}
},
"approvals":[
{
"actor":"admin@example.com",
"method":"ui",
"timestamp":"2026-03-18T16:01:00Z",
"outcome":"approved",
"reason":"approved"
}
],
"result":{
"success":true,
"output":"done"
},
"verificationOutcome":{
"status":"unknown"
}
}
],
"count":1,
"resourceId":"vm:42"
}`
assertJSONSnapshot(t, got, want)
}
func TestContract_UnifiedActionLifecycleEventsJSONSnapshot(t *testing.T) {
now := time.Date(2026, 3, 18, 16, 0, 0, 0, time.UTC)
payload := struct {
ActionID string `json:"actionId"`
Events []unifiedresources.ActionLifecycleEvent `json:"events"`
Count int `json:"count"`
}{
ActionID: "action-1",
Events: []unifiedresources.ActionLifecycleEvent{
{
ActionID: "action-1",
Timestamp: now,
State: unifiedresources.ActionStatePlanned,
Actor: "system",
Message: "planned",
},
},
Count: 1,
}
got, err := json.Marshal(payload)
if err != nil {
t.Fatalf("marshal action lifecycle response: %v", err)
}
const want = `{
"actionId":"action-1",
"events":[
{
"actionId":"action-1",
"timestamp":"2026-03-18T16:00:00Z",
"state":"planned",
"actor":"system",
"message":"planned"
}
],
"count":1
}`
assertJSONSnapshot(t, got, want)
}
func TestContract_UnifiedExportAuditsJSONSnapshot(t *testing.T) {
now := time.Date(2026, 3, 18, 16, 0, 0, 0, time.UTC)
payload := struct {
Audits []unifiedresources.ExportAuditRecord `json:"audits"`
Count int `json:"count"`
}{
Audits: []unifiedresources.ExportAuditRecord{
{
ID: "export-1",
Timestamp: now,
Actor: "agent:ops",
EnvelopeHash: "hash-1",
Decision: unifiedresources.ExportRedacted,
Destination: "local-llama",
Redactions: []string{"metadata.hostname"},
},
},
Count: 1,
}
got, err := json.Marshal(payload)
if err != nil {
t.Fatalf("marshal export audits response: %v", err)
}
const want = `{
"audits":[
{
"id":"export-1",
"timestamp":"2026-03-18T16:00:00Z",
"actor":"agent:ops",
"envelopeHash":"hash-1",
"decision":"redacted",
"destination":"local-llama",
"redactions":["metadata.hostname"]
}
],
"count":1
}`
assertJSONSnapshot(t, got, want)
}
func TestContract_UnifiedAuditLimitCapsOversizedRequests(t *testing.T) {
if got := parseAuditLimit("5000", 100); got != 1000 {
t.Fatalf("parseAuditLimit oversized request = %d, want 1000", got)
}
if got := parseAuditLimit("250", 100); got != 250 {
t.Fatalf("parseAuditLimit normal request = %d, want 250", got)
}
}
func TestContract_EmbeddedFrontendWarningUsesCanonicalDevEntrypoints(t *testing.T) {
path := filepath.Join("DO_NOT_EDIT_FRONTEND_HERE.md")
body, err := os.ReadFile(path)
if err != nil {
t.Fatalf("read embedded frontend warning: %v", err)
}
text := string(body)
if !strings.Contains(text, "http://127.0.0.1:5173") {
t.Fatalf("embedded frontend warning must point to the frontend dev shell on 5173")
}
if !strings.Contains(text, "http://127.0.0.1:7655") {
t.Fatalf("embedded frontend warning must identify the backend on 7655")
}
if strings.Contains(text, "The dev server (port 7655) will hot-reload") {
t.Fatalf("embedded frontend warning must not describe 7655 as the hot-reload dev server")
}
}
func TestContract_ShippedSecurityDocReferencesStayLocal(t *testing.T) {
if shippedSecurityDocPath != "/docs/SECURITY.md" {
t.Fatalf("expected shipped security doc path, got %q", shippedSecurityDocPath)
}
if shippedSecurityContainerNoticeDocAnchor != "/docs/SECURITY.md#critical-security-notice-for-container-deployments" {
t.Fatalf("expected shipped security container notice path, got %q", shippedSecurityContainerNoticeDocAnchor)
}
}
// TestContract_ConnectionPayloadShapeStaysCanonical pins the JSON shape of
// the unified connections ledger so the Go types in
// `internal/api/connections_types.go` stay in lockstep with the
// `frontend-modern/src/api/connections.ts` client. Adding or renaming fields
// on this wire shape requires an explicit update to both ends in the same
// commit.
func TestContract_ConnectionPayloadShapeStaysCanonical(t *testing.T) {
conn := Connection{
ID: "pve-lab",
Type: ConnectionTypePVE,
Name: "lab",
Address: "https://pve.lab:8006",
State: ConnectionStateActive,
StateReason: "",
Enabled: true,
Surfaces: []string{"vms", "containers"},
Scope: map[string]bool{"vms": true, "containers": true},
LastSeen: timePtr(time.Date(2026, 4, 19, 10, 0, 0, 0, time.UTC)),
LastError: nil,
Source: ConnectionSourceManual,
Fleet: ConnectionFleetGovernance{
EnrollmentState: fleetStateConfigured,
LivenessState: fleetStateActive,
VersionDrift: fleetStateNotApplicable,
AdapterHealth: fleetStateHealthy,
ConfigRollout: fleetStateConfigured,
CredentialStatus: fleetStateVerified,
UpdateStatus: fleetStateNotApplicable,
RemoteControl: fleetStateNotApplicable,
},
Capabilities: ConnectionCapabilities{SupportsPause: true, SupportsScope: true, SupportsTest: true},
}
system := ConnectionSystem{
ID: "pve-lab",
Type: ConnectionTypePVE,
ClusterName: "homelab",
Components: []ConnectionSystemComponent{
{
ConnectionID: "pve-lab",
Type: ConnectionTypePVE,
Role: ConnectionSystemComponentRolePrimary,
},
},
}
body, err := json.Marshal(ConnectionsListResponse{
Connections: []Connection{conn},
Systems: []ConnectionSystem{system},
})
if err != nil {
t.Fatalf("marshal Connection: %v", err)
}
want := `{"connections":[{"id":"pve-lab","type":"pve","name":"lab","address":"https://pve.lab:8006","state":"active","enabled":true,"surfaces":["vms","containers"],"scope":{"containers":true,"vms":true},"lastSeen":"2026-04-19T10:00:00Z","source":"manual","fleet":{"enrollmentState":"configured","livenessState":"active","versionDrift":"not-applicable","adapterHealth":"healthy","configRollout":"configured","credentialStatus":"verified","updateStatus":"not-applicable","remoteControl":"not-applicable"},"capabilities":{"supportsPause":true,"supportsScope":true,"supportsTest":true}}],"systems":[{"id":"pve-lab","type":"pve","clusterName":"homelab","components":[{"connectionId":"pve-lab","type":"pve","role":"primary"}]}]}`
assertJSONSnapshot(t, body, want)
}
func TestContract_ConnectionSystemMembersPayloadShapeStaysCanonical(t *testing.T) {
system := ConnectionSystem{
ID: "pve-lab",
Type: ConnectionTypePVE,
ClusterName: "homelab",
Components: []ConnectionSystemComponent{
{
ConnectionID: "pve-lab",
Type: ConnectionTypePVE,
Role: ConnectionSystemComponentRolePrimary,
},
{
ConnectionID: "agent:agent-lab",
Type: ConnectionTypeAgent,
Role: ConnectionSystemComponentRoleAttachment,
},
},
Members: []ConnectionSystemMember{
{
ID: "node-lab",
Name: "lab",
Endpoint: "https://lab:8006",
HostAliases: []string{"lab", "192.168.0.2"},
State: ConnectionStateActive,
LastSeen: timePtr(time.Date(2026, 4, 23, 12, 0, 0, 0, time.UTC)),
Primary: true,
AgentConnectionID: "agent:agent-lab",
},
{
ID: "node-minipc",
Name: "minipc",
Endpoint: "https://minipc:8006",
HostAliases: []string{"minipc"},
State: ConnectionStateStale,
LastSeen: timePtr(time.Date(2026, 4, 23, 11, 55, 0, 0, time.UTC)),
},
},
}
body, err := json.Marshal(system)
if err != nil {
t.Fatalf("marshal ConnectionSystem with members: %v", err)
}
want := `{"id":"pve-lab","type":"pve","clusterName":"homelab","components":[{"connectionId":"pve-lab","type":"pve","role":"primary"},{"connectionId":"agent:agent-lab","type":"agent","role":"attachment"}],"members":[{"id":"node-lab","name":"lab","endpoint":"https://lab:8006","hostAliases":["lab","192.168.0.2"],"state":"active","lastSeen":"2026-04-23T12:00:00Z","primary":true,"agentConnectionId":"agent:agent-lab"},{"id":"node-minipc","name":"minipc","endpoint":"https://minipc:8006","hostAliases":["minipc"],"state":"stale","lastSeen":"2026-04-23T11:55:00Z"}]}`
assertJSONSnapshot(t, body, want)
}
func TestContract_ConnectionSystemHostAgentAttachmentPayloadShapeStaysCanonical(t *testing.T) {
response := ConnectionsListResponse{
Connections: []Connection{
{
ID: "pve:delly",
Type: ConnectionTypePVE,
Name: "delly",
Address: "https://delly:8006",
HostAliases: []string{"delly"},
State: ConnectionStateUnauthorized,
Enabled: true,
Surfaces: []string{"vms", "containers", "storage", "backups"},
Scope: map[string]bool{"vms": true, "containers": true, "storage": true, "backups": true},
Source: ConnectionSourceAgent,
Fleet: ConnectionFleetGovernance{
EnrollmentState: fleetStateConfigured,
LivenessState: string(ConnectionStateUnauthorized),
VersionDrift: fleetStateNotApplicable,
AdapterHealth: fleetStateBlocked,
ConfigRollout: fleetStateConfigured,
CredentialStatus: fleetStateInvalid,
UpdateStatus: fleetStateNotApplicable,
RemoteControl: fleetStateNotApplicable,
},
Capabilities: ConnectionCapabilities{SupportsPause: true, SupportsScope: true, SupportsTest: true},
},
{
ID: "agent:agent-delly",
Type: ConnectionTypeAgent,
Name: "delly",
Address: "delly",
HostAliases: []string{"delly", "192.168.0.5"},
State: ConnectionStateActive,
Enabled: true,
Surfaces: []string{"host"},
Scope: map[string]bool{"host": true},
LastSeen: timePtr(time.Date(2026, 5, 13, 23, 45, 0, 0, time.UTC)),
Source: ConnectionSourceAgent,
Fleet: ConnectionFleetGovernance{
EnrollmentState: fleetStateEnrolled,
LivenessState: fleetStateActive,
VersionDrift: fleetStateUnknown,
AdapterHealth: fleetStateHealthy,
ConfigRollout: fleetStateReported,
CredentialStatus: fleetStateVerified,
UpdateStatus: fleetStateUnknown,
RemoteControl: fleetStateDisabled,
},
Capabilities: ConnectionCapabilities{SupportsPause: false, SupportsScope: false, SupportsTest: false},
},
},
Systems: []ConnectionSystem{
{
ID: "pve:delly",
Type: ConnectionTypePVE,
Components: []ConnectionSystemComponent{
{
ConnectionID: "pve:delly",
Type: ConnectionTypePVE,
Role: ConnectionSystemComponentRolePrimary,
},
{
ConnectionID: "agent:agent-delly",
Type: ConnectionTypeAgent,
Role: ConnectionSystemComponentRoleAttachment,
},
},
},
},
}
body, err := json.Marshal(response)
if err != nil {
t.Fatalf("marshal ConnectionsListResponse with direct host agent attachment: %v", err)
}
want := `{"connections":[{"id":"pve:delly","type":"pve","name":"delly","address":"https://delly:8006","hostAliases":["delly"],"state":"unauthorized","enabled":true,"surfaces":["vms","containers","storage","backups"],"scope":{"backups":true,"containers":true,"storage":true,"vms":true},"source":"agent","fleet":{"enrollmentState":"configured","livenessState":"unauthorized","versionDrift":"not-applicable","adapterHealth":"blocked","configRollout":"configured","credentialStatus":"invalid","updateStatus":"not-applicable","remoteControl":"not-applicable"},"capabilities":{"supportsPause":true,"supportsScope":true,"supportsTest":true}},{"id":"agent:agent-delly","type":"agent","name":"delly","address":"delly","hostAliases":["delly","192.168.0.5"],"state":"active","enabled":true,"surfaces":["host"],"scope":{"host":true},"lastSeen":"2026-05-13T23:45:00Z","source":"agent","fleet":{"enrollmentState":"enrolled","livenessState":"active","versionDrift":"unknown","adapterHealth":"healthy","configRollout":"reported","credentialStatus":"verified","updateStatus":"unknown","remoteControl":"disabled"},"capabilities":{"supportsPause":false,"supportsScope":false,"supportsTest":false}}],"systems":[{"id":"pve:delly","type":"pve","components":[{"connectionId":"pve:delly","type":"pve","role":"primary"},{"connectionId":"agent:agent-delly","type":"agent","role":"attachment"}]}]}`
assertJSONSnapshot(t, body, want)
}
func TestContract_AgentConnectionPayloadIncludesVersionFields(t *testing.T) {
conn := Connection{
ID: "agent:host-1",
Type: ConnectionTypeAgent,
Name: "host-1",
Address: "host-1",
HostAliases: []string{"host-1", "192.168.0.2"},
State: ConnectionStateActive,
Enabled: true,
Surfaces: []string{"host"},
Scope: map[string]bool{"host": true},
LastSeen: timePtr(time.Date(2026, 4, 22, 12, 0, 0, 0, time.UTC)),
Source: ConnectionSourceAgent,
AgentIdentity: &ConnectionAgentIdentity{
Hostname: "host-1",
Platform: "linux",
HostProfile: "unraid",
OSName: "Unraid",
OSVersion: "7.1.0",
KernelVersion: "6.12.0",
Architecture: "x86_64",
ReportIP: "192.168.0.2",
CommandsEnabled: true,
},
AgentVersion: "6.0.0",
ExpectedAgentVersion: "6.0.2",
AgentUpdateAvailable: true,
Fleet: ConnectionFleetGovernance{
EnrollmentState: fleetStateEnrolled,
LivenessState: fleetStateActive,
VersionDrift: fleetStateBehind,
AdapterHealth: fleetStateHealthy,
ConfigRollout: fleetStateReported,
CredentialStatus: fleetStateVerified,
UpdateStatus: fleetStateUpdateAvailable,
RemoteControl: fleetStateEnabled,
CommandPolicy: &ConnectionFleetCommandPolicy{
Status: fleetStateEnabled,
Desired: fleetStateEnabled,
Applied: fleetStateEnabled,
Enforcement: fleetCommandPolicyInSync,
Reason: "agent command execution matches the desired enabled policy",
},
},
Capabilities: ConnectionCapabilities{
SupportsPause: false,
SupportsScope: false,
SupportsTest: false,
},
}
body, err := json.Marshal(conn)
if err != nil {
t.Fatalf("marshal agent Connection: %v", err)
}
if conn.AgentIdentity.Platform == conn.AgentIdentity.HostProfile {
t.Fatalf("agent platform must remain runtime platform, got profile id %q", conn.AgentIdentity.Platform)
}
want := `{"id":"agent:host-1","type":"agent","name":"host-1","address":"host-1","hostAliases":["host-1","192.168.0.2"],"state":"active","enabled":true,"surfaces":["host"],"scope":{"host":true},"lastSeen":"2026-04-22T12:00:00Z","source":"agent","agentIdentity":{"hostname":"host-1","platform":"linux","hostProfile":"unraid","osName":"Unraid","osVersion":"7.1.0","kernelVersion":"6.12.0","architecture":"x86_64","reportIp":"192.168.0.2","commandsEnabled":true},"agentVersion":"6.0.0","expectedAgentVersion":"6.0.2","agentUpdateAvailable":true,"fleet":{"enrollmentState":"enrolled","livenessState":"active","versionDrift":"behind","adapterHealth":"healthy","configRollout":"reported","credentialStatus":"verified","updateStatus":"update-available","remoteControl":"enabled","commandPolicy":{"status":"enabled","desired":"enabled","applied":"enabled","enforcement":"in-sync","reason":"agent command execution matches the desired enabled policy"}},"capabilities":{"supportsPause":false,"supportsScope":false,"supportsTest":false}}`
assertJSONSnapshot(t, body, want)
}
func TestContract_AgentDefaultDesiredConfigDoesNotCreateRolloutAttention(t *testing.T) {
now := time.Date(2026, 5, 14, 10, 30, 0, 0, time.UTC)
connections := buildConnections(aggregatorInputs{
hosts: []models.Host{
{
ID: "host-1",
Hostname: "host-1",
ReportIP: "192.0.2.42",
LastSeen: now.Add(-10 * time.Second),
AgentVersion: "6.0.0",
Platform: "linux",
CommandsEnabled: false,
TokenID: "token-1",
},
},
agentDesiredConfigs: map[string]connectionAgentDesiredConfig{
"host-1": {},
},
expectedAgentVersion: "6.0.0",
now: now,
})
if len(connections) != 1 {
t.Fatalf("expected one agent connection, got %d", len(connections))
}
body, err := json.Marshal(connections[0])
if err != nil {
t.Fatalf("marshal agent Connection: %v", err)
}
want := `{"id":"agent:host-1","type":"agent","name":"host-1","address":"host-1","hostAliases":["host-1","192.0.2.42"],"state":"active","enabled":true,"surfaces":["host"],"scope":{"host":true},"lastSeen":"2026-05-14T10:29:50Z","source":"agent","agentIdentity":{"hostname":"host-1","platform":"linux","reportIp":"192.0.2.42"},"agentVersion":"6.0.0","expectedAgentVersion":"6.0.0","fleet":{"enrollmentState":"enrolled","livenessState":"active","versionDrift":"current","adapterHealth":"healthy","configRollout":"reported","credentialStatus":"verified","updateStatus":"current","remoteControl":"disabled","configDrift":{"status":"not-applicable","lastObservedAt":"2026-05-14T10:29:50Z","reason":"no managed agent configuration override is assigned"},"rollout":{"status":"current","stage":"applied","reason":"no managed agent configuration rollout is assigned"},"credentialHealth":{"status":"verified","kind":"agent-token","rotation":"healthy","lastVerifiedAt":"2026-05-14T10:29:50Z"},"commandPolicy":{"status":"disabled","desired":"unknown","applied":"disabled","enforcement":"not-applicable","reason":"no desired command-policy override is configured; reporting the agent-applied state"}},"capabilities":{"supportsPause":false,"supportsScope":false,"supportsTest":false}}`
assertJSONSnapshot(t, body, want)
}
func TestContract_ConnectionsListIncludesAgentHostsFromUnifiedReadState(t *testing.T) {
cfg := &config.Config{DataPath: t.TempDir()}
monitor, err := monitoring.New(cfg)
if err != nil {
t.Fatalf("monitoring.New: %v", err)
}
t.Cleanup(func() { monitor.Stop() })
adapter := unifiedresources.NewMonitorAdapter(nil)
setTestUnexportedField(t, monitor, "resourceStore", monitoring.ResourceStoreInterface(adapter))
now := time.Now().UTC()
adapter.PopulateSupplementalRecords(unifiedresources.SourceAgent, []unifiedresources.IngestRecord{
{
SourceID: "agent-1",
Resource: unifiedresources.Resource{
ID: "resource-host-1",
Type: unifiedresources.ResourceTypeAgent,
Name: "mini-pc",
Status: unifiedresources.StatusOnline,
LastSeen: now,
Agent: &unifiedresources.AgentData{
AgentID: "agent-1",
Hostname: "mini-pc",
MachineID: "machine-1",
},
Identity: unifiedresources.ResourceIdentity{
MachineID: "machine-1",
Hostnames: []string{"mini-pc"},
},
},
},
})
if got := len(monitorState(t, monitor).GetHosts()); got != 0 {
t.Fatalf("expected legacy host snapshot to stay empty, got %d hosts", got)
}
if got := len(monitor.GetUnifiedReadStateOrSnapshot().Hosts()); got != 1 {
t.Fatalf("expected unified read-state to expose 1 agent host, got %d", got)
}
handler := NewConnectionsHandlers(
func(context.Context) *config.Config { return cfg },
func(context.Context) *config.ConfigPersistence { return nil },
func(context.Context) *monitoring.Monitor { return monitor },
)
req := httptest.NewRequest(http.MethodGet, "/api/connections", nil)
rec := httptest.NewRecorder()
handler.HandleList(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status = %d, body=%s", rec.Code, rec.Body.String())
}
var resp ConnectionsListResponse
if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil {
t.Fatalf("decode connections response: %v", err)
}
if len(resp.Connections) != 1 {
t.Fatalf("expected 1 connection from unified read-state, got %d", len(resp.Connections))
}
if len(resp.Systems) != 1 {
t.Fatalf("expected 1 grouped system from unified read-state, got %d", len(resp.Systems))
}
conn := resp.Connections[0]
if conn.ID != "agent:agent-1" {
t.Fatalf("connection id = %q, want %q", conn.ID, "agent:agent-1")
}
if conn.Type != ConnectionTypeAgent {
t.Fatalf("connection type = %q, want %q", conn.Type, ConnectionTypeAgent)
}
if conn.Name != "mini-pc" || conn.Address != "mini-pc" {
t.Fatalf("unexpected agent identity: name=%q address=%q", conn.Name, conn.Address)
}
if conn.State != ConnectionStateActive {
t.Fatalf("connection state = %q, want %q", conn.State, ConnectionStateActive)
}
if conn.Source != ConnectionSourceAgent {
t.Fatalf("connection source = %q, want %q", conn.Source, ConnectionSourceAgent)
}
if resp.Systems[0].ID != "agent:agent-1" || resp.Systems[0].Type != ConnectionTypeAgent {
t.Fatalf("unexpected grouped system projection: %+v", resp.Systems[0])
}
}
// TestContract_ProbePayloadShapeStaysCanonical pins the POST
// /api/connections/probe wire shape. Hint keys are free-form; the envelope
// fields (type, host, port, hints) are the contract boundary.
func TestContract_ProbePayloadShapeStaysCanonical(t *testing.T) {
resp := ProbeResponse{
Candidates: []ProbeCandidate{
{
Type: ConnectionTypePVE,
Host: "https://pve.lab:8006",
Port: 8006,
Hints: map[string]string{"product": "Proxmox VE", "version": "8.2.4"},
},
},
ProbedMs: 812,
}
body, err := json.Marshal(resp)
if err != nil {
t.Fatalf("marshal ProbeResponse: %v", err)
}
want := `{"candidates":[{"type":"pve","host":"https://pve.lab:8006","port":8006,"hints":{"product":"Proxmox VE","version":"8.2.4"}}],"probedMs":812}`
assertJSONSnapshot(t, body, want)
}
func TestContract_ProbeRejectsBlockedMetadataAddress(t *testing.T) {
handler := NewConnectionsHandlers(nil, nil, nil)
req := httptest.NewRequest(http.MethodPost, "/api/connections/probe", strings.NewReader(`{"address":"169.254.169.254"}`))
req.Header.Set("Content-Type", "application/json")
rec := httptest.NewRecorder()
handler.HandleProbe(rec, req)
if rec.Code != http.StatusBadRequest {
t.Fatalf("status = %d, want %d (%s)", rec.Code, http.StatusBadRequest, rec.Body.String())
}
var payload APIError
if err := json.NewDecoder(rec.Body).Decode(&payload); err != nil {
t.Fatalf("decode error payload: %v", err)
}
if payload.Code != "invalid_address" {
t.Fatalf("code = %q, want %q", payload.Code, "invalid_address")
}
if !strings.Contains(payload.ErrorMessage, "metadata service address is not allowed") {
t.Fatalf("error = %q, want metadata-service rejection", payload.ErrorMessage)
}
}
func TestContract_SimpleStatsUsesTextNodesForContainerFields(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/simple-stats", nil)
rec := httptest.NewRecorder()
var router Router
router.handleSimpleStats(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status = %d, want %d", rec.Code, http.StatusOK)
}
body := rec.Body.String()
if strings.Contains(body, "row.innerHTML") {
t.Fatalf("simple stats page must not use row.innerHTML for container rendering")
}
if !strings.Contains(body, "textContent") {
t.Fatalf("simple stats page must use textContent for container rendering")
}
}
func TestContract_APIRejectsUnsafeIncomingRequestIDHeader(t *testing.T) {
handler := ErrorHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNoContent)
}))
req := httptest.NewRequest(http.MethodGet, "/api/health", nil)
req.Header.Set("X-Request-ID", "bad\nrequest-id")
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
got := rec.Header().Get("X-Request-ID")
if got == "" {
t.Fatal("expected generated request id")
}
if got == "bad\nrequest-id" {
t.Fatalf("unsafe request id header must not round-trip: %q", got)
}
if strings.ContainsAny(got, "\r\n") {
t.Fatalf("response request id must not contain control characters: %q", got)
}
}
func TestContract_BootstrapTokenValidationRateLimitsPerClient(t *testing.T) {
dataDir := t.TempDir()
router := &Router{
config: &config.Config{
DataPath: dataDir,
ConfigPath: dataDir,
},
bootstrapTokenValidationLimiter: NewRateLimiter(1, time.Hour),
}
t.Cleanup(router.bootstrapTokenValidationLimiter.Stop)
router.initializeBootstrapToken()
req := httptest.NewRequest(http.MethodPost, "/api/security/validate-bootstrap-token", strings.NewReader(`{"token":"deadbeef"}`))
req.RemoteAddr = "127.0.0.1:1234"
rec := httptest.NewRecorder()
router.handleValidateBootstrapToken(rec, req)
if rec.Code != http.StatusUnauthorized {
t.Fatalf("status = %d, want %d", rec.Code, http.StatusUnauthorized)
}
req = httptest.NewRequest(http.MethodPost, "/api/security/validate-bootstrap-token", strings.NewReader(`{"token":"deadbeef"}`))
req.RemoteAddr = "127.0.0.1:1234"
rec = httptest.NewRecorder()
router.handleValidateBootstrapToken(rec, req)
if rec.Code != http.StatusTooManyRequests {
t.Fatalf("status = %d, want %d (%s)", rec.Code, http.StatusTooManyRequests, rec.Body.String())
}
if retryAfter := rec.Header().Get("Retry-After"); retryAfter == "" {
t.Fatal("expected Retry-After header on bootstrap token validation rate limit")
}
}
func TestContract_DeployHandlersDoNotSurfaceLicenseSlotCapacityCopy(t *testing.T) {
source, err := os.ReadFile("deploy_handlers.go")
if err != nil {
t.Fatalf("read deploy handlers: %v", err)
}
text := string(source)
if strings.Contains(text, "No license slots available") {
t.Fatal("deploy retry capacity denial must not surface legacy license-slot copy")
}
if strings.Contains(text, "reservedLicenseSlots") {
t.Fatal("deploy response must not expose retired reservedLicenseSlots field")
}
}
func mustStreamEvent(t *testing.T, eventType string, data interface{}) chat.StreamEvent {
t.Helper()
raw, err := json.Marshal(data)
if err != nil {
t.Fatalf("marshal stream data: %v", err)
}
return chat.StreamEvent{
Type: eventType,
Data: raw,
}
}
func assertJSONSnapshot(t *testing.T, got []byte, want string, excludeKeys ...string) {
t.Helper()
// excludeKeys lets callers strip top-level fields whose values are
// dynamic per run (timestamps, durations) before snapshot comparison.
// The structural shape of the response is still validated; only the
// listed keys are removed from both `got` and `want`.
gotBytes := got
wantBytes := []byte(want)
if len(excludeKeys) > 0 {
var err error
gotBytes, err = stripJSONTopLevelKeys(got, excludeKeys)
if err != nil {
t.Fatalf("strip got json: %v", err)
}
wantBytes, err = stripJSONTopLevelKeys([]byte(want), excludeKeys)
if err != nil {
t.Fatalf("strip want json: %v", err)
}
}
var gotCompact bytes.Buffer
var wantCompact bytes.Buffer
if err := json.Compact(&gotCompact, gotBytes); err != nil {
t.Fatalf("compact got json: %v", err)
}
if err := json.Compact(&wantCompact, wantBytes); err != nil {
t.Fatalf("compact want json: %v", err)
}
if gotCompact.String() != wantCompact.String() {
t.Fatalf("json snapshot mismatch\nwant: %s\ngot: %s", wantCompact.String(), gotCompact.String())
}
}
// stripJSONTopLevelKeys removes the named top-level keys from a JSON object
// and re-serializes. Used by assertJSONSnapshot to ignore dynamic fields.
func stripJSONTopLevelKeys(data []byte, keys []string) ([]byte, error) {
var obj map[string]interface{}
if err := json.Unmarshal(data, &obj); err != nil {
return nil, err
}
for _, k := range keys {
delete(obj, k)
}
return json.Marshal(obj)
}
// TestContract_InvestigationProjectionCarriesNeverAutoRemediate pins
// the cross-path agreement: the projection-builder closure in
// router.go must populate NeverAutoRemediate on the projection so
// the investigation read path sees the same lock-against-remediation
// flag the action broker enforces. Without this, Patrol could
// propose fixes on locked resources that the broker would refuse —
// the fix proposal shouldn't have happened in the first place.
func TestContract_InvestigationProjectionCarriesNeverAutoRemediate(t *testing.T) {
source, err := os.ReadFile("router.go")
if err != nil {
t.Fatalf("read router.go: %v", err)
}
src := string(source)
if !strings.Contains(src, "NeverAutoRemediate: state.NeverAutoRemediate,") {
t.Error("router.go must populate NeverAutoRemediate on the projection so the investigation read path sees the operator's lock — drift here means Patrol can propose fixes the broker refuses")
}
}
// TestContract_AgentEventsStreamPublishesOnFindingCreated pins the
// agent SSE stream's most consequential producer hook: the
// findings-runtime callback in router.go must publish a
// finding.created event when a finding is new AND not auto-dismissed
// by operator-state suppression. Without this guard, every
// patrol-cycle re-detection would fire an event (noise) and findings
// the operator already silenced would still notify (contradicting
// slice 31/32 suppression semantics).
func TestContract_AgentEventsStreamPublishesOnFindingCreated(t *testing.T) {
source, err := os.ReadFile("router.go")
if err != nil {
t.Fatalf("read router.go: %v", err)
}
src := string(source)
if !strings.Contains(src, "PublishFindingCreated(AgentEventFindingCreatedPayload{") {
t.Error("router.go must publish finding.created events to the agent stream when a new finding is added")
}
if !strings.Contains(src, "isNew && r.agentEventBroadcaster != nil && f.DismissedReason == \"\"") {
t.Error("publish gate must fire only when isNew, broadcaster wired, and finding not auto-dismissed by operator-state — otherwise the stream contradicts the suppression contract")
}
}
// TestContract_AgentEventsStreamPublishesOnApprovalPending pins the
// agent SSE stream's governance-aware producer hook: the approval
// store's post-create callback must publish an approval.pending
// event so an agent holding /api/agent/events open hears about new
// pending approvals in real time without polling. The callback is
// installed via AIHandler.SetApprovalCreatedCallback so it survives
// the AIHandler's lazy approval-store rebuilds. Drift here means
// the only signal an agent has that an operator decision is needed
// is poll-based — losing the substrate's push-notification
// guarantee for governance state.
func TestContract_AgentEventsStreamPublishesOnApprovalPending(t *testing.T) {
source, err := os.ReadFile("router.go")
if err != nil {
t.Fatalf("read router.go: %v", err)
}
src := string(source)
if !strings.Contains(src, "r.aiHandler.SetApprovalCreatedCallback(") {
t.Error("router.go must install the approval-created callback on AIHandler so the agent SSE stream is wired before the first approval lands")
}
if !strings.Contains(src, "broadcaster.PublishApprovalPending(AgentEventApprovalPendingPayload{") {
t.Error("router.go must bridge approval creation into PublishApprovalPending — drift means agents lose the push-notification path for governance state")
}
if !strings.Contains(src, "ApprovalID: req.ID") {
t.Error("approval.pending payload must carry the approval id so agents can correlate the event with /api/approvals/{id}")
}
if !strings.Contains(src, "ResourceID: req.CanonicalResourceID()") {
t.Error("approval.pending payload must derive the canonical resourceId via CanonicalResourceID so agents can match it against the rest of Pulse")
}
}
// TestContract_ApprovalStorePostCreateCallback pins the seam the
// SSE bridge depends on. The approval store must expose a
// SetOnApprovalCreated setter and CreateApproval must dispatch the
// callback on its own goroutine after the approval is persisted.
// Removing or in-lining the callback breaks the bridge silently.
func TestContract_ApprovalStorePostCreateCallback(t *testing.T) {
source, err := os.ReadFile("../ai/approval/store.go")
if err != nil {
t.Fatalf("read approval/store.go: %v", err)
}
src := string(source)
if !strings.Contains(src, "func (s *Store) SetOnApprovalCreated(cb func(*ApprovalRequest))") {
t.Error("approval store must expose SetOnApprovalCreated so the API layer can bridge approval creation into the agent SSE stream")
}
if !strings.Contains(src, "if cb := s.onApprovalCreated; cb != nil {") {
t.Error("CreateApproval must check the post-create callback under lock and dispatch it; drift here drops approval.pending events")
}
if !strings.Contains(src, "go cb(&snapshot)") {
t.Error("post-create callback must run on its own goroutine to keep the approval hot path off any consumer's slowness")
}
}
// TestContract_AgentEventApprovalPendingKindIsStable pins the
// wire-stable event kind string. Renaming AgentEventApprovalPending
// breaks every agent that branches on the SSE event-type field; the
// constant is part of the contract, not an implementation detail.
func TestContract_AgentEventApprovalPendingKindIsStable(t *testing.T) {
source, err := os.ReadFile("agent_events.go")
if err != nil {
t.Fatalf("read agent_events.go: %v", err)
}
src := string(source)
if !strings.Contains(src, `AgentEventApprovalPending AgentEventKind = "approval.pending"`) {
t.Error("AgentEventApprovalPending must remain bound to the wire-stable string \"approval.pending\" — renaming it breaks every agent that branches on the SSE event type")
}
}
// TestContract_SubscribeEventsCapabilityListsApprovalPending pins
// the discovery contract: the capabilities manifest must mention
// approval.pending under subscribe_events so an agent reading the
// manifest learns the kind exists without out-of-band documentation.
// Drift here means external agents miss the event entirely because
// they didn't know to listen for it.
func TestContract_SubscribeEventsCapabilityListsApprovalPending(t *testing.T) {
source, err := os.ReadFile("agent_capabilities.go")
if err != nil {
t.Fatalf("read agent_capabilities.go: %v", err)
}
src := string(source)
if !strings.Contains(src, "approval.pending when a remediation request enters StatusPending") {
t.Error("subscribe_events description must mention approval.pending so agents discover the kind through the manifest")
}
}
// TestContract_AgentEventsHeartbeatIsStreamLocal pins that
// heartbeat keepalives are written to the connected SSE response
// rather than published through the broadcaster. Heartbeats are
// connection health signals, not product events; publishing them
// globally makes N subscribers create N heartbeat events that fan
// out to every other subscriber.
func TestContract_AgentEventsHeartbeatIsStreamLocal(t *testing.T) {
source, err := os.ReadFile("agent_events.go")
if err != nil {
t.Fatalf("read agent_events.go: %v", err)
}
src := string(source)
if !strings.Contains(src, "writeAgentSSEEvent(w, b.stampEvent(AgentEvent{Kind: AgentEventHeartbeat}))") {
t.Error("HandleAgentEvents must write heartbeat events directly to the connected response")
}
if strings.Contains(src, "case <-heartbeat.C:\n\t\t\tb.Publish(AgentEvent{Kind: AgentEventHeartbeat})") {
t.Error("HandleAgentEvents must not publish heartbeat events through the broadcaster")
}
}
// TestContract_AgentEventsStreamPublishesOnActionCompleted pins the
// agent SSE stream's third producer hook: the executor's
// post-completion callback must publish action.completed events for
// every terminal-state audit so an agent holding /api/agent/events
// open closes the dispatch loop without polling. The bridge lives in
// wireAIChatDependenciesForService so the callback is re-installed
// on every per-org chat-service init. Drift here means agents lose
// the push-notification path for dispatch outcomes — including the
// refused-before-dispatch failures that carry stable error tokens.
func TestContract_AgentEventsStreamPublishesOnActionCompleted(t *testing.T) {
source, err := os.ReadFile("router.go")
if err != nil {
t.Fatalf("read router.go: %v", err)
}
src := string(source)
if !strings.Contains(src, "executor.SetOnActionCompleted(func(record unifiedresources.ActionAuditRecord)") {
t.Error("router.go must install the post-completion callback on the executor returned from chatService.GetExecutor() so the agent SSE stream is wired per-org")
}
if !strings.Contains(src, "broadcaster.PublishActionCompletedRecord(record)") {
t.Error("router.go must bridge executor action completion into PublishActionCompletedRecord — drift means agents lose the push-notification path for dispatch outcomes")
}
if !strings.Contains(src, "r.resourceHandlers.SetActionCompletedPublisher(r.agentEventBroadcaster.PublishActionCompletedRecord)") {
t.Error("router.go must also bridge API-owned action execution completions into the agent SSE stream")
}
eventsSource, err := os.ReadFile("agent_events.go")
if err != nil {
t.Fatalf("read agent_events.go: %v", err)
}
eventsSrc := string(eventsSource)
if !strings.Contains(eventsSrc, "func (b *AgentEventBroadcaster) PublishActionCompletedRecord(record unifiedresources.ActionAuditRecord)") {
t.Error("agent_events.go must keep a shared action-audit-record projector for every action.completed producer")
}
if !strings.Contains(eventsSrc, "ActionID: record.ID") {
t.Error("action.completed payload must carry the canonical action id so agents can correlate the event with /api/actions/{id}")
}
if !strings.Contains(eventsSrc, "ResourceID: record.Request.ResourceID") {
t.Error("action.completed payload must carry the canonical resource id so agents can match it against the rest of Pulse")
}
}
// TestContract_ExecutorPostCompletionCallback pins the seam the SSE
// bridge depends on. The executor must expose SetOnActionCompleted
// and the action_audit hot path must dispatch the callback for every
// terminal-state record (success, runtime fail, plan-drift refusal,
// remediation-lock refusal). Removing or in-lining the callback
// breaks the bridge silently.
func TestContract_ExecutorPostCompletionCallback(t *testing.T) {
executor, err := os.ReadFile("../ai/tools/executor.go")
if err != nil {
t.Fatalf("read executor.go: %v", err)
}
executorSrc := string(executor)
if !strings.Contains(executorSrc, "func (e *PulseToolExecutor) SetOnActionCompleted(cb func(unifiedresources.ActionAuditRecord))") {
t.Error("executor must expose SetOnActionCompleted so the API layer can bridge action completion into the agent SSE stream")
}
audit, err := os.ReadFile("../ai/tools/action_audit.go")
if err != nil {
t.Fatalf("read action_audit.go: %v", err)
}
auditSrc := string(audit)
if !strings.Contains(auditSrc, "func (e *PulseToolExecutor) publishActionCompleted(record unifiedresources.ActionAuditRecord)") {
t.Error("action_audit.go must define publishActionCompleted helper so terminal-state writers route through one bridge point")
}
if !strings.Contains(auditSrc, "go cb(record)") {
t.Error("post-completion callback must run on its own goroutine to keep the dispatch hot path off any consumer's slowness")
}
// Pin that all four terminal sites call publishActionCompleted.
terminalSites := []string{
`e.recordActionLifecycle(record.ID, unifiedresources.ActionStateFailed, requestedBy, "plan drift refused")
e.publishActionCompleted(record)`,
`e.recordActionLifecycle(record.ID, unifiedresources.ActionStateFailed, requestedBy, "resource remediation lock refused")
e.publishActionCompleted(record)`,
`e.recordActionLifecycle(record.ID, record.State, actor, message)
e.publishActionCompleted(record)`,
}
for _, site := range terminalSites {
if !strings.Contains(auditSrc, site) {
t.Errorf("action_audit.go must call publishActionCompleted at every terminal-state site; missing block:\n%s", site)
}
}
}
// TestContract_AgentEventActionCompletedKindIsStable pins the
// wire-stable event kind string. Renaming AgentEventActionCompleted
// breaks every agent that branches on the SSE event-type field; the
// constant is part of the contract, not an implementation detail.
func TestContract_AgentEventActionCompletedKindIsStable(t *testing.T) {
source, err := os.ReadFile("agent_events.go")
if err != nil {
t.Fatalf("read agent_events.go: %v", err)
}
src := string(source)
if !strings.Contains(src, `AgentEventActionCompleted AgentEventKind = "action.completed"`) {
t.Error("AgentEventActionCompleted must remain bound to the wire-stable string \"action.completed\" — renaming it breaks every agent that branches on the SSE event type")
}
}
// TestContract_SubscribeEventsCapabilityListsActionCompleted pins
// the discovery contract: the capabilities manifest must mention
// action.completed under subscribe_events so an agent reading the
// manifest learns the kind exists without out-of-band documentation.
// Drift here means external agents miss the event entirely because
// they didn't know to listen for it.
func TestContract_SubscribeEventsCapabilityListsActionCompleted(t *testing.T) {
source, err := os.ReadFile("agent_capabilities.go")
if err != nil {
t.Fatalf("read agent_capabilities.go: %v", err)
}
src := string(source)
if !strings.Contains(src, "action.completed when an action audit reaches a terminal state") {
t.Error("subscribe_events description must mention action.completed so agents discover the kind through the manifest")
}
}
// TestContract_AgentCapabilitiesManifestVersionIsPinned pins the
// manifest's version contract: bumping it is reserved for breaking
// shape changes, additive capabilities ship under the same version.
// Pinning the version string here means a refactor that accidentally
// bumps it shows up as a test failure rather than silently breaking
// every external agent that validates compatibility on startup.
func TestContract_AgentCapabilitiesManifestVersionIsPinned(t *testing.T) {
source, err := os.ReadFile("agent_capabilities.go")
if err != nil {
t.Fatalf("read agent_capabilities.go: %v", err)
}
src := string(source)
if !strings.Contains(src, `Version: "v1"`) {
t.Error("agent capabilities manifest must pin Version to v1; bumping is a breaking-change decision and should not happen silently")
}
// Capability surface anchors — these names are agent-stable
// identifiers and renaming any of them breaks every external
// integration. Pin them here so renames must be deliberate.
required := []string{
"\"get_resource_context\"",
"\"get_operator_state\"",
"\"set_operator_state\"",
"\"clear_operator_state\"",
"\"list_findings\"",
}
for _, name := range required {
if !strings.Contains(src, name) {
t.Errorf("agent capabilities manifest must declare canonical capability %s", name)
}
}
}
// TestContract_AgentResourceContextEndpointSurfacesStableShape pins
// the agent-paradigm substrate contract: the bundled endpoint must
// return arrays (never null) for activeFindings and recentActions so
// agents can iterate without nil-checking, must omit operatorState
// when no entry exists so agents can branch on field presence, and
// must preserve refusal-token prefixes verbatim so agents can branch
// on the stable code without parsing human messages. Without this
// pin, future refactors of the projection helpers could silently
// drift the wire shape and break external agent consumers.
func TestContract_AgentResourceContextEndpointSurfacesStableShape(t *testing.T) {
source, err := os.ReadFile("agent_resource_context.go")
if err != nil {
t.Fatalf("read agent_resource_context.go: %v", err)
}
src := string(source)
// Always-array initialization for the iteration-safe contract.
if !strings.Contains(src, "ActiveFindings: []AgentResourceFindingSnapshot{},") {
t.Error("activeFindings must default to an empty slice, not nil — agents iterate without nil-checks")
}
if !strings.Contains(src, "RecentActions: []AgentResourceActionSummary{},") {
t.Error("recentActions must default to an empty slice, not nil")
}
// Field-presence branching contract for operatorState.
if !strings.Contains(src, "OperatorState *AgentResourceOperatorState `json:\"operatorState,omitempty\"`") {
t.Error("operatorState must be omitempty so absent entries surface as missing field")
}
// Server-computed maintenance-active flag — server pre-computes so
// agents don't re-evaluate timestamps client-side.
if !strings.Contains(src, "MaintenanceWindowActive: state.IsInMaintenanceAt(now)") {
t.Error("MaintenanceWindowActive must be computed server-side via state.IsInMaintenanceAt(now)")
}
// Refusal-token preservation: ErrorMessage flows through verbatim
// from the audit record's ExecutionResult, no rewriting.
if !strings.Contains(src, "summary.ErrorMessage = audit.Result.ErrorMessage") {
t.Error("audit ErrorMessage must round-trip verbatim so refusal tokens (resource_remediation_locked:, plan_drift:) reach agents")
}
// pendingApprovals must initialize as an empty slice so agents
// iterate without nil-checking — same iteration-safe contract
// as activeFindings and recentActions.
if !strings.Contains(src, "PendingApprovals: []AgentResourceApprovalSummary{}") {
t.Error("pendingApprovals must default to an empty slice, not nil — agents iterate without nil-checks")
}
// Pending approvals must reuse the AgentResourceApprovalSummary
// shape so the bundle and approval.pending SSE events stay in
// the same vocabulary.
if !strings.Contains(src, "PendingApprovals []AgentResourceApprovalSummary `json:\"pendingApprovals\"`") {
t.Error("AgentResourceContext must carry PendingApprovals as a stable []AgentResourceApprovalSummary field")
}
}
// TestContract_AgentResourceContextWiresApprovalsProvider pins the
// startup-time wire-up that lets the bundle endpoint surface pending
// approvals scoped to a resource. Drift here means the field always
// shows up as an empty array even when there ARE pending approvals,
// silently breaking the substrate's "everything an agent needs in
// one read" contract for the governance dimension.
//
// The wire-up has two halves: router.go installs the provider with
// a provider that delegates to named functions in
// agent_resource_context.go, and those functions apply the org and
// resource filters while supporting the fleet endpoint's bulk count
// projection. Both halves are pinned here so a refactor that moves
// the filter logic still passes the test only if the resulting code
// preserves the safety and scale properties.
func TestContract_AgentResourceContextWiresApprovalsProvider(t *testing.T) {
router, err := os.ReadFile("router.go")
if err != nil {
t.Fatalf("read router.go: %v", err)
}
if !strings.Contains(string(router), "r.agentContextHandler.SetApprovalsProvider(agentApprovalStoreProvider{})") {
t.Error("router.go must wire the approvals provider on the agent context handler so the bundle's pendingApprovals section is populated")
}
if !strings.Contains(string(router), "approval.SetStore") {
t.Error("router.go must keep documenting that the agent approval provider resolves the process-global approval store at request time")
}
bundle, err := os.ReadFile("agent_resource_context.go")
if err != nil {
t.Fatalf("read agent_resource_context.go: %v", err)
}
if !strings.Contains(string(bundle), "type agentApprovalStoreProvider struct{}") {
t.Error("agent_resource_context.go must define the request-time approval store provider that router.go wires")
}
if !strings.Contains(string(bundle), "return pendingApprovalsForResourceFromStore(approval.GetStore(), resourceID, orgID)") {
t.Error("agentApprovalStoreProvider must resolve the approval store at request time for per-resource context reads")
}
if !strings.Contains(string(bundle), "return pendingApprovalCountsByResourceFromStore(approval.GetStore(), orgID)") {
t.Error("agentApprovalStoreProvider must use the bulk count helper for fleet context reads")
}
if !strings.Contains(string(bundle), "func pendingApprovalsForResourceFromStore(") {
t.Error("agent_resource_context.go must define pendingApprovalsForResourceFromStore as the named, testable filter — drift here breaks the cross-org / cross-resource isolation pins")
}
if !strings.Contains(string(bundle), "func pendingApprovalCountsByResourceFromStore(") {
t.Error("agent_resource_context.go must define pendingApprovalCountsByResourceFromStore so fleet context counts are grouped by one bounded pending-approval scan")
}
if !strings.Contains(string(bundle), "if !approval.BelongsToOrg(req, orgID)") {
t.Error("the named filters must scope pending approvals via BelongsToOrg so cross-tenant requests don't leak into an agent context bundle")
}
if !strings.Contains(string(bundle), "if req.CanonicalResourceID() != resourceID") {
t.Error("the named filter must filter pending approvals by CanonicalResourceID so the bundle only carries approvals targeting the requested resource")
}
if !strings.Contains(string(bundle), "counts[resourceID]++") {
t.Error("the bulk count helper must group pending approvals by canonical resource id instead of forcing fleet context into per-resource scans")
}
}
// TestContract_GetResourceContextCapabilityListsPendingApprovals pins
// the discovery contract: the capabilities manifest must mention
// pendingApprovals under get_resource_context so an agent reading
// the manifest learns the bundle now carries the governance
// dimension. Drift here means external agents still expect the
// pre-slice-45 shape.
func TestContract_GetResourceContextCapabilityListsPendingApprovals(t *testing.T) {
source, err := os.ReadFile("agent_capabilities.go")
if err != nil {
t.Fatalf("read agent_capabilities.go: %v", err)
}
src := string(source)
if !strings.Contains(src, "pending approvals scoped to this resource") {
t.Error("get_resource_context description must mention pending approvals so agents discover the bundle's new governance section through the manifest")
}
}
func TestContract_AgentCommandPayloadsRequireActionScope(t *testing.T) {
source, err := os.ReadFile("agent_command_redaction.go")
if err != nil {
t.Fatalf("read agent_command_redaction.go: %v", err)
}
src := string(source)
if !strings.Contains(src, "requestCanReadAgentCommandPayloads") {
t.Error("agent command redaction must stay behind a named request-scope helper")
}
if !strings.Contains(src, "token.HasScope(config.ScopeAIExecute)") {
t.Error("agent command payloads must require ai:execute on API-token callers, not only monitoring:read")
}
if !strings.Contains(src, "redactAgentEventCommandsForRequest") {
t.Error("agent SSE events must pass through command redaction before serialization")
}
if !strings.Contains(src, "redactAgentResourceContextCommandsForRequest") {
t.Error("agent resource-context bundles must pass through command redaction before serialization")
}
}
// TestContract_AgentFleetContextEndpointSurfacesStableShape pins the
// agent-paradigm fleet view contract: the endpoint must return a
// `resources` array (never null) so agents iterate without
// nil-checking, must initialize per-severity counts as a struct (not
// a map) so agents branch on stable field names, and must carry the
// operator-intent flags (intentionallyOffline, neverAutoRemediate,
// maintenanceWindowActive) that gate auto-action. Drift here would
// silently break the substrate's "where do I focus?" answer.
func TestContract_AgentFleetContextEndpointSurfacesStableShape(t *testing.T) {
source, err := os.ReadFile("agent_resource_context.go")
if err != nil {
t.Fatalf("read agent_resource_context.go: %v", err)
}
src := string(source)
if !strings.Contains(src, "Resources: make([]AgentFleetResourceSummary, 0, len(resources))") {
t.Error("AgentFleetContext.Resources must default to a non-nil empty slice — agents iterate without nil-checks")
}
if !strings.Contains(src, "Resources []AgentFleetResourceSummary `json:\"resources\"`") {
t.Error("AgentFleetContext must carry Resources as a stable []AgentFleetResourceSummary field")
}
if !strings.Contains(src, "IntentionallyOffline bool `json:\"intentionallyOffline\"`") {
t.Error("AgentFleetResourceSummary must carry IntentionallyOffline so agents see operator lock at a glance")
}
if !strings.Contains(src, "NeverAutoRemediate bool `json:\"neverAutoRemediate\"`") {
t.Error("AgentFleetResourceSummary must carry NeverAutoRemediate")
}
if !strings.Contains(src, "MaintenanceWindowActive bool `json:\"maintenanceWindowActive\"`") {
t.Error("AgentFleetResourceSummary must carry MaintenanceWindowActive — server pre-computes so agents don't re-evaluate timestamps client-side")
}
if !strings.Contains(src, "Findings AgentFleetFindingCounts `json:\"findings\"`") {
t.Error("AgentFleetResourceSummary must carry per-severity finding counts as a struct so agents branch on stable field names")
}
if !strings.Contains(src, "PendingApprovalCount int `json:\"pendingApprovalCount\"`") {
t.Error("AgentFleetResourceSummary must carry PendingApprovalCount so agents see governance-blocked resources at a glance")
}
}
// TestContract_ActionCompletedPayloadCarriesVerification pins that
// the action.completed SSE payload exposes the verification block
// — the agent-stable projection of the post-execution probe so
// agents close the "did it actually work?" loop without a follow-up
// audit fetch. Drift here forces every agent watching the stream to
// poll /api/actions/{id} after every dispatch, defeating the
// substrate's push-notification guarantee for dispatch certainty.
func TestContract_ActionCompletedPayloadCarriesVerification(t *testing.T) {
source, err := os.ReadFile("agent_events.go")
if err != nil {
t.Fatalf("read agent_events.go: %v", err)
}
src := string(source)
if !strings.Contains(src, "Verification *AgentResourceActionVerification `json:\"verification,omitempty\"`") {
t.Error("AgentEventActionCompletedPayload must carry Verification as a *AgentResourceActionVerification — agents close the certainty loop on this field")
}
}
// TestContract_RouterBridgesVerificationOntoActionCompleted pins
// that the router-side bridge actually carries the verification
// projection from the canonical top-level verification helper onto
// the SSE payload.
// The payload field exists is one half of the contract; this is the
// other half — that the bridge populates it when the underlying
// audit has a verification result.
func TestContract_RouterBridgesVerificationOntoActionCompleted(t *testing.T) {
source, err := os.ReadFile("router.go")
if err != nil {
t.Fatalf("read router.go: %v", err)
}
src := string(source)
if !strings.Contains(src, "if v := projectAgentResourceVerification(unifiedresources.CanonicalActionVerification(record)); v != nil {") {
t.Error("router.go must project canonical action verification onto the action.completed payload via projectAgentResourceVerification — drift here means verification reaches the audit store but never the SSE stream")
}
if !strings.Contains(src, "payload.Verification = v") {
t.Error("router.go must assign the projected verification onto payload.Verification")
}
}
// TestContract_AgentResourceActionSummaryCarriesVerification pins
// that the resource-context bundle's recent-actions surface also
// carries verification, paralleling the SSE payload. Symmetry
// here matters: an agent should get the same verification fact
// whether they read the bundle (depth) or watch the doorbell
// (push). Drift would let one surface ship a feature the other
// doesn't.
func TestContract_AgentResourceActionSummaryCarriesVerification(t *testing.T) {
source, err := os.ReadFile("agent_resource_context.go")
if err != nil {
t.Fatalf("read agent_resource_context.go: %v", err)
}
src := string(source)
if !strings.Contains(src, "Verification *AgentResourceActionVerification `json:\"verification,omitempty\"`") {
t.Error("AgentResourceActionSummary must carry Verification so the bundle's recent-actions and the action.completed SSE payload speak the same vocabulary")
}
if !strings.Contains(src, "func projectAgentResourceVerification(v *unified.ActionVerificationResult)") {
t.Error("projectAgentResourceVerification must exist as the shared helper — both the SSE bridge and the bundle's projector route through it so the wire shape cannot drift between surfaces")
}
if !strings.Contains(src, "projectAgentResourceVerification(unified.CanonicalActionVerification(audit))") {
t.Error("AgentResourceActionSummary must project canonical action verification, not only legacy result.verification")
}
}
// TestContract_OperatorStateWriteServerPopulatesAttribution pins the
// audit-honesty contract for the agent surface's only write loop:
// the PUT handler must populate SetAt and SetBy from the server,
// ignoring any client-supplied values. Drift here would let an
// agent (or a misconfigured client) spoof attribution timestamps,
// breaking the audit trail's trust model.
func TestContract_OperatorStateWriteServerPopulatesAttribution(t *testing.T) {
source, err := os.ReadFile("resources_operator_state.go")
if err != nil {
t.Fatalf("read resources_operator_state.go: %v", err)
}
src := string(source)
if !strings.Contains(src, "SetAt: time.Now().UTC(),") {
t.Error("operator-state PUT must populate SetAt with the server clock — client cannot spoof attribution")
}
if !strings.Contains(src, "SetBy: getUserID(r),") {
t.Error("operator-state PUT must populate SetBy from the authenticated identity — client cannot spoof who-did-it")
}
if !strings.Contains(src, "// canonical_id from URL wins over body") {
t.Error("operator-state PUT must take the canonical id from the URL, not the body — drift would let scope-confusion writes succeed")
}
}
// TestContract_OperatorStateWriteEmitsStableErrorTokens pins that
// the only two error codes the manifest declares for set/get
// operator-state actually reach the wire from the handler. Drift
// here means external agents branching on operator_state_not_set
// or operator_state_invalid would silently miss the relevant
// failures — manifest claims a code that the handler never emits.
func TestContract_OperatorStateWriteEmitsStableErrorTokens(t *testing.T) {
source, err := os.ReadFile("resources_operator_state.go")
if err != nil {
t.Fatalf("read resources_operator_state.go: %v", err)
}
src := string(source)
if !strings.Contains(src, `"operator_state_not_set"`) {
t.Error("GET handler must emit operator_state_not_set when no entry exists — manifest declares this code")
}
if !strings.Contains(src, `"operator_state_invalid"`) {
t.Error("PUT handler must emit operator_state_invalid on validation failure — manifest declares this code")
}
if !strings.Contains(src, "errors.Is(err, unified.ErrResourceOperatorStateInvalid)") {
t.Error("PUT handler must branch on ErrResourceOperatorStateInvalid so domain validation errors map to the stable wire token — string-matching the error message would drift")
}
}
// TestContract_AgentSurfaceErrorCodesMatchManifestDeclarations pins
// the symmetry between what handlers actually emit and what the
// capabilities manifest declares. The doc says the manifest's
// errorCodes list is "the closed set of values the error field may
// carry on failure"; this test enforces that claim by reading every
// writeJSONError call from the two agent-surface handler files and
// asserting each emitted code is either (a) declared by the matching
// capability, or (b) one of the three cross-cutting codes the auth
// middleware emits universally. Drift in either direction is a
// contract regression — emitting an undeclared code silently breaks
// agents that branch on the closed set; declaring a code the
// handler never emits misleads agents into writing dead-code paths.
func TestContract_AgentSurfaceErrorCodesMatchManifestDeclarations(t *testing.T) {
// Codes emitted by the multi-tenant / auth middleware that apply
// to every authenticated endpoint and are intentionally not
// duplicated on per-capability errorCodes lists. Documented in
// api-contracts.md "cross-cutting codes" paragraph.
crossCutting := map[string]bool{
"invalid_org": true,
"org_suspended": true,
"access_denied": true,
}
// Handler files that back agent-surface capabilities. If a future
// capability adds a new file, append it here so the audit follows.
handlerFiles := []string{
"agent_resource_context.go",
"resources_operator_state.go",
"actions.go",
}
// Codes the action handlers emit that are deliberately NOT in
// the per-capability manifest entries: 5xx internal-failure
// codes (audit-store outages, encode failures) and codes that
// surface from edge paths the manifest declares against a
// different capability. Agents branch on 5xx generically; they
// don't need the specific token. Whitelisted here so the
// emit/declare audit doesn't false-positive on them.
internalOnlyCodes := map[string]bool{
"action_audit_unavailable": true,
"action_audit_persist_failed": true,
"action_plan_failed": true,
"action_plan_encode_failed": true,
"action_audit_query_failed": true,
"action_decision_persist_failed": true,
"action_decision_encode_failed": true,
"action_decision_failed": true,
"action_execution_persist_failed": true,
"action_execution_encode_failed": true,
"action_execution_failed": true,
"action_not_executing": true,
"action_plan_validation_failed": true,
"resource_registry_unavailable": true,
}
// Extract every emitted code from writeJSONError /
// writeJSONErrorWithDetails calls. The regex tolerates
// whitespace/newlines that gofmt may insert between the call's
// arguments and matches both function names since they share
// the same envelope contract.
emitted := map[string]bool{}
for _, file := range handlerFiles {
body, err := os.ReadFile(file)
if err != nil {
t.Fatalf("read %s: %v", file, err)
}
// Match the second string literal (the code) on
// writeJSONError(w, status, "code", ...) or
// writeJSONErrorWithDetails(w, status, "code", ...). The
// first arg `w` is an identifier, then a status int or
// http.StatusXxx selector, so the regex skips past those.
looseRE := regexp.MustCompile(`writeJSONError(?:WithDetails)?\([^)]*?"([a-z_][a-z_0-9]+)"`)
for _, m := range looseRE.FindAllSubmatch(body, -1) {
code := string(m[1])
if internalOnlyCodes[code] {
continue
}
emitted[code] = true
}
}
if len(emitted) == 0 {
t.Fatal("no writeJSONError emissions found in agent-surface handler files; the audit regex must be wrong")
}
// Pull declared codes from the manifest source so the test
// follows manifest edits without duplicating the data here.
manifestSrc, err := os.ReadFile("agent_capabilities.go")
if err != nil {
t.Fatalf("read agent_capabilities.go: %v", err)
}
declared := map[string]bool{}
declaredRE := regexp.MustCompile(`ErrorCodes:\s*\[\]string\{([^}]+)\}`)
for _, m := range declaredRE.FindAllSubmatch(manifestSrc, -1) {
// The capture group is the content between `{` and `}`,
// shaped like `"code1", "code2"`. Pull each quoted string.
quoted := regexp.MustCompile(`"([a-z_][a-z_0-9]+)"`)
for _, q := range quoted.FindAllSubmatch(m[1], -1) {
declared[string(q[1])] = true
}
}
// Every emitted code must be either declared somewhere in the
// manifest OR a cross-cutting code.
for code := range emitted {
if declared[code] || crossCutting[code] {
continue
}
t.Errorf("handler emits %q but no capability in the manifest declares it and it is not a documented cross-cutting code — drift here breaks agents that branch on the closed set", code)
}
// Every manifest-declared code must have a matching emission
// somewhere in the agent-surface handlers, otherwise agents
// follow a code path the substrate never walks.
for code := range declared {
if !emitted[code] {
t.Errorf("manifest declares %q but no agent-surface handler emits it — declaring a dead code misleads agents into writing branches the substrate never triggers", code)
}
}
}
// TestContract_AgentCapabilitiesManifestIsPublic pins the auth
// contract for the discovery surface: the manifest must be in the
// router's publicPaths list so it serves without a token. The
// manifest's purpose is "let an agent introspect Pulse before it
// has credentials" — gating the manifest behind the same
// monitoring:read scope that gates everything else makes
// discovery a chicken-and-egg problem. Slice 40 had this wrong;
// slice 47 fixed it. Pin so it cannot regress silently.
func TestContract_AgentCapabilitiesManifestIsPublic(t *testing.T) {
source, err := os.ReadFile("router.go")
if err != nil {
t.Fatalf("read router.go: %v", err)
}
src := string(source)
if !strings.Contains(src, "\"/api/agent/capabilities\", // Agent-paradigm discovery manifest") {
t.Error("router.go publicPaths must include /api/agent/capabilities so the discovery manifest serves without a token — agents need to introspect Pulse before they have credentials")
}
}
// TestContract_GetFleetContextCapabilityListed pins the discovery
// contract: the capabilities manifest must declare get_fleet_context
// so an agent reading the manifest learns the fleet view exists and
// what its response shape is. Drift here means external agents
// still expect to walk every resource id one-by-one.
func TestContract_GetFleetContextCapabilityListed(t *testing.T) {
source, err := os.ReadFile("agent_capabilities.go")
if err != nil {
t.Fatalf("read agent_capabilities.go: %v", err)
}
src := string(source)
if !strings.Contains(src, `Name: "get_fleet_context"`) {
t.Error("agent capabilities manifest must declare get_fleet_context — the substrate's triage entry point")
}
if !strings.Contains(src, `Path: "/api/agent/fleet-context"`) {
t.Error("get_fleet_context must point at /api/agent/fleet-context")
}
if !strings.Contains(src, `ResponseShape: "AgentFleetContext"`) {
t.Error("get_fleet_context response shape must be AgentFleetContext so agents know what to parse")
}
}
// TestContract_FindingsResourceOperatorStateProviderIsWired pins the
// startup wiring that gives the findings runtime access to
// per-resource operator-set state via the API layer's adapter. The
// router must call SetResourceOperatorStateProvider with a closure
// that reads the unified store's GetResourceOperatorState and
// projects IsInMaintenanceAt into the ActiveMaintenanceWindow shape;
// regressions here would silently disable maintenance-window
// auto-acknowledgement without breaking any unit tests because the
// behavior is opt-in.
func TestContract_FindingsResourceOperatorStateProviderIsWired(t *testing.T) {
source, err := os.ReadFile("router.go")
if err != nil {
t.Fatalf("read router.go: %v", err)
}
src := string(source)
if !strings.Contains(src, "SetResourceOperatorStateProvider(") {
t.Error("router.go must call SetResourceOperatorStateProvider on the findings store at startup")
}
if !strings.Contains(src, "ResourceOperatorStateProviderFunc(") {
t.Error("router.go must use the ai.ResourceOperatorStateProviderFunc adapter so the closure satisfies the provider interface")
}
if !strings.Contains(src, "IsInMaintenanceAt(now)") {
t.Error("router.go must gate the projection on state.IsInMaintenanceAt so a stale window does not return as active")
}
if !strings.Contains(src, "GetResourceOperatorState(canonicalID)") {
t.Error("router.go must read state from the unified store's canonical accessor")
}
if !strings.Contains(src, "IntentionallyOffline: state.IntentionallyOffline") {
t.Error("router.go must propagate state.IntentionallyOffline through the projection so the findings runtime sees it")
}
}
// TestContract_ResourceOperatorStateUrlCanonicalIDWinsOverBody pins the
// security-relevant decision that the URL path's canonical_id always
// wins over any body-supplied value on PUT
// /api/resources/{id}/operator-state. Without this rule a request
// authorized to write at /vm:101 could retarget the write at /vm:102
// through body manipulation, defeating per-resource scoping.
func TestContract_ResourceOperatorStateUrlCanonicalIDWinsOverBody(t *testing.T) {
cfg := &config.Config{DataPath: t.TempDir()}
h := NewResourceHandlers(cfg)
body := []byte(`{"canonicalId":"vm:999","intentionallyOffline":true}`)
rec := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodPut, "/api/resources/vm:101/operator-state", bytes.NewReader(body))
h.HandleResourceOperatorState(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("expected 200; got %d body=%s", rec.Code, rec.Body.String())
}
var persisted resourceOperatorStateAPI
if err := json.Unmarshal(rec.Body.Bytes(), &persisted); err != nil {
t.Fatalf("response not JSON: %v", err)
}
if persisted.CanonicalID != "vm:101" {
t.Errorf("URL canonical_id must override body; got %q (security regression)", persisted.CanonicalID)
}
// And the resource named in the body must NOT have a state row,
// confirming the write didn't bleed across resource scope.
rec2 := httptest.NewRecorder()
req2 := httptest.NewRequest(http.MethodGet, "/api/resources/vm:999/operator-state", nil)
h.HandleResourceOperatorState(rec2, req2)
if rec2.Code != http.StatusNotFound {
t.Errorf("body-supplied canonical_id must not have been persisted; GET vm:999 returned %d", rec2.Code)
}
}
// TestContract_ProxmoxGuestDockerDetectionRequiresExplicitOptIn pins the
// privacy boundary for Proxmox-side LXC Docker socket hinting and inventory.
// Router startup may wire a Docker checker or collector only when the server
// has the explicit opt-in config.
func TestContract_ProxmoxGuestDockerDetectionRequiresExplicitOptIn(t *testing.T) {
t.Run("disabled without explicit opt-in", func(t *testing.T) {
monitor := &monitoring.Monitor{}
router := &Router{
config: &config.Config{
EnableProxmoxGuestDockerDetection: false,
},
agentExecServer: agentexec.NewServer(func(token string, agentID string, hostname string) bool {
return false
}),
}
router.configureProxmoxGuestDockerDetection(monitor)
if got := monitor.GetDockerChecker(); got != nil {
t.Fatalf("expected Proxmox guest Docker checker to stay disabled without explicit opt-in, got %T", got)
}
if got := monitor.GetDockerInventoryCollector(); got != nil {
t.Fatalf("expected Proxmox guest Docker inventory collector to stay disabled without explicit opt-in, got %T", got)
}
})
t.Run("socket hinting enabled only with explicit opt-in", func(t *testing.T) {
monitor := &monitoring.Monitor{}
router := &Router{
config: &config.Config{
EnableProxmoxGuestDockerDetection: true,
},
agentExecServer: agentexec.NewServer(func(token string, agentID string, hostname string) bool {
return false
}),
}
router.configureProxmoxGuestDockerDetection(monitor)
if got := monitor.GetDockerChecker(); got == nil {
t.Fatal("expected Proxmox guest Docker checker after explicit opt-in")
}
if got := monitor.GetDockerInventoryCollector(); got != nil {
t.Fatalf("expected Proxmox guest Docker inventory collector to stay disabled without inventory opt-in, got %T", got)
}
})
t.Run("inventory opt-in wires checker and collector", func(t *testing.T) {
monitor := &monitoring.Monitor{}
router := &Router{
config: &config.Config{
EnableProxmoxGuestDockerInventory: true,
},
agentExecServer: agentexec.NewServer(func(token string, agentID string, hostname string) bool {
return false
}),
}
router.configureProxmoxGuestDockerDetection(monitor)
if got := monitor.GetDockerChecker(); got == nil {
t.Fatal("expected Proxmox guest Docker checker after inventory opt-in")
}
if got := monitor.GetDockerInventoryCollector(); got == nil {
t.Fatal("expected Proxmox guest Docker inventory collector after inventory opt-in")
}
})
}