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_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 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", "appendUnifiedFindingOperatorBriefingContext(&b, f, handoffActions)", "[Operator Briefing]", `appendChatContextLine(b, "Recency", formatUnifiedFindingBriefingRecencyFacts(f))`, "formatUnifiedFindingBriefingRecencyFacts(f)", `appendChatContextLine(b, "Attention Reason", attention)`, "unifiedFindingBriefingAttention(f)", `appendChatContextLine(b, "Evidence Snapshot", evidence)`, `appendChatContextLine(b, "Verification", verification)`, "formatInvestigationRecordEvidenceBriefing", "formatBriefingStringList", `appendChatContextLine(b, "Operator Decision", decision)`, "unifiedFindingBriefingOperatorDecision(f, handoffActions)", "unifiedFindingPrimaryHandoffAction(f, handoffActions)", "Latest Lifecycle Event", "Operator 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") } 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", "Operator 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, 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 { 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, 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.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=vmware-vsphere&type=storage", len(vmware.DefaultFixtures().Datastores)) } 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") } 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_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", "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", "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", "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)", "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_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: "explore_status", event: mustStreamEvent(t, "explore_status", chat.ExploreStatusData{ Phase: "started", Message: "Explore pre-pass running (read-only context).", Model: "openai:explore-fast", }), want: `{"type":"explore_status","data":{"phase":"started","message":"Explore pre-pass running (read-only context).","model":"openai:explore-fast"}}`, }, { 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_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.ActionStatePlanned || gotAudit.Result != nil { 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) != 0 { t.Fatalf("dry-run execution should not append lifecycle events, got %d", len(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 err := h.validateActionPlanFresh(orgID, record); err != nil", "errors.Is(err, unified.ErrActionPlanDrift)", "recordRefusedActionExecution(store, record, actor, now, err)", "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_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_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, }, 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"},"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 record.Result.Verification 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(record.Result.Verification); v != nil {") { t.Error("router.go must project record.Result.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") } } // 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) } }