package ai import ( "strings" "testing" "time" "github.com/rcourtman/pulse-go-rewrite/internal/agentexec" "github.com/rcourtman/pulse-go-rewrite/internal/truenas" unifiedresources "github.com/rcourtman/pulse-go-rewrite/internal/unifiedresources" ) func TestBuildUnifiedResourceContext_NilProvider(t *testing.T) { s := &Service{} if got := s.buildUnifiedResourceContext(); got != "" { t.Errorf("expected empty context, got %q", got) } } func TestBuildUnifiedResourceContext_FullContext(t *testing.T) { nodeWithAgent := unifiedresources.Resource{ ID: "node-1", Name: "pve-node", Type: unifiedresources.ResourceTypeAgent, Status: unifiedresources.StatusOnline, Identity: unifiedresources.ResourceIdentity{ ClusterName: "cluster-a", }, Metrics: &unifiedresources.ResourceMetrics{ CPU: &unifiedresources.MetricValue{Percent: 12.3}, Memory: &unifiedresources.MetricValue{Percent: 45.6}, }, Proxmox: &unifiedresources.ProxmoxData{ NodeName: "pve-node", ClusterName: "cluster-a", }, } nodeNoAgent := unifiedresources.Resource{ ID: "node-2", Name: "minipc", Type: unifiedresources.ResourceTypeAgent, Status: unifiedresources.StatusWarning, Proxmox: &unifiedresources.ProxmoxData{NodeName: "minipc"}, } dockerNode := unifiedresources.Resource{ ID: "dock-node", Name: "dock-node", Type: unifiedresources.ResourceTypeAgent, Status: unifiedresources.StatusOnline, Docker: &unifiedresources.DockerData{Hostname: "dock-node"}, } host := unifiedresources.Resource{ ID: "host-1", Name: "barehost", Type: unifiedresources.ResourceTypeAgent, Status: unifiedresources.StatusOnline, Identity: unifiedresources.ResourceIdentity{ IPAddresses: []string{"192.168.1.10"}, }, Metrics: &unifiedresources.ResourceMetrics{ CPU: &unifiedresources.MetricValue{Percent: 5}, Memory: &unifiedresources.MetricValue{Percent: 10}, }, Agent: &unifiedresources.AgentData{ Hostname: "barehost", }, } dockerHost := unifiedresources.Resource{ ID: "docker-1", Name: "dockhost", Type: unifiedresources.ResourceTypeAgent, Status: unifiedresources.StatusOnline, Docker: &unifiedresources.DockerData{Hostname: "dockhost"}, } nodeWithAgentID := nodeWithAgent.ID dockerHostID := dockerHost.ID unknownParentID := "unknown-parent" vm := unifiedresources.Resource{ ID: "vm-100", Name: "web-vm", Type: unifiedresources.ResourceTypeVM, ParentID: &nodeWithAgentID, Status: unifiedresources.StatusOnline, Identity: unifiedresources.ResourceIdentity{ IPAddresses: []string{"10.0.0.1", "10.0.0.2", "10.0.0.3"}, }, Metrics: &unifiedresources.ResourceMetrics{ CPU: &unifiedresources.MetricValue{Percent: 65.4}, Memory: &unifiedresources.MetricValue{Percent: 70.2}, }, Proxmox: &unifiedresources.ProxmoxData{ VMID: 100, }, } ct := unifiedresources.Resource{ ID: "ct-200", Name: "db-ct", Type: unifiedresources.ResourceTypeSystemContainer, ParentID: &nodeWithAgentID, Status: unifiedresources.StatusOffline, Proxmox: &unifiedresources.ProxmoxData{ VMID: 200, }, } dockerContainer := unifiedresources.Resource{ ID: "dock-300", Name: "redis", Type: unifiedresources.ResourceTypeAppContainer, ParentID: &dockerHostID, Status: unifiedresources.StatusOnline, Metrics: &unifiedresources.ResourceMetrics{ Disk: &unifiedresources.MetricValue{Percent: 70}, }, } dockerStopped := unifiedresources.Resource{ ID: "dock-301", Name: "cache", Type: unifiedresources.ResourceTypeAppContainer, ParentID: &dockerHostID, Status: unifiedresources.StatusOffline, } unknownParent := unifiedresources.Resource{ ID: "vm-999", Name: "mystery", Type: unifiedresources.ResourceTypeVM, ParentID: &unknownParentID, Status: unifiedresources.StatusOnline, Proxmox: &unifiedresources.ProxmoxData{ VMID: 999, }, } orphan := unifiedresources.Resource{ ID: "orphan-1", Name: "orphan", Type: unifiedresources.ResourceTypeSystemContainer, Status: unifiedresources.StatusOnline, Identity: unifiedresources.ResourceIdentity{ IPAddresses: []string{"172.16.0.5"}, }, } infrastructure := []unifiedresources.Resource{nodeWithAgent, nodeNoAgent, host, dockerHost, dockerNode} workloads := []unifiedresources.Resource{vm, ct, dockerContainer, dockerStopped, unknownParent, orphan} all := append(append([]unifiedresources.Resource{}, infrastructure...), workloads...) stats := unifiedresources.ResourceStats{ Total: len(all), ByType: map[unifiedresources.ResourceType]int{ unifiedresources.ResourceTypeAgent: 5, unifiedresources.ResourceTypeVM: 2, unifiedresources.ResourceTypeSystemContainer: 2, unifiedresources.ResourceTypeAppContainer: 2, }, ByStatus: map[unifiedresources.ResourceStatus]int{ unifiedresources.StatusOnline: 7, unifiedresources.StatusWarning: 1, unifiedresources.StatusOffline: 3, }, BySource: map[unifiedresources.DataSource]int{ unifiedresources.SourceProxmox: 4, unifiedresources.SourceAgent: 1, unifiedresources.SourceDocker: 2, }, } mockURP := &mockUnifiedResourceProvider{ getStatsFunc: func() unifiedresources.ResourceStats { return stats }, getInfrastructureFunc: func() []unifiedresources.Resource { return infrastructure }, getWorkloadsFunc: func() []unifiedresources.Resource { return workloads }, getAllFunc: func() []unifiedresources.Resource { return all }, getTopCPUFunc: func(limit int, types []unifiedresources.ResourceType) []unifiedresources.Resource { return []unifiedresources.Resource{vm} }, getTopMemoryFunc: func(limit int, types []unifiedresources.ResourceType) []unifiedresources.Resource { return []unifiedresources.Resource{host} }, getTopDiskFunc: func(limit int, types []unifiedresources.ResourceType) []unifiedresources.Resource { return []unifiedresources.Resource{dockerContainer} }, } s := &Service{ unifiedResourceProvider: mockURP, alertProvider: &resourceContextAlertProvider{ active: []AlertInfo{ { ResourceID: vm.ID, ResourceName: vm.Name, Message: "CPU high", Level: "critical", }, }, }, } s.agentServer = &mockAgentServer{ agents: []agentexec.ConnectedAgent{ {AgentID: "agent-1", Hostname: "pve-node"}, }, } got := s.buildUnifiedResourceContext() if got == "" { t.Fatal("expected non-empty context") } assertContains := func(substr string) { t.Helper() if !strings.Contains(got, substr) { t.Fatalf("expected context to contain %q", substr) } } assertContains("## Unified Infrastructure View") assertContains("Total resources: 11 (Infrastructure: 5, Workloads: 6)") assertContains("Data Governance") expectedSensitivity := strings.Join(unifiedresources.ResourcePolicySensitivitySummaryFromCounts(map[unifiedresources.ResourceSensitivity]int{ unifiedresources.ResourceSensitivityPublic: 0, unifiedresources.ResourceSensitivityInternal: 5, unifiedresources.ResourceSensitivitySensitive: 6, unifiedresources.ResourceSensitivityRestricted: 0, }), ", ") assertContains("Sensitivity: " + expectedSensitivity) expectedRouting := strings.Join(unifiedresources.ResourcePolicyRoutingSummaryFromCounts(map[unifiedresources.ResourceRoutingScope]int{ unifiedresources.ResourceRoutingScopeCloudSummary: 5, unifiedresources.ResourceRoutingScopeLocalFirst: 6, unifiedresources.ResourceRoutingScopeLocalOnly: 0, }), ", ") assertContains("Routing: " + expectedRouting) assertContains("Policy Redaction Hints") assertContains("Redactions in use: Hostname, IP Address, Platform ID, Alias") if count := strings.Count(got, "### Policy Redaction Hints"); count != 1 { t.Fatalf("expected one policy redaction hints section, got %d in %q", count, got) } assertContains("Proxmox VE Nodes") assertContains("HAS AGENT") assertContains("NO AGENT") assertContains("cluster: cluster-a") assertContains("Standalone Hosts") assertContains("192.168.1.10") assertContains("Docker/Podman Hosts") assertContains("1/2 containers running") assertContains("Workloads (VMs & Containers)") assertContains("On pve-node") assertContains("On unresolved parent resource") assertContains("Other workloads") assertContains("virtual machine resource; status online; linked to parent resource; redacted for cloud summary") assertContains("IPs " + unifiedresources.ResourcePolicyRedactedLabel) assertContains("Resources with Active Alerts") assertContains("CPU high") assertContains("Infrastructure Summary") assertContains("Resources with alerts: 1") assertContains("Average utilization by type") assertContains("Top CPU Consumers") assertContains("virtual machine resource; status online; linked to parent resource; redacted for cloud summary** (vm): 65.4%") assertContains("Top Memory Consumers") assertContains("Top Disk Usage") assertContains("application container resource; status online; linked to parent resource; redacted for cloud summary** (app-container): 70.0%") if strings.Contains(got, "web-vm") { t.Fatalf("expected sensitive VM name to be redacted, got %q", got) } if strings.Contains(got, "10.0.0.1") || strings.Contains(got, "10.0.0.2") { t.Fatalf("expected sensitive workload IPs to be redacted, got %q", got) } } func TestBuildUnifiedResourceContextUsesAISafeSummaryForLocalOnlyResources(t *testing.T) { restricted := unifiedresources.Resource{ ID: "mail-1", Name: "customer-mail-gateway", Type: unifiedresources.ResourceTypePMG, Status: unifiedresources.StatusWarning, PMG: &unifiedresources.PMGData{ Hostname: "mail-gateway.internal", }, } unifiedresources.RefreshCanonicalMetadata(&restricted) stats := unifiedresources.ResourceStats{ Total: 1, ByType: map[unifiedresources.ResourceType]int{ unifiedresources.ResourceTypePMG: 1, }, ByStatus: map[unifiedresources.ResourceStatus]int{ unifiedresources.StatusWarning: 1, }, } mockURP := &mockUnifiedResourceProvider{ getStatsFunc: func() unifiedresources.ResourceStats { return stats }, getInfrastructureFunc: func() []unifiedresources.Resource { return []unifiedresources.Resource{restricted} }, getAllFunc: func() []unifiedresources.Resource { return []unifiedresources.Resource{restricted} }, } s := &Service{unifiedResourceProvider: mockURP} got := s.buildUnifiedResourceContext() if !strings.Contains(got, "mail gateway resource; status warning; local-only context") { t.Fatalf("expected AI-safe summary in context, got %q", got) } if strings.Contains(got, "customer-mail-gateway") { t.Fatalf("expected raw restricted resource name to be hidden, got %q", got) } } func TestBuildUnifiedResourceContextForExternalModelOmitsLocalOnlyResourceDetails(t *testing.T) { restricted := unifiedresources.Resource{ ID: "mail-1", Name: "customer-mail-gateway", Type: unifiedresources.ResourceTypePMG, Status: unifiedresources.StatusWarning, PMG: &unifiedresources.PMGData{ Hostname: "mail-gateway.internal", }, } publicNode := unifiedresources.Resource{ ID: "public-1", Name: "public-node", Type: unifiedresources.ResourceTypeAgent, Status: unifiedresources.StatusOnline, Tags: []string{"public"}, } resources := unifiedresources.RefreshCanonicalMetadataSlice([]unifiedresources.Resource{restricted, publicNode}) stats := unifiedresources.ResourceStats{ Total: 2, ByType: map[unifiedresources.ResourceType]int{ unifiedresources.ResourceTypePMG: 1, unifiedresources.ResourceTypeAgent: 1, }, ByStatus: map[unifiedresources.ResourceStatus]int{ unifiedresources.StatusWarning: 1, unifiedresources.StatusOnline: 1, }, } mockURP := &mockUnifiedResourceProvider{ getStatsFunc: func() unifiedresources.ResourceStats { return stats }, getInfrastructureFunc: func() []unifiedresources.Resource { return resources }, getAllFunc: func() []unifiedresources.Resource { return resources }, getTopCPUFunc: func(limit int, types []unifiedresources.ResourceType) []unifiedresources.Resource { return []unifiedresources.Resource{restricted} }, } s := &Service{ unifiedResourceProvider: mockURP, alertProvider: &resourceContextAlertProvider{ active: []AlertInfo{ { ResourceID: restricted.ID, ResourceName: restricted.Name, Message: "customer-mail-gateway at mail-gateway.internal is degraded", Level: "warning", }, }, }, } got := s.buildUnifiedResourceContextForModel("openai:gpt-4o") for _, raw := range []string{"customer-mail-gateway", "mail-gateway.internal", "mail gateway resource; status warning; local-only context"} { if strings.Contains(got, raw) { t.Fatalf("expected external context to omit local-only detail %q, got %q", raw, got) } } if !strings.Contains(got, "Local-only resources: 1") { t.Fatalf("expected aggregate local-only count, got %q", got) } if !strings.Contains(got, "External model handling: 1 local-only resources are represented only in aggregate and omitted from detailed context.") { t.Fatalf("expected external model handling note, got %q", got) } if !strings.Contains(got, "1 alerts on local-only resources omitted from external model context by policy.") { t.Fatalf("expected local-only alert omission note, got %q", got) } if strings.Contains(got, "Top CPU Consumers") { t.Fatalf("expected local-only top consumers to be omitted, got %q", got) } } func TestBuildUnifiedResourceContextUsesAISafeSummaryForSensitiveResources(t *testing.T) { sensitiveVM := unifiedresources.Resource{ ID: "prod-west:101", Name: "finance-db", Type: unifiedresources.ResourceTypeVM, Status: unifiedresources.StatusOnline, Identity: unifiedresources.ResourceIdentity{ ClusterName: "prod-west", IPAddresses: []string{"10.10.0.5"}, }, Proxmox: &unifiedresources.ProxmoxData{ NodeName: "pve-prod", ClusterName: "prod-west", VMID: 101, }, } unifiedresources.RefreshCanonicalMetadata(&sensitiveVM) stats := unifiedresources.ResourceStats{ Total: 1, ByType: map[unifiedresources.ResourceType]int{ unifiedresources.ResourceTypeVM: 1, }, ByStatus: map[unifiedresources.ResourceStatus]int{ unifiedresources.StatusOnline: 1, }, } mockURP := &mockUnifiedResourceProvider{ getStatsFunc: func() unifiedresources.ResourceStats { return stats }, getWorkloadsFunc: func() []unifiedresources.Resource { return []unifiedresources.Resource{sensitiveVM} }, getAllFunc: func() []unifiedresources.Resource { return []unifiedresources.Resource{sensitiveVM} }, getTopCPUFunc: func(limit int, types []unifiedresources.ResourceType) []unifiedresources.Resource { return []unifiedresources.Resource{sensitiveVM} }, } s := &Service{unifiedResourceProvider: mockURP} got := s.buildUnifiedResourceContext() if !strings.Contains(got, "virtual machine resource; status online; redacted for cloud summary") { t.Fatalf("expected AI-safe summary for sensitive resource, got %q", got) } if !strings.Contains(got, "IPs "+unifiedresources.ResourcePolicyRedactedLabel) { t.Fatalf("expected sensitive IP redaction marker, got %q", got) } if strings.Contains(got, "finance-db") { t.Fatalf("expected sensitive resource name to be hidden, got %q", got) } if strings.Contains(got, "10.10.0.5") { t.Fatalf("expected sensitive IP to be hidden, got %q", got) } if strings.Contains(got, "prod-west") { t.Fatalf("expected sensitive cluster name to be hidden, got %q", got) } } func TestBuildUnifiedResourceContextRedactsSensitiveAlertMessageText(t *testing.T) { sensitiveVM := unifiedresources.Resource{ ID: "prod-west:101", Name: "finance-db", Type: unifiedresources.ResourceTypeVM, Status: unifiedresources.StatusWarning, Identity: unifiedresources.ResourceIdentity{ Hostnames: []string{"finance-db.internal"}, IPAddresses: []string{"10.10.0.5"}, }, Canonical: &unifiedresources.CanonicalIdentity{ PlatformID: "prod-west:101", Aliases: []string{"finance-db-primary"}, }, } unifiedresources.RefreshCanonicalMetadata(&sensitiveVM) stats := unifiedresources.ResourceStats{ Total: 1, ByType: map[unifiedresources.ResourceType]int{ unifiedresources.ResourceTypeVM: 1, }, ByStatus: map[unifiedresources.ResourceStatus]int{ unifiedresources.StatusWarning: 1, }, } mockURP := &mockUnifiedResourceProvider{ getStatsFunc: func() unifiedresources.ResourceStats { return stats }, getWorkloadsFunc: func() []unifiedresources.Resource { return []unifiedresources.Resource{sensitiveVM} }, getAllFunc: func() []unifiedresources.Resource { return []unifiedresources.Resource{sensitiveVM} }, } s := &Service{ unifiedResourceProvider: mockURP, alertProvider: &resourceContextAlertProvider{ active: []AlertInfo{ { ResourceID: sensitiveVM.ID, ResourceName: sensitiveVM.Name, Message: "finance-db at finance-db.internal / 10.10.0.5 on prod-west:101 is degraded", Level: "critical", }, }, }, } got := s.buildUnifiedResourceContextForModel("anthropic:claude-3-5-sonnet-latest") for _, raw := range []string{"finance-db", "finance-db.internal", "10.10.0.5", "prod-west:101"} { if strings.Contains(got, raw) { t.Fatalf("expected sensitive alert text to redact %q, got %q", raw, got) } } if !strings.Contains(got, unifiedresources.ResourcePolicyRedactedLabel) { t.Fatalf("expected redaction marker in alert context, got %q", got) } } func TestBuildUnifiedResourceContextUsesAISafeSummaryForPathRedactedResources(t *testing.T) { storage := unifiedresources.Resource{ ID: "storage-1", Name: "backup-volume", Type: unifiedresources.ResourceTypeStorage, Status: unifiedresources.StatusOnline, Storage: &unifiedresources.StorageMeta{ Path: "/mnt/pve/backups", }, } unifiedresources.RefreshCanonicalMetadata(&storage) stats := unifiedresources.ResourceStats{ Total: 1, ByType: map[unifiedresources.ResourceType]int{ unifiedresources.ResourceTypeStorage: 1, }, ByStatus: map[unifiedresources.ResourceStatus]int{ unifiedresources.StatusOnline: 1, }, } mockURP := &mockUnifiedResourceProvider{ getStatsFunc: func() unifiedresources.ResourceStats { return stats }, getInfrastructureFunc: func() []unifiedresources.Resource { return []unifiedresources.Resource{storage} }, getAllFunc: func() []unifiedresources.Resource { return []unifiedresources.Resource{storage} }, } s := &Service{unifiedResourceProvider: mockURP} got := s.buildUnifiedResourceContext() if !strings.Contains(got, "storage resource; status online; redacted for cloud summary") { t.Fatalf("expected AI-safe summary for path-redacted resource, got %q", got) } if strings.Contains(got, "backup-volume") { t.Fatalf("expected path-redacted resource name to be hidden, got %q", got) } } func TestResourcePolicyHelpersShareAISafeSummaryDecision(t *testing.T) { policy := &unifiedresources.ResourcePolicy{ Sensitivity: unifiedresources.ResourceSensitivitySensitive, Routing: unifiedresources.ResourceRoutingPolicy{ Scope: unifiedresources.ResourceRoutingScopeLocalFirst, Redact: []unifiedresources.ResourceRedactionHint{ unifiedresources.ResourceRedactionHostname, unifiedresources.ResourceRedactionIPAddress, }, }, } if !unifiedresources.ResourcePolicyUsesAISafeSummary("virtual machine resource; status online; redacted for cloud summary", policy) { t.Fatal("expected AI-safe summary helper to honor shared policy rules") } if !unifiedresources.ResourcePolicyRedacts(policy, unifiedresources.ResourceRedactionHostname) { t.Fatal("expected hostname redaction helper to honor shared policy rules") } if unifiedresources.ResourcePolicyRedacts(policy, unifiedresources.ResourceRedactionAlias) { t.Fatal("did not expect alias redaction helper to match") } } func TestUnifiedResourceDisplayNameUsesSharedHelper(t *testing.T) { if got := unifiedresources.ResourceDisplayName(unifiedresources.Resource{Name: " node-a ", ID: "id-a"}); got != "node-a" { t.Fatalf("ResourceDisplayName() with name = %q, want node-a", got) } if got := unifiedresources.ResourceDisplayName(unifiedresources.Resource{Name: " ", ID: " id-b "}); got != "id-b" { t.Fatalf("ResourceDisplayName() with fallback ID = %q, want id-b", got) } } func TestBuildUnifiedResourceContext_UnifiedPath(t *testing.T) { clusterID := "k8s-cluster-1" k8sCluster := unifiedresources.Resource{ ID: clusterID, Name: "prod-cluster", Type: unifiedresources.ResourceTypeK8sCluster, Status: unifiedresources.StatusOnline, Identity: unifiedresources.ResourceIdentity{ ClusterName: "prod-cluster", }, Kubernetes: &unifiedresources.K8sData{ ClusterID: "prod-cluster", ClusterName: "prod-cluster", }, } k8sNode := unifiedresources.Resource{ ID: "k8s-node-1", Name: "worker-1", Type: unifiedresources.ResourceTypeK8sNode, Status: unifiedresources.StatusWarning, ParentID: &clusterID, Identity: unifiedresources.ResourceIdentity{ ClusterName: "prod-cluster", }, Metrics: &unifiedresources.ResourceMetrics{ CPU: &unifiedresources.MetricValue{Percent: 91}, Memory: &unifiedresources.MetricValue{Percent: 77}, }, Kubernetes: &unifiedresources.K8sData{ ClusterID: "prod-cluster", ClusterName: "prod-cluster", }, } all := []unifiedresources.Resource{k8sCluster, k8sNode} stats := unifiedresources.ResourceStats{ Total: len(all), ByType: map[unifiedresources.ResourceType]int{ unifiedresources.ResourceTypeK8sCluster: 1, unifiedresources.ResourceTypeK8sNode: 1, }, ByStatus: map[unifiedresources.ResourceStatus]int{ unifiedresources.StatusOnline: 1, unifiedresources.StatusWarning: 1, }, BySource: map[unifiedresources.DataSource]int{ unifiedresources.SourceK8s: 2, }, } unifiedProvider := &mockUnifiedResourceProvider{ getStatsFunc: func() unifiedresources.ResourceStats { return stats }, getInfrastructureFunc: func() []unifiedresources.Resource { return all }, getAllFunc: func() []unifiedresources.Resource { return all }, getTopCPUFunc: func(limit int, types []unifiedresources.ResourceType) []unifiedresources.Resource { return []unifiedresources.Resource{k8sNode} }, } s := &Service{ unifiedResourceProvider: unifiedProvider, alertProvider: &resourceContextAlertProvider{ active: []AlertInfo{ { ResourceID: k8sNode.ID, ResourceName: "wrong-fallback-name", Message: "Node not ready", Level: "warning", }, }, }, } got := s.buildUnifiedResourceContext() if got == "" { t.Fatal("expected non-empty unified context") } assertContains := func(substr string) { t.Helper() if !strings.Contains(got, substr) { t.Fatalf("expected context to contain %q", substr) } } assertContains("Total resources: 2 (Infrastructure: 2, Workloads: 0)") assertContains("Kubernetes") assertContains("prod-cluster") assertContains("worker-1") assertContains("Resources with Active Alerts") assertContains("Node not ready") } func TestBuildUnifiedResourceContextIncludesTrueNASResources(t *testing.T) { previous := truenas.IsFeatureEnabled() truenas.SetFeatureEnabled(true) t.Cleanup(func() { truenas.SetFeatureEnabled(previous) }) registry := unifiedresources.NewRegistry(unifiedresources.NewMemoryStore()) records := truenas.NewDefaultProvider().Records() if len(records) == 0 { t.Fatal("expected truenas fixture records") } registry.IngestRecords(unifiedresources.SourceTrueNAS, records) adapter := unifiedresources.NewUnifiedAIAdapter(registry) s := &Service{} s.SetUnifiedResourceProvider(adapter) got := s.buildUnifiedResourceContext() if got == "" { t.Fatal("expected non-empty context") } if !strings.Contains(got, "TrueNAS Systems") { t.Fatalf("expected context to include TrueNAS section, got %q", got) } if !strings.Contains(got, "### Storage") { t.Fatalf("expected context to include storage section, got %q", got) } if !strings.Contains(got, "Storage Pools") { t.Fatalf("expected context to include storage pools section, got %q", got) } if !strings.Contains(got, "Physical Disks Needing Attention") { t.Fatalf("expected context to include disk attention section, got %q", got) } if strings.Contains(got, "tank/apps") { t.Fatalf("expected storage pool section to exclude dataset names, got %q", got) } if !strings.Contains(got, "agent resource; status warning; sources truenas") { t.Fatalf("expected context to include governed truenas summary, got %q", got) } if strings.Contains(got, "truenas-main") { t.Fatalf("expected raw truenas host name to be redacted, got %q", got) } } func TestBuildUnifiedResourceContext_TruncatesLargeContext(t *testing.T) { largeName := strings.Repeat("a", 60000) node := unifiedresources.Resource{ ID: "node-1", Name: largeName, Type: unifiedresources.ResourceTypeAgent, Status: unifiedresources.StatusOnline, } stats := unifiedresources.ResourceStats{ Total: 1, ByType: map[unifiedresources.ResourceType]int{ unifiedresources.ResourceTypeAgent: 1, }, } mockURP := &mockUnifiedResourceProvider{ getStatsFunc: func() unifiedresources.ResourceStats { return stats }, getInfrastructureFunc: func() []unifiedresources.Resource { return []unifiedresources.Resource{node} }, getWorkloadsFunc: func() []unifiedresources.Resource { return nil }, getAllFunc: func() []unifiedresources.Resource { return []unifiedresources.Resource{node} }, } s := &Service{unifiedResourceProvider: mockURP} got := s.buildUnifiedResourceContext() if !strings.Contains(got, "[... Context truncated ...]") { t.Fatal("expected context to be truncated") } if len(got) <= 50000 { t.Fatalf("expected truncated context length > 50000, got %d", len(got)) } } func TestResourceContextUnifiedProvider_NilRegistry(t *testing.T) { adapter := unifiedresources.NewUnifiedAIAdapter(nil) if got := adapter.GetAll(); len(got) != 0 { t.Fatalf("GetAll() len = %d, want 0", len(got)) } if got := adapter.GetInfrastructure(); len(got) != 0 { t.Fatalf("GetInfrastructure() len = %d, want 0", len(got)) } if got := adapter.GetWorkloads(); len(got) != 0 { t.Fatalf("GetWorkloads() len = %d, want 0", len(got)) } if got := adapter.GetByType(unifiedresources.ResourceTypeAgent); len(got) != 0 { t.Fatalf("GetByType(host) len = %d, want 0", len(got)) } if got := adapter.GetTopByCPU(3, nil); len(got) != 0 { t.Fatalf("GetTopByCPU() len = %d, want 0", len(got)) } if got := adapter.GetRelated("missing"); len(got) != 0 { t.Fatalf("GetRelated(missing) len = %d, want 0", len(got)) } if got := adapter.FindContainerHost("missing"); got != "" { t.Fatalf("FindContainerHost(missing) = %q, want empty", got) } if stats := adapter.GetStats(); stats.Total != 0 { t.Fatalf("GetStats().Total = %d, want 0", stats.Total) } } func TestResourceContextUnifiedProvider_ResourceCounts(t *testing.T) { registry := unifiedresources.NewRegistry(nil) now := time.Now().UTC() registry.IngestRecords(unifiedresources.SourceAgent, []unifiedresources.IngestRecord{ { SourceID: "host-1", Resource: unifiedresources.Resource{ Type: unifiedresources.ResourceTypeAgent, Name: "host-1", Status: unifiedresources.StatusOnline, LastSeen: now, }, }, }) registry.IngestRecords(unifiedresources.SourceProxmox, []unifiedresources.IngestRecord{ { SourceID: "vm-1", Resource: unifiedresources.Resource{ Type: unifiedresources.ResourceTypeVM, Name: "vm-1", Status: unifiedresources.StatusOnline, LastSeen: now, }, }, { SourceID: "ct-1", Resource: unifiedresources.Resource{ Type: unifiedresources.ResourceTypeSystemContainer, Name: "ct-1", Status: unifiedresources.StatusOnline, LastSeen: now, }, }, }) unified := unifiedresources.NewUnifiedAIAdapter(registry) if got, want := len(unified.GetAll()), 3; got != want { t.Fatalf("GetAll() count mismatch: got=%d want=%d", got, want) } } func TestResourceContextUnifiedProvider_InfrastructureWorkloadSplit(t *testing.T) { registry := unifiedresources.NewRegistry(nil) now := time.Now().UTC() registry.IngestRecords(unifiedresources.SourceAgent, []unifiedresources.IngestRecord{ { SourceID: "host-1", Resource: unifiedresources.Resource{ Type: unifiedresources.ResourceTypeAgent, Name: "host-1", Status: unifiedresources.StatusOnline, LastSeen: now, }, }, }) registry.IngestRecords(unifiedresources.SourceProxmox, []unifiedresources.IngestRecord{ { SourceID: "vm-1", Resource: unifiedresources.Resource{ Type: unifiedresources.ResourceTypeVM, Name: "vm-1", Status: unifiedresources.StatusOnline, LastSeen: now, }, }, { SourceID: "ct-1", Resource: unifiedresources.Resource{ Type: unifiedresources.ResourceTypeSystemContainer, Name: "ct-1", Status: unifiedresources.StatusOnline, LastSeen: now, }, }, }) unified := unifiedresources.NewUnifiedAIAdapter(registry) if got, want := len(unified.GetInfrastructure()), 1; got != want { t.Fatalf("GetInfrastructure() count mismatch: got=%d want=%d", got, want) } if got, want := len(unified.GetWorkloads()), 2; got != want { t.Fatalf("GetWorkloads() count mismatch: got=%d want=%d", got, want) } } func TestResourceContextUnifiedProvider_TopCPU(t *testing.T) { registry := unifiedresources.NewRegistry(nil) now := time.Now().UTC() registry.IngestRecords(unifiedresources.SourceAgent, []unifiedresources.IngestRecord{ { SourceID: "host-low", Resource: unifiedresources.Resource{ Type: unifiedresources.ResourceTypeAgent, Name: "host-low", Status: unifiedresources.StatusOnline, LastSeen: now, Metrics: &unifiedresources.ResourceMetrics{ CPU: &unifiedresources.MetricValue{Percent: 25}, }, }, }, }) registry.IngestRecords(unifiedresources.SourceProxmox, []unifiedresources.IngestRecord{ { SourceID: "vm-high", Resource: unifiedresources.Resource{ Type: unifiedresources.ResourceTypeVM, Name: "vm-high", Status: unifiedresources.StatusOnline, LastSeen: now, Metrics: &unifiedresources.ResourceMetrics{ CPU: &unifiedresources.MetricValue{Percent: 92}, }, }, }, }) registry.IngestRecords(unifiedresources.SourceDocker, []unifiedresources.IngestRecord{ { SourceID: "ct-mid", Resource: unifiedresources.Resource{ Type: unifiedresources.ResourceTypeAppContainer, Name: "ct-mid", Status: unifiedresources.StatusOnline, LastSeen: now, Metrics: &unifiedresources.ResourceMetrics{ CPU: &unifiedresources.MetricValue{Percent: 61}, }, }, }, }) unified := unifiedresources.NewUnifiedAIAdapter(registry) top := unified.GetTopByCPU(2, []unifiedresources.ResourceType{ unifiedresources.ResourceTypeAgent, unifiedresources.ResourceTypeVM, unifiedresources.ResourceTypeAppContainer, }) if len(top) != 2 { t.Fatalf("GetTopByCPU() len = %d, want 2", len(top)) } if top[0].Name != "vm-high" || top[1].Name != "ct-mid" { t.Fatalf("unexpected top CPU ordering: got [%s, %s]", top[0].Name, top[1].Name) } } func TestResourceContextUnifiedProvider_FindContainerHost(t *testing.T) { registry := unifiedresources.NewRegistry(nil) now := time.Now().UTC() registry.IngestRecords(unifiedresources.SourceDocker, []unifiedresources.IngestRecord{ { SourceID: "docker-host-1", Resource: unifiedresources.Resource{ Type: unifiedresources.ResourceTypeAgent, Name: "docker-node-1", Status: unifiedresources.StatusOnline, LastSeen: now, }, }, { SourceID: "container-1", ParentSourceID: "docker-host-1", Resource: unifiedresources.Resource{ Type: unifiedresources.ResourceTypeAppContainer, Name: "web", Status: unifiedresources.StatusOnline, LastSeen: now, Docker: &unifiedresources.DockerData{ContainerID: "abc123"}, }, }, }) unified := unifiedresources.NewUnifiedAIAdapter(registry) if got := unified.FindContainerHost("web"); got != "docker-node-1" { t.Fatalf("FindContainerHost(web) = %q, want docker-node-1", got) } if got := unified.FindContainerHost("abc123"); got != "docker-node-1" { t.Fatalf("FindContainerHost(abc123) = %q, want docker-node-1", got) } } type resourceContextAlertProvider struct { active []AlertInfo history []ResolvedAlertInfo } func (m *resourceContextAlertProvider) GetActiveAlerts() []AlertInfo { return m.active } func (m *resourceContextAlertProvider) GetRecentlyResolved(minutes int) []ResolvedAlertInfo { return m.history } func (m *resourceContextAlertProvider) GetAlertsByResource(resourceID string) []AlertInfo { out := make([]AlertInfo, 0) for _, alert := range m.active { if alert.ResourceID == resourceID { out = append(out, alert) } } return out } func (m *resourceContextAlertProvider) GetAlertHistory(resourceID string, limit int) []ResolvedAlertInfo { out := make([]ResolvedAlertInfo, 0) for _, alert := range m.history { if alert.ResourceID == resourceID { out = append(out, alert) } if limit > 0 && len(out) >= limit { break } } return out }