Pulse/internal/api/contract_test.go

8849 lines
301 KiB
Go

package api
import (
"bytes"
"context"
"crypto/ed25519"
"encoding/base64"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"os"
"path/filepath"
"reflect"
"strings"
"testing"
"time"
"github.com/golang-jwt/jwt/v5"
"github.com/gorilla/websocket"
"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/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"
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"
"github.com/rcourtman/pulse-go-rewrite/pkg/metrics"
"github.com/rcourtman/pulse-go-rewrite/pkg/reporting"
)
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_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_AIQuickstartPayloadFieldsRemainCanonical(t *testing.T) {
settingsBody, err := json.Marshal(AISettingsResponse{
QuickstartCreditsRemaining: 7,
QuickstartCreditsTotal: 25,
UsingQuickstart: true,
})
if err != nil {
t.Fatalf("marshal AI settings response: %v", err)
}
if !bytes.Contains(settingsBody, []byte(`"quickstart_credits_remaining":7`)) {
t.Fatalf("expected AI settings payload to expose quickstart_credits_remaining, got %s", settingsBody)
}
if !bytes.Contains(settingsBody, []byte(`"quickstart_credits_total":25`)) {
t.Fatalf("expected AI settings payload to expose quickstart_credits_total, got %s", settingsBody)
}
if !bytes.Contains(settingsBody, []byte(`"using_quickstart":true`)) {
t.Fatalf("expected AI settings payload to expose using_quickstart, got %s", settingsBody)
}
statusBody, err := json.Marshal(PatrolStatusResponse{
QuickstartCreditsRemaining: 7,
QuickstartCreditsTotal: 25,
UsingQuickstart: true,
})
if err != nil {
t.Fatalf("marshal patrol status response: %v", err)
}
if !bytes.Contains(statusBody, []byte(`"quickstart_credits_remaining":7`)) {
t.Fatalf("expected patrol status payload to expose quickstart_credits_remaining, got %s", statusBody)
}
if !bytes.Contains(statusBody, []byte(`"quickstart_credits_total":25`)) {
t.Fatalf("expected patrol status payload to expose quickstart_credits_total, got %s", statusBody)
}
if !bytes.Contains(statusBody, []byte(`"using_quickstart":true`)) {
t.Fatalf("expected patrol status payload to expose using_quickstart, got %s", statusBody)
}
}
func TestContract_AISettingsUpdateQuickstartBootstrapJSONSnapshot(t *testing.T) {
tmp := t.TempDir()
cfg := &config.Config{DataPath: tmp}
persistence := config.NewConfigPersistence(tmp)
persistQuickstartActivationState(t, persistence)
useTestQuickstartBootstrapServer(t, func(r *http.Request, reqBody map[string]any) {
if got := strings.TrimSpace(r.Header.Get("Authorization")); got != "Bearer pit_live_test" {
t.Fatalf("authorization=%q want Bearer pit_live_test", got)
}
instanceFingerprint, _ := reqBody["instance_fingerprint"].(string)
if instanceFingerprint != "fp-live-test" {
t.Fatalf("instance_fingerprint=%q want fp-live-test", instanceFingerprint)
}
if reqBody["use_case"] != "patrol" {
t.Fatalf("use_case=%v want patrol", reqBody["use_case"])
}
})
handler := newTestAISettingsHandler(cfg, persistence, nil)
handler.defaultAIService.SetQuickstartCredits(ai.NewPersistentQuickstartCreditManager(
persistence,
"default",
func() *config.AIConfig {
cfg, _ := persistence.LoadAIConfig()
return cfg
},
))
req := httptest.NewRequest(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.StatusOK {
t.Fatalf("status=%d, want %d: %s", rec.Code, http.StatusOK, rec.Body.String())
}
const want = `{
"enabled":true,
"model":"quickstart:pulse-hosted",
"chat_model":"quickstart:pulse-hosted",
"patrol_model":"quickstart:pulse-hosted",
"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":false,
"ollama_base_url":"http://localhost:11434",
"ollama_password_set":false,
"configured_providers":[],
"control_level":"read_only",
"protected_guests":[],
"discovery_enabled":false,
"quickstart_credits_total":25,
"quickstart_credits_used":0,
"quickstart_credits_remaining":25,
"quickstart_credits_available":true,
"using_quickstart":true
}`
assertJSONSnapshot(t, rec.Body.Bytes(), want)
}
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 := httptest.NewRequest(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,
"quickstart_credits_total":0,
"quickstart_credits_used":0,
"quickstart_credits_remaining":0,
"quickstart_credits_available":false,
"using_quickstart":false
}`, ollama.URL)
assertJSONSnapshot(t, rec.Body.Bytes(), want)
}
func TestContract_PatrolStatusActivationRequiredSurface(t *testing.T) {
handler, _, _, _ := setupAIHandlerWithPatrol(t)
persistence := handler.defaultPersistence
aiCfg := config.NewDefaultAIConfig()
aiCfg.Enabled = true
if err := persistence.SaveAIConfig(*aiCfg); err != nil {
t.Fatalf("SaveAIConfig: %v", err)
}
handler.defaultAIService.SetQuickstartCredits(ai.NewPersistentQuickstartCreditManager(
persistence,
"default",
func() *config.AIConfig {
cfg, _ := persistence.LoadAIConfig()
return cfg
},
))
if err := handler.defaultAIService.LoadConfig(); err != nil {
t.Fatalf("LoadConfig: %v", err)
}
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 %q", resp.RuntimeState, ai.PatrolRuntimeStateBlocked)
}
if !resp.Enabled {
t.Fatal("expected patrol status to remain enabled while activation is required")
}
if resp.Healthy {
t.Fatal("expected activation-required patrol status to report healthy=false")
}
if resp.BlockedReason != ai.QuickstartActivationRequiredReason() {
t.Fatalf("blocked_reason=%q want %q", resp.BlockedReason, ai.QuickstartActivationRequiredReason())
}
if resp.QuickstartCreditsRemaining != 0 || resp.QuickstartCreditsTotal != 0 {
t.Fatalf("quickstart credits=%d/%d want 0/0", resp.QuickstartCreditsRemaining, resp.QuickstartCreditsTotal)
}
if resp.UsingQuickstart {
t.Fatal("expected using_quickstart=false while activation is required")
}
}
func TestContract_AISettingsActivationRequiredSurface(t *testing.T) {
tmp := t.TempDir()
cfg := &config.Config{DataPath: tmp}
persistence := config.NewConfigPersistence(tmp)
handler := newTestAISettingsHandler(cfg, persistence, nil)
handler.defaultAIService.SetQuickstartCredits(ai.NewPersistentQuickstartCreditManager(
persistence,
"default",
func() *config.AIConfig { return &config.AIConfig{Enabled: true} },
))
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.QuickstartBlockedReason != ai.QuickstartActivationRequiredReason() {
t.Fatalf(
"quickstart_blocked_reason=%q want %q",
resp.QuickstartBlockedReason,
ai.QuickstartActivationRequiredReason(),
)
}
if resp.QuickstartCreditsRemaining != 0 || resp.QuickstartCreditsTotal != 0 {
t.Fatalf("quickstart credits=%d/%d want 0/0", resp.QuickstartCreditsRemaining, resp.QuickstartCreditsTotal)
}
if resp.QuickstartCreditsAvailable {
t.Fatal("expected quickstart_credits_available=false while activation is required")
}
if !bytes.Contains(rec.Body.Bytes(), []byte(`"quickstart_blocked_reason":"`+ai.QuickstartActivationRequiredReason()+`"`)) {
t.Fatalf("expected AI settings payload to expose quickstart_blocked_reason, got %s", rec.Body.Bytes())
}
}
func TestContract_AISettingsBYOKOverrideRetainsQuickstartInventoryJSONSnapshot(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)
handler.defaultAIService.SetQuickstartCredits(&stubQuickstartCreditManager{
remaining: 12,
total: pkglicensing.QuickstartCreditsTotal,
})
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,
"quickstart_credits_total":25,
"quickstart_credits_used":13,
"quickstart_credits_remaining":12,
"quickstart_credits_available":true,
"using_quickstart":false
}`
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.Date(2026, time.April, 1, 12, 0, 0, 0, time.UTC)
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)
}
}
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.Date(2026, time.March, 31, 12, 0, 0, 0, time.UTC)
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_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.Date(2026, time.March, 31, 12, 0, 0, 0, time.UTC)
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.Date(2026, time.April, 1, 12, 0, 0, 0, time.UTC)
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.Date(2026, time.March, 31, 12, 0, 0, 0, time.UTC)
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_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_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,
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,
"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)
}
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",
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",
"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{
OrgID: "org-123",
UserID: "owner@example.com",
Message: "Check your email for a magic link to finish signing in.",
}
got, err := json.Marshal(payload)
if err != nil {
t.Fatalf("marshal hosted signup response: %v", err)
}
const want = `{
"org_id":"org-123",
"user_id":"owner@example.com",
"message":"Check your email for a magic link to finish signing in."
}`
assertJSONSnapshot(t, got, want)
}
func TestContract_TrialStartHostedSignupRedirectContract(t *testing.T) {
baseDir := t.TempDir()
mtp := config.NewMultiTenantPersistence(baseDir)
h := NewLicenseHandlers(mtp, false, &config.Config{
PublicURL: "https://pulse.example.com",
ProTrialSignupURL: "https://billing.example.com/start-pro-trial?source=contract",
})
req := httptest.NewRequest(http.MethodPost, "/api/license/trial/start", nil).WithContext(
context.WithValue(context.Background(), OrgIDContextKey, "default"),
)
rec := httptest.NewRecorder()
h.HandleStartTrial(rec, req)
if rec.Code != http.StatusConflict {
t.Fatalf("status=%d, want %d: %s", rec.Code, http.StatusConflict, rec.Body.String())
}
var payload APIError
if err := json.NewDecoder(rec.Body).Decode(&payload); err != nil {
t.Fatalf("decode response: %v", err)
}
if payload.Code != "trial_signup_required" {
t.Fatalf("code=%q, want %q", payload.Code, "trial_signup_required")
}
if strings.TrimSpace(payload.Details["action_url"]) == "" {
t.Fatal("expected action_url in contract payload")
}
}
func TestContract_TrialStartRateLimitContract(t *testing.T) {
baseDir := t.TempDir()
mtp := config.NewMultiTenantPersistence(baseDir)
h := NewLicenseHandlers(mtp, false, &config.Config{
PublicURL: "https://pulse.example.com",
ProTrialSignupURL: "https://billing.example.com/start-pro-trial?source=contract",
})
h.trialLimiter = NewRateLimiter(1, time.Minute)
ctx := context.WithValue(context.Background(), OrgIDContextKey, "default")
firstReq := httptest.NewRequest(http.MethodPost, "/api/license/trial/start", nil).WithContext(ctx)
firstRec := httptest.NewRecorder()
h.HandleStartTrial(firstRec, firstReq)
if firstRec.Code != http.StatusConflict {
t.Fatalf("first status=%d, want %d: %s", firstRec.Code, http.StatusConflict, firstRec.Body.String())
}
secondReq := httptest.NewRequest(http.MethodPost, "/api/license/trial/start", nil).WithContext(ctx)
secondRec := httptest.NewRecorder()
h.HandleStartTrial(secondRec, secondReq)
if secondRec.Code != http.StatusTooManyRequests {
t.Fatalf("second status=%d, want %d: %s", secondRec.Code, http.StatusTooManyRequests, secondRec.Body.String())
}
retryAfter := secondRec.Header().Get("Retry-After")
if retryAfter == "" {
t.Fatal("expected Retry-After header")
}
var payload APIError
if err := json.NewDecoder(secondRec.Body).Decode(&payload); err != nil {
t.Fatalf("decode response: %v", err)
}
if payload.Code != "trial_rate_limited" {
t.Fatalf("code=%q, want %q", payload.Code, "trial_rate_limited")
}
if payload.Details["retry_after_seconds"] != retryAfter {
t.Fatalf("retry_after_seconds=%q, want %q", payload.Details["retry_after_seconds"], retryAfter)
}
}
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_HostedAISettingsAutoBootstrapJSONSnapshot(t *testing.T) {
t.Setenv("PULSE_HOSTED_MODE", "true")
useTestQuickstartBootstrapServer(t, func(r *http.Request, reqBody map[string]any) {
authHeader := strings.TrimSpace(r.Header.Get("Authorization"))
if !strings.HasPrefix(authHeader, "Bearer ") {
t.Fatalf("expected Bearer auth, got %q", authHeader)
}
if parts := strings.Split(strings.TrimSpace(strings.TrimPrefix(authHeader, "Bearer ")), "."); len(parts) != 3 {
t.Fatalf("expected entitlement JWT bearer token, got %q", authHeader)
}
if _, hasFingerprint := reqBody["instance_fingerprint"]; hasFingerprint {
t.Fatalf("expected hosted quickstart bootstrap to omit instance_fingerprint, got %v", reqBody)
}
if got := reqBody["use_case"]; got != "patrol" {
t.Fatalf("use_case=%v want patrol", got)
}
})
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 to persist canonical ai.enc bootstrap")
}
const want = `{
"enabled":true,
"model":"quickstart:pulse-hosted",
"chat_model":"quickstart:pulse-hosted",
"patrol_model":"quickstart:pulse-hosted",
"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":false,
"ollama_base_url":"http://localhost:11434",
"ollama_password_set":false,
"configured_providers":[],
"control_level":"read_only",
"protected_guests":[],
"discovery_enabled":false,
"quickstart_credits_total":25,
"quickstart_credits_used":0,
"quickstart_credits_remaining":25,
"quickstart_credits_available":true,
"using_quickstart":true
}`
assertJSONSnapshot(t, rec.Body.Bytes(), want)
}
func TestContract_AISettingsLegacyQuickstartAliasJSONSnapshot(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":"quickstart:pulse-hosted",
"chat_model":"quickstart:pulse-hosted",
"patrol_model":"quickstart:pulse-hosted",
"auto_fix_model":"quickstart:pulse-hosted",
"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,
"quickstart_credits_total":0,
"quickstart_credits_used":0,
"quickstart_credits_remaining":0,
"quickstart_credits_available":false,
"using_quickstart":false
}`
assertJSONSnapshot(t, rec.Body.Bytes(), want)
if bytes.Contains(rec.Body.Bytes(), []byte("quickstart:minimax-2.5m")) {
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":"openai:gpt-4o",
"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,
"quickstart_credits_total":0,
"quickstart_credits_used":0,
"quickstart_credits_remaining":0,
"quickstart_credits_available":false,
"using_quickstart":false
}`
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 (Pro)",
"description":"Generate PDF and CSV performance reports plus current-state VM inventory exports across infrastructure and workload resources."
},
"guidance":{
"title":"Advanced Insights",
"description":"Performance reports come from the historical metrics store, while VM inventory export captures the current runtime state for spreadsheet-friendly fleet reviews. Use reports for trends and the inventory export for current allocation and usage 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_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_HostedTenantAISettingsFallbackJSONSnapshot(t *testing.T) {
t.Setenv("PULSE_HOSTED_MODE", "true")
useTestQuickstartBootstrapServer(t, func(r *http.Request, reqBody map[string]any) {
authHeader := strings.TrimSpace(r.Header.Get("Authorization"))
if !strings.HasPrefix(authHeader, "Bearer ") {
t.Fatalf("expected Bearer auth, got %q", authHeader)
}
if parts := strings.Split(strings.TrimSpace(strings.TrimPrefix(authHeader, "Bearer ")), "."); len(parts) != 3 {
t.Fatalf("expected entitlement JWT bearer token, got %q", authHeader)
}
if _, hasFingerprint := reqBody["instance_fingerprint"]; hasFingerprint {
t.Fatalf("expected hosted quickstart bootstrap to omit instance_fingerprint, got %v", reqBody)
}
if got := reqBody["use_case"]; got != "patrol" {
t.Fatalf("use_case=%v want patrol", got)
}
})
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 to persist tenant ai.enc bootstrap")
}
const want = `{
"enabled":true,
"model":"quickstart:pulse-hosted",
"chat_model":"quickstart:pulse-hosted",
"patrol_model":"quickstart:pulse-hosted",
"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":false,
"ollama_base_url":"http://localhost:11434",
"ollama_password_set":false,
"configured_providers":[],
"control_level":"read_only",
"protected_guests":[],
"discovery_enabled":false,
"quickstart_credits_total":25,
"quickstart_credits_used":0,
"quickstart_credits_remaining":25,
"quickstart_credits_available":true,
"using_quickstart":true
}`
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,
Limit: 5,
}
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,
"limit":5
}`
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,
Limit: 5,
}
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,
"limit":5
}`
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_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)
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_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, `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, `"source":"script"`) {
t.Fatalf("setup script missing canonical /api/auto-register source marker: %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)
}
foundMonitoredSystemLimit := false
for _, limit := range payload.Limits {
if limit.Key == pkglicensing.MaxMonitoredSystemsLicenseGateKey {
foundMonitoredSystemLimit = true
if limit.Limit != 50 {
t.Fatalf("max_monitored_systems limit=%d, want 50", limit.Limit)
}
}
}
if !foundMonitoredSystemLimit {
t.Fatalf("expected max_monitored_systems limit in payload, 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_EntitlementPayloadMonitoredSystemUsageJSONSnapshot(t *testing.T) {
payload := buildEntitlementPayloadWithUsage(&licenseStatus{
Valid: true,
Tier: pkglicensing.TierPro,
Features: append([]string(nil), pkglicensing.TierFeatures[pkglicensing.TierPro]...),
MaxMonitoredSystems: 15,
}, string(pkglicensing.SubStateActive), entitlementUsageSnapshot{
MonitoredSystems: 7,
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","ai_patrol","relay","mobile_app","push_notifications","long_term_metrics","ai_alerts","ai_autofix","kubernetes_ai","agent_profiles","advanced_sso","rbac","audit_logging","advanced_reporting"],
"limits":[{"key":"max_monitored_systems","limit":15,"current":7,"current_available":true,"state":"ok"}],
"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_EntitlementPayloadMonitoredSystemUsageUnavailableJSONSnapshot(t *testing.T) {
payload := buildEntitlementPayloadWithUsage(&licenseStatus{
Valid: true,
Tier: pkglicensing.TierPro,
Features: append([]string(nil), pkglicensing.TierFeatures[pkglicensing.TierPro]...),
MaxMonitoredSystems: 15,
MonitoredSystemContinuity: &pkglicensing.MonitoredSystemContinuityStatus{
PlanLimit: 15,
EffectiveLimit: 15,
CapturePending: true,
},
}, 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","ai_patrol","relay","mobile_app","push_notifications","long_term_metrics","ai_alerts","ai_autofix","kubernetes_ai","agent_profiles","advanced_sso","rbac","audit_logging","advanced_reporting"],
"limits":[{"key":"max_monitored_systems","limit":15,"current":0,"current_available":false,"current_unavailable_reason":"supplemental_inventory_unsettled","state":"ok"}],
"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":0,"docker_hosts":0,"kubernetes_clusters":0},
"has_migration_gap":false,
"monitored_system_continuity":{"plan_limit":15,"effective_limit":15,"capture_pending":true}
}`
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_LegacyMigrationGrandfatherFloorJSONSnapshot(t *testing.T) {
t.Setenv("PULSE_LICENSE_DEV_MODE", "false")
grantJWT, grantPublicKey, err := pkglicensing.GenerateGrantJWTForTesting(pkglicensing.GrantClaims{
LicenseID: "lic_contract_floor",
Tier: "pro",
PlanKey: "v5_pro_monthly_grandfathered",
State: "active",
Features: []string{"relay"},
MaxMonitoredSystems: 10,
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)
}
w.WriteHeader(http.StatusCreated)
_ = json.NewEncoder(w).Encode(pkglicensing.ActivateInstallationResponse{
License: pkglicensing.ActivateResponseLicense{
LicenseID: "lic_contract_floor",
State: "active",
Tier: "pro",
Features: []string{"relay"},
MaxMonitoredSystems: 10,
},
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 := pkglicensing.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.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)
}
statusContinuity := status.MonitoredSystemContinuity
if statusContinuity != nil {
copied := *statusContinuity
if copied.CapturedAt > 0 {
copied.CapturedAt = 123
}
statusContinuity = &copied
}
payloadContinuity := payload.MonitoredSystemContinuity
if payloadContinuity != nil {
copied := *payloadContinuity
if copied.CapturedAt > 0 {
copied.CapturedAt = 123
}
payloadContinuity = &copied
}
got, err := json.Marshal(struct {
Status struct {
Tier pkglicensing.Tier `json:"tier"`
PlanVersion string `json:"plan_version"`
MaxMonitoredSystems int `json:"max_monitored_systems"`
Valid bool `json:"valid"`
MonitoredSystemContinuity *pkglicensing.MonitoredSystemContinuityStatus `json:"monitored_system_continuity,omitempty"`
} `json:"status"`
Entitlements struct {
Tier string `json:"tier"`
PlanVersion string `json:"plan_version"`
SubscriptionState string `json:"subscription_state"`
Limits []pkglicensing.LimitStatus `json:"limits"`
MonitoredSystemContinuity *pkglicensing.MonitoredSystemContinuityStatus `json:"monitored_system_continuity,omitempty"`
} `json:"entitlements"`
}{
Status: struct {
Tier pkglicensing.Tier `json:"tier"`
PlanVersion string `json:"plan_version"`
MaxMonitoredSystems int `json:"max_monitored_systems"`
Valid bool `json:"valid"`
MonitoredSystemContinuity *pkglicensing.MonitoredSystemContinuityStatus `json:"monitored_system_continuity,omitempty"`
}{
Tier: status.Tier,
PlanVersion: status.PlanVersion,
MaxMonitoredSystems: status.MaxMonitoredSystems,
Valid: status.Valid,
MonitoredSystemContinuity: statusContinuity,
},
Entitlements: struct {
Tier string `json:"tier"`
PlanVersion string `json:"plan_version"`
SubscriptionState string `json:"subscription_state"`
Limits []pkglicensing.LimitStatus `json:"limits"`
MonitoredSystemContinuity *pkglicensing.MonitoredSystemContinuityStatus `json:"monitored_system_continuity,omitempty"`
}{
Tier: payload.Tier,
PlanVersion: payload.PlanVersion,
SubscriptionState: payload.SubscriptionState,
Limits: payload.Limits,
MonitoredSystemContinuity: payloadContinuity,
},
})
if err != nil {
t.Fatalf("marshal snapshot payload: %v", err)
}
const want = `{
"status":{
"tier":"pro",
"plan_version":"v5_pro_monthly_grandfathered",
"max_monitored_systems":23,
"valid":true,
"monitored_system_continuity":{"plan_limit":10,"grandfathered_floor":23,"effective_limit":23,"capture_pending":false,"captured_at":123}
},
"entitlements":{
"tier":"pro",
"plan_version":"v5_pro_monthly_grandfathered",
"subscription_state":"active",
"limits":[{"key":"max_monitored_systems","limit":23,"current":23,"current_available":true,"state":"enforced"}],
"monitored_system_continuity":{"plan_limit":10,"grandfathered_floor":23,"effective_limit":23,"capture_pending":false,"captured_at":123}
}
}`
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":{"max_monitored_systems":50},
"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_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.MethodPost, path: "/api/license/trial/start"},
{method: http.MethodGet, path: "/api/license/monitored-system-ledger"},
{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/upgrade-metrics/stats"},
{method: http.MethodPost, path: "/api/upgrade-metrics/events"},
{method: http.MethodGet, path: licensePurchaseStartPath},
{method: http.MethodGet, path: licensePurchaseHandoffPath},
{method: http.MethodGet, path: "/auth/trial-activate"},
}
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 := pkglicensing.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)
}
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_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 != "max_monitored_systems" {
t.Fatalf("claims.Feature=%q, want max_monitored_systems", 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":"max_monitored_systems",
"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=max_monitored_systems\u0026purchase=cancelled"
}`
assertJSONSnapshot(t, got, want)
}
func TestContract_SelfHostedPurchaseLegacyHandoffJSONSnapshot(t *testing.T) {
handler := createTestHandler(t)
handler.SetConfig(&config.Config{PublicURL: "https://pulse.example.com"})
returnToken := issuePurchaseReturnToken(t, handler, "default", "max_monitored_systems")
req := httptest.NewRequest(
http.MethodGet,
"https://pulse.example.com"+licensePurchaseHandoffPath+
"?purchase_return_token="+url.QueryEscape(returnToken),
nil,
)
rec := httptest.NewRecorder()
handler.HandleCheckoutHandoff(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status=%d, want %d: %s", rec.Code, http.StatusOK, rec.Body.String())
}
if got := rec.Header().Get("Access-Control-Allow-Origin"); got != "*" {
t.Fatalf("allow-origin=%q, want *", got)
}
if got := rec.Header().Get("Cache-Control"); got != "no-store" {
t.Fatalf("cache-control=%q, want no-store", got)
}
var payload struct {
Feature string `json:"feature"`
ActivationURLTemplate string `json:"activation_url_template"`
}
if err := json.NewDecoder(rec.Body).Decode(&payload); err != nil {
t.Fatalf("decode payload: %v", err)
}
wantTemplate, err := licensePurchaseActivationTemplateURL(
"https://pulse.example.com"+licensePurchaseActivationPath,
returnToken,
)
if err != nil {
t.Fatalf("licensePurchaseActivationTemplateURL: %v", err)
}
if payload.Feature != "max_monitored_systems" {
t.Fatalf("feature=%q, want max_monitored_systems", payload.Feature)
}
if payload.ActivationURLTemplate != wantTemplate {
t.Fatalf("activation_url_template=%q, want %q", payload.ActivationURLTemplate, wantTemplate)
}
}
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"
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_HostedCloudHandoffEnsuresTenantOrganizationMembership(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 := config.NewMultiTenantPersistence(configDir)
if err := mtp.SaveOrganization(&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()},
},
}); err != nil {
t.Fatalf("save organization: %v", err)
}
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("courtmanr@gmail.com"); got != models.OrgRoleOwner {
t.Fatalf("member role=%q, want %q", got, models.OrgRoleOwner)
}
}
func TestContract_HostedDirectCloudHandoffPreservesMembershipClaims(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 := config.NewMultiTenantPersistence(configDir)
if err := mtp.SaveOrganization(&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()},
},
}); err != nil {
t.Fatalf("save organization: %v", err)
}
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("courtmanr@gmail.com"); got != models.OrgRoleOwner {
t.Fatalf("member role=%q, want %q", got, models.OrgRoleOwner)
}
}
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_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_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_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)
}
}
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)
}
}
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, "//install.sh") {
t.Fatalf("install command preserved double-slash install path: %s", got)
}
}
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_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_QuickSecuritySetupBootstrapRetrievalGuidance(t *testing.T) {
tempDir := t.TempDir()
cfg := &config.Config{
DataPath: tempDir,
ConfigPath: tempDir,
}
router := &Router{
config: cfg,
persistence: config.NewConfigPersistence(cfg.DataPath),
}
router.initializeBootstrapToken()
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 = "198.51.100.40:54321"
rec := httptest.NewRecorder()
authLimiter.Reset("198.51.100.40")
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)
}
}
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_SecurityStatusPresentationPolicyDefaultsClosedOutsideDemo(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": false,
} {
if got, _ := presentationPolicy[key].(bool); got != want {
t.Fatalf("presentationPolicy.%s = %v, want %v", key, presentationPolicy[key], want)
}
}
}
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_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",
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_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_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",
BlockedAt: &blockedAt,
QuickstartCreditsRemaining: 7,
QuickstartCreditsTotal: pkglicensing.QuickstartCreditsTotal,
UsingQuickstart: true,
LicenseRequired: true,
LicenseStatus: "none",
UpgradeURL: "https://pulserelay.pro/upgrade?feature=ai_autofix",
}
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_at":"2026-03-12T09:45:00Z",
"quickstart_credits_remaining":7,
"quickstart_credits_total":25,
"using_quickstart":true,
"license_required":true,
"license_status":"none",
"upgrade_url":"https://pulserelay.pro/upgrade?feature=ai_autofix",
"summary":{"critical":1,"warning":2,"watch":0,"info":4}
}`
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: "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",
Risk: "high",
Description: "Restart web service",
}),
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","risk":"high","description":"Restart web service"}}`,
},
{
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":{}}}`
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)
}
}
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},
},
}
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}}
}`
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_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"`
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",
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",
"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_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",
},
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",
},
},
},
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"
},
"approvals":[
{
"actor":"admin@example.com",
"method":"ui",
"timestamp":"2026-03-18T16:01:00Z",
"outcome":"approved",
"reason":"approved"
}
],
"result":{
"success":true,
"output":"done"
}
}
],
"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)
}
}
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) {
t.Helper()
var gotCompact bytes.Buffer
var wantCompact bytes.Buffer
if err := json.Compact(&gotCompact, got); err != nil {
t.Fatalf("compact got json: %v", err)
}
if err := json.Compact(&wantCompact, []byte(want)); 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())
}
}