Pulse/internal/unifiedresources/registry_test.go
2026-07-07 09:46:51 +01:00

4833 lines
149 KiB
Go

package unifiedresources
import (
"reflect"
"strings"
"testing"
"time"
"github.com/rcourtman/pulse-go-rewrite/internal/models"
"github.com/rcourtman/pulse-go-rewrite/internal/storagehealth"
)
// TestMemoryStore_RecordActionAuditAppliesRedaction is an integration check
// at the registry-store boundary. The MemoryStore is the backing store the
// registry uses in tests and contract examples, and operator-authored audit
// fields must be scrubbed of secret shapes uniformly across both SQLite and
// in-memory persistence so test fixtures cannot accidentally exercise an
// unredacted path that production never sees.
func TestMemoryStore_RecordActionAuditAppliesRedaction(t *testing.T) {
store := NewMemoryStore()
now := time.Now().UTC()
record := ActionAuditRecord{
ID: "act-redact",
CreatedAt: now,
UpdatedAt: now,
State: ActionStatePlanned,
Request: ActionRequest{
RequestID: "req-redact",
ResourceID: "vm-100",
CapabilityName: "pulse_control",
Reason: "rotate sk-leakedkey1234567 because it appeared in logs",
RequestedBy: "operator@example.com",
Params: map[string]any{
"command": "curl -H 'Authorization: Bearer eyJleak' https://api.example.com",
"targetType": "agent",
},
},
Plan: ActionPlan{
ActionID: "act-redact",
RequestID: "req-redact",
Allowed: true,
PlanHash: "deadbeef",
},
}
if err := store.RecordActionAudit(record); err != nil {
t.Fatalf("RecordActionAudit: %v", err)
}
stored, ok, err := store.GetActionAudit("act-redact")
if err != nil {
t.Fatalf("GetActionAudit: %v", err)
}
if !ok {
t.Fatalf("expected stored audit, got none")
}
if strings.Contains(stored.Request.Reason, "sk-leakedkey1234567") {
t.Fatalf("Reason still contains secret after persistence: %s", stored.Request.Reason)
}
if cmd, _ := stored.Request.Params["command"].(string); strings.Contains(cmd, "eyJleak") {
t.Fatalf("Params[command] still contains Bearer token: %s", cmd)
}
// Plan fields must be untouched so PlanHash-based drift detection
// continues to work.
if stored.Plan.PlanHash != "deadbeef" {
t.Fatalf("Plan.PlanHash mutated by redaction: %s", stored.Plan.PlanHash)
}
}
func TestResourceRegistry_GetByReferenceResolvesSourceIDAndCanonicalAlias(t *testing.T) {
rr := NewRegistry(nil)
rr.IngestRecords(SourceProxmox, []IngestRecord{{
SourceID: "delly:delly:101",
Resource: Resource{
Type: ResourceTypeSystemContainer,
Name: "homeassistant",
},
}})
rr.IngestResources([]Resource{{
ID: "agent-host-1",
Type: ResourceTypeAgent,
Name: "delly",
Agent: &AgentData{
AgentID: "machine-1",
Hostname: "delly.local",
},
}})
resource, resolvedID, ok := rr.GetByReference("delly:delly:101")
if !ok {
t.Fatal("expected Proxmox source ID to resolve")
}
wantID := SourceSpecificID(ResourceTypeSystemContainer, SourceProxmox, "delly:delly:101")
if resolvedID != wantID || resource.ID != wantID {
t.Fatalf("source reference resolved to id=%q resource=%q, want %q", resolvedID, resource.ID, wantID)
}
resource, resolvedID, ok = rr.GetByReference("machine-1")
if !ok {
t.Fatal("expected canonical alias to resolve")
}
if resolvedID != "agent-host-1" || resource.ID != "agent-host-1" {
t.Fatalf("alias reference resolved to id=%q resource=%q, want agent-host-1", resolvedID, resource.ID)
}
}
func TestResourceRegistryAvailabilityLinkedResourceResolvesSourceReference(t *testing.T) {
rr := NewRegistry(nil)
now := time.Now().UTC()
const serviceSourceID = "swarm-1:service:svc-api"
rr.IngestRecords(SourceDocker, []IngestRecord{{
SourceID: serviceSourceID,
Resource: Resource{
Type: ResourceTypeDockerService,
Name: "api",
Status: StatusOnline,
LastSeen: now,
},
Identity: ResourceIdentity{Hostnames: []string{"api"}},
}})
rr.IngestRecords(SourceAvailability, []IngestRecord{
availabilityProbeRecord("probe-service", "api.example.test", &AvailabilityData{
LinkedResourceID: serviceSourceID,
Address: "api.example.test",
Protocol: "tcp",
Port: 8443,
TargetKind: "service",
Enabled: true,
Available: true,
}),
})
if got := rr.ListByType(ResourceTypeNetworkEndpoint); len(got) != 0 {
t.Fatalf("expected source-referenced probe to attach (0 endpoints), got %d", len(got))
}
services := rr.ListByType(ResourceTypeDockerService)
if len(services) != 1 {
t.Fatalf("expected 1 docker service, got %d", len(services))
}
service := services[0]
if service.Availability == nil || service.Availability.TargetID != "probe-service" {
t.Fatalf("expected availability facet probe-service on docker service, got %+v", service.Availability)
}
if service.Availability.LinkedResourceID != serviceSourceID {
t.Fatalf("linkedResourceId = %q, want %q", service.Availability.LinkedResourceID, serviceSourceID)
}
if !hasDataSource(service.Sources, SourceAvailability) {
t.Fatalf("expected docker service sources to include availability, got %v", service.Sources)
}
}
func TestResourceRegistry_ListByType(t *testing.T) {
rr := NewRegistry(nil)
now := time.Date(2026, 2, 10, 0, 0, 0, 0, time.UTC)
rr.IngestRecords(SourceAgent, []IngestRecord{
{
SourceID: "host-1",
Resource: Resource{
Type: ResourceTypeAgent,
Name: "host-1",
Status: StatusOnline,
LastSeen: now,
},
Identity: ResourceIdentity{MachineID: "machine-1"},
},
})
rr.IngestRecords(SourceProxmox, []IngestRecord{
{
SourceID: "vm-100",
Resource: Resource{
Type: ResourceTypeVM,
Name: "vm-100",
Status: StatusOnline,
LastSeen: now,
},
Identity: ResourceIdentity{Hostnames: []string{"vm-100"}},
},
{
SourceID: "vm-101",
Resource: Resource{
Type: ResourceTypeVM,
Name: "vm-101",
Status: StatusOnline,
LastSeen: now,
},
Identity: ResourceIdentity{Hostnames: []string{"vm-101"}},
},
{
SourceID: "ct-200",
Resource: Resource{
Type: ResourceTypeSystemContainer,
Name: "ct-200",
Status: StatusOnline,
LastSeen: now,
},
Identity: ResourceIdentity{Hostnames: []string{"ct-200"}},
},
})
got := rr.ListByType(ResourceTypeVM)
if len(got) != 2 {
t.Fatalf("expected 2 VMs, got %d", len(got))
}
for _, r := range got {
if r.Type != ResourceTypeVM {
t.Fatalf("expected all resources to be type %q, got %q", ResourceTypeVM, r.Type)
}
}
// Deterministic ordering (sorted by ID).
wantIDs := []string{
rr.sourceSpecificID(ResourceTypeVM, SourceProxmox, "vm-100"),
rr.sourceSpecificID(ResourceTypeVM, SourceProxmox, "vm-101"),
}
// Hash order isn't meaningful; the contract is lexicographic by ID.
if wantIDs[0] > wantIDs[1] {
wantIDs[0], wantIDs[1] = wantIDs[1], wantIDs[0]
}
if got[0].ID != wantIDs[0] || got[1].ID != wantIDs[1] {
t.Fatalf("expected IDs %v, got [%s %s]", wantIDs, got[0].ID, got[1].ID)
}
// Returned resources should be copies (mutating the result does not mutate the registry).
origName := got[0].Name
got[0].Name = "mutated"
if r, ok := rr.Get(got[0].ID); !ok || r == nil {
t.Fatalf("expected Get(%q) to succeed", got[0].ID)
} else if r.Name != origName {
t.Fatalf("expected registry resource name %q, got %q", origName, r.Name)
}
}
func TestResourceRegistry_ListByType_Empty(t *testing.T) {
rr := NewRegistry(nil)
if got := rr.ListByType(ResourceTypeVM); len(got) != 0 {
t.Fatalf("expected empty result, got %d", len(got))
}
}
func TestResourceRegistry_IngestRecordsPreservesAvailabilityEndpoints(t *testing.T) {
rr := NewRegistry(nil)
now := time.Date(2026, 5, 6, 12, 0, 0, 0, time.UTC)
rr.IngestRecords(SourceAvailability, []IngestRecord{
{
SourceID: "energy-meter",
Resource: Resource{
Type: ResourceTypeNetworkEndpoint,
Name: "Energy meter",
Status: StatusOffline,
LastSeen: now,
Sources: []DataSource{SourceAvailability},
Availability: &AvailabilityData{
TargetID: "energy-meter",
Address: "192.0.2.44",
Protocol: "icmp",
Enabled: true,
Available: false,
ConsecutiveFailures: 2,
FailureThreshold: 2,
},
Incidents: []ResourceIncident{{
Provider: "availability",
NativeID: "energy-meter",
Code: "availability_unreachable",
Severity: storagehealth.RiskCritical,
Source: "availability",
Summary: "Energy meter is unreachable by ICMP probe",
}},
},
Identity: ResourceIdentity{IPAddresses: []string{"192.0.2.44"}},
},
})
got := rr.ListByType(ResourceTypeNetworkEndpoint)
if len(got) != 1 {
t.Fatalf("expected 1 network endpoint, got %d", len(got))
}
resource := got[0]
if resource.Availability == nil {
t.Fatal("expected availability metadata")
}
if resource.Availability.TargetID != "energy-meter" {
t.Fatalf("target id = %q, want energy-meter", resource.Availability.TargetID)
}
if resource.Status != StatusOffline {
t.Fatalf("status = %q, want %q", resource.Status, StatusOffline)
}
if len(resource.Incidents) != 1 || resource.Incidents[0].Code != "availability_unreachable" {
t.Fatalf("expected availability incident, got %+v", resource.Incidents)
}
if resource.Canonical == nil || resource.Canonical.PrimaryID != "availability:energy-meter" {
t.Fatalf("canonical identity = %+v, want primary availability:energy-meter", resource.Canonical)
}
}
func TestResourceRegistry_ListUsesDeterministicNameTieBreakers(t *testing.T) {
rr := NewRegistry(nil)
now := time.Date(2026, 4, 11, 0, 0, 0, 0, time.UTC)
rr.IngestResources([]Resource{
{ID: "storage-z", Type: ResourceTypeStorage, Name: "backup-vault-a", Status: StatusOnline, LastSeen: now},
{ID: "storage-b", Type: ResourceTypeStorage, Name: "backup-vault-a", Status: StatusOnline, LastSeen: now},
{ID: "agent-1", Type: ResourceTypeAgent, Name: "alpha-host", Status: StatusOnline, LastSeen: now},
{ID: "storage-a", Type: ResourceTypeStorage, Name: "backup-vault-a", Status: StatusOnline, LastSeen: now},
})
got := rr.List()
gotIDs := make([]string, 0, len(got))
for _, resource := range got {
gotIDs = append(gotIDs, resource.ID)
}
assertStringSlice(t, gotIDs, []string{"agent-1", "storage-a", "storage-b", "storage-z"})
}
func TestResourceRegistry_IngestResourcesSeedsMatcherForOverlayRecords(t *testing.T) {
rr := NewRegistry(nil)
now := time.Date(2026, 4, 15, 12, 0, 0, 0, time.UTC)
rr.IngestResources([]Resource{
{
ID: "node-1",
Type: ResourceTypeAgent,
Name: "tower",
Status: StatusOnline,
LastSeen: now,
Sources: []DataSource{SourceProxmox},
Identity: ResourceIdentity{
MachineID: "machine-1",
Hostnames: []string{"tower"},
},
Proxmox: &ProxmoxData{
NodeName: "tower",
Instance: "lab",
},
},
})
rr.IngestRecords(SourceAgent, []IngestRecord{
{
SourceID: "host-1",
Resource: Resource{
Type: ResourceTypeAgent,
Name: "tower",
Status: StatusOnline,
LastSeen: now.Add(time.Minute),
Agent: &AgentData{
AgentID: "agent-1",
},
},
Identity: ResourceIdentity{
MachineID: "machine-1",
Hostnames: []string{"tower"},
},
},
})
resources := rr.List()
if len(resources) != 1 {
t.Fatalf("expected overlay record to merge into seeded resource, got %d resources", len(resources))
}
if resources[0].Proxmox == nil || resources[0].Agent == nil {
t.Fatalf("expected merged resource to keep seeded and overlay metadata, got %+v", resources[0])
}
if got := len(resources[0].Sources); got != 2 {
t.Fatalf("expected merged sources from seed and overlay, got %d (%+v)", got, resources[0].Sources)
}
}
func TestResourceRegistry_IngestResourcesRebuildsSourceMappingsForMetricsTargets(t *testing.T) {
rr := NewRegistry(nil)
now := time.Date(2026, 4, 21, 9, 0, 0, 0, time.UTC)
rr.IngestResources([]Resource{
{
ID: "agent-dashboard-1",
Type: ResourceTypeAgent,
Name: "delly",
Status: StatusOnline,
LastSeen: now,
Sources: []DataSource{SourceProxmox, SourceAgent},
Identity: ResourceIdentity{
MachineID: "machine-delly",
Hostnames: []string{"delly"},
},
Proxmox: &ProxmoxData{
SourceID: "homelab-delly",
NodeName: "delly",
Instance: "homelab-entry",
},
Agent: &AgentData{
AgentID: "agent-source-delly",
Hostname: "delly",
},
},
})
target := BuildMetricsTargetForRegistry(rr, "agent-dashboard-1")
if target == nil {
t.Fatal("expected seeded unified agent to rebuild a metrics target")
}
// The agent source ID wins over the proxmox node ID: it is the host.ID
// key the agent metrics writer stores rows under.
if target.ResourceType != "agent" || target.ResourceID != "agent-source-delly" {
t.Fatalf("metrics target = %+v, want agent/agent-source-delly", target)
}
targets := rr.SourceTargets("agent-dashboard-1")
if len(targets) != 2 {
t.Fatalf("expected 2 source targets, got %d", len(targets))
}
foundAgent := false
foundProxmox := false
for _, sourceTarget := range targets {
switch sourceTarget.Source {
case SourceAgent:
foundAgent = sourceTarget.SourceID == "agent-source-delly"
case SourceProxmox:
foundProxmox = sourceTarget.SourceID == "homelab-delly"
}
}
if !foundAgent || !foundProxmox {
t.Fatalf("expected rebuilt agent+proxmox source targets, got %+v", targets)
}
}
func TestResourceRegistry_IngestResourcesSeedsHostScopedDockerContainerMappings(t *testing.T) {
rr := NewRegistry(nil)
now := time.Date(2026, 5, 19, 14, 0, 0, 0, time.UTC)
host105 := "proxmox-lxc-docker:pve-a:node-a:105"
host141 := "proxmox-lxc-docker:pve-a:node-a:141"
rr.IngestResources([]Resource{
{
ID: "docker-container-frigate-105",
Type: ResourceTypeAppContainer,
Name: "frigate",
Status: StatusOnline,
LastSeen: now,
Sources: []DataSource{SourceDocker},
Docker: &DockerData{
HostSourceID: host105,
ContainerID: "frigate",
DisplayName: "frigate",
Runtime: "docker",
ContainerState: "running",
},
},
{
ID: "docker-container-frigate-141",
Type: ResourceTypeAppContainer,
Name: "frigate",
Status: StatusOnline,
LastSeen: now,
Sources: []DataSource{SourceDocker},
Docker: &DockerData{
HostSourceID: host141,
ContainerID: "frigate",
DisplayName: "frigate",
Runtime: "docker",
ContainerState: "running",
},
},
})
resources := rr.ListByType(ResourceTypeAppContainer)
if len(resources) != 2 {
t.Fatalf("expected seeded resources to preserve two host-scoped containers, got %d", len(resources))
}
assertSourceTarget := func(resourceID, wantSourceID string) {
t.Helper()
for _, target := range rr.SourceTargets(resourceID) {
if target.Source == SourceDocker && target.SourceID == wantSourceID {
wantCandidateID := SourceSpecificID(ResourceTypeAppContainer, SourceDocker, wantSourceID)
if target.CandidateID != wantCandidateID {
t.Fatalf("resource %q docker candidate ID = %q, want %q", resourceID, target.CandidateID, wantCandidateID)
}
return
}
}
t.Fatalf("resource %q missing docker source target %q; got %+v", resourceID, wantSourceID, rr.SourceTargets(resourceID))
}
assertSourceTarget("docker-container-frigate-105", host105+"/container/frigate")
assertSourceTarget("docker-container-frigate-141", host141+"/container/frigate")
scopesByHost := make(map[string][]string, len(resources))
for _, resource := range resources {
if resource.Docker == nil {
t.Fatalf("docker container resource missing Docker payload: %#v", resource)
}
scopesByHost[resource.Docker.HostSourceID] = resource.PlatformScopes
}
for _, host := range []string{host105, host141} {
if got := scopesByHost[host]; !reflect.DeepEqual(got, []string{"proxmox-pve", "docker"}) {
t.Fatalf("host %q platform scopes = %#v, want proxmox-pve + docker", host, got)
}
}
}
func TestResourceRegistry_IngestResourcesDerivesClusterWorkloadParentFromSeededProxmoxNode(t *testing.T) {
rr := NewRegistry(nil)
now := time.Date(2026, 5, 14, 10, 0, 0, 0, time.UTC)
rr.IngestResources([]Resource{
{
ID: "agent-delly",
Type: ResourceTypeAgent,
Name: "delly",
Status: StatusOnline,
LastSeen: now,
Sources: []DataSource{SourceProxmox, SourceAgent},
Identity: ResourceIdentity{
MachineID: "machine-delly",
Hostnames: []string{"delly"},
},
Proxmox: &ProxmoxData{
SourceID: "homelab-delly",
NodeName: "delly",
ClusterName: "homelab",
Instance: "delly",
},
Agent: &AgentData{
AgentID: "agent-source-delly",
Hostname: "delly",
},
},
{
ID: "system-container-cloudflared",
Type: ResourceTypeSystemContainer,
Name: "cloudflared",
Status: StatusOnline,
LastSeen: now,
Sources: []DataSource{SourceProxmox},
Identity: ResourceIdentity{Hostnames: []string{"cloudflared"}},
Proxmox: &ProxmoxData{
SourceID: "delly:delly:104",
NodeName: "delly",
ClusterName: "homelab",
Instance: "delly",
VMID: 104,
},
},
})
child, ok := rr.Get("system-container-cloudflared")
if !ok {
t.Fatal("expected cloudflared resource")
}
if child.ParentID == nil || *child.ParentID != "agent-delly" {
t.Fatalf("expected cloudflared parent agent-delly, got %+v", child.ParentID)
}
if child.ParentName != "delly" {
t.Fatalf("expected cloudflared parent name delly, got %q", child.ParentName)
}
parent, ok := rr.Get("agent-delly")
if !ok {
t.Fatal("expected delly parent resource")
}
if parent.ChildCount != 1 {
t.Fatalf("expected delly child count 1, got %d", parent.ChildCount)
}
}
func TestStorageRiskSemanticsPrefersUnraidParitySummaryOverGenericDiskCounts(t *testing.T) {
risk := &StorageRisk{
Reasons: []StorageRiskReason{
{Code: "unraid_disabled_disks", Severity: storagehealth.RiskCritical, Summary: "Unraid array reports 1 disabled disk(s)"},
{Code: "unraid_parity_unavailable", Severity: storagehealth.RiskCritical, Summary: "Unraid parity protection is unavailable"},
},
}
_, protectionReduced, _, protectionSummary, _ := StorageRiskSemantics(risk)
if !protectionReduced {
t.Fatal("expected protectionReduced=true")
}
if protectionSummary != "Unraid parity protection is unavailable" {
t.Fatalf("protectionSummary = %q, want %q", protectionSummary, "Unraid parity protection is unavailable")
}
}
func TestResourceRegistry_MergesPhysicalDiskTemperatureAggregate(t *testing.T) {
rr := NewRegistry(nil)
now := time.Date(2026, 3, 29, 12, 0, 0, 0, time.UTC)
rr.IngestRecords(SourceTrueNAS, []IngestRecord{
{
SourceID: "disk-sda",
Resource: Resource{
Type: ResourceTypePhysicalDisk,
Name: "sda",
Status: StatusOnline,
LastSeen: now,
PhysicalDisk: &PhysicalDiskMeta{
DevPath: "/dev/sda",
Model: "Seagate Exos X18",
Serial: "SER-A",
DiskType: "sata",
SizeBytes: 1_000_000,
Health: "PASSED",
Temperature: 34,
Wearout: -1,
TemperatureAggregate: &TemperatureAggregateMeta{
WindowDays: 7,
MinCelsius: 29.0,
AvgCelsius: 32.7,
MaxCelsius: 38.0,
},
},
},
Identity: ResourceIdentity{MachineID: "SER-A"},
},
})
disks := rr.ListByType(ResourceTypePhysicalDisk)
if len(disks) != 1 {
t.Fatalf("expected 1 physical disk, got %d", len(disks))
}
aggregate := disks[0].PhysicalDisk.TemperatureAggregate
if aggregate == nil {
t.Fatalf("expected temperature aggregate on merged disk record: %+v", disks[0].PhysicalDisk)
}
if aggregate.WindowDays != 7 || aggregate.MinCelsius != 29.0 || aggregate.AvgCelsius != 32.7 || aggregate.MaxCelsius != 38.0 {
t.Fatalf("unexpected merged temperature aggregate: %+v", aggregate)
}
aggregate.MaxCelsius = 99.0
got, ok := rr.Get(disks[0].ID)
if !ok || got == nil || got.PhysicalDisk == nil || got.PhysicalDisk.TemperatureAggregate == nil {
t.Fatalf("expected stored physical disk record, got %+v", got)
}
if got.PhysicalDisk.TemperatureAggregate.MaxCelsius != 38.0 {
t.Fatalf("expected registry clone isolation for temperature aggregate, got %+v", got.PhysicalDisk.TemperatureAggregate)
}
}
func TestResourceRegistry_IngestsVMwareSourceAsCanonicalResources(t *testing.T) {
rr := NewRegistry(nil)
now := time.Date(2026, 3, 30, 18, 30, 0, 0, time.UTC)
rr.IngestRecords(SourceVMware, []IngestRecord{
{
SourceID: "vc-1:host:host-101",
Resource: Resource{
Type: ResourceTypeAgent,
Name: "esxi-01.lab.local",
Status: StatusOnline,
LastSeen: now,
VMware: &VMwareData{
ConnectionID: "vc-1",
ConnectionName: "Lab VC",
VCenterHost: "vc.lab.local",
ManagedObjectID: "host-101",
EntityType: "host",
HostUUID: "uuid-host-1",
},
},
Identity: ResourceIdentity{
DMIUUID: "uuid-host-1",
Hostnames: []string{"esxi-01.lab.local"},
},
},
})
agents := rr.ListByType(ResourceTypeAgent)
if len(agents) != 1 {
t.Fatalf("expected 1 VMware agent resource, got %d", len(agents))
}
agent := agents[0]
if len(agent.Sources) != 1 || agent.Sources[0] != SourceVMware {
t.Fatalf("expected VMware source ownership, got %+v", agent.Sources)
}
if agent.VMware == nil {
t.Fatalf("expected VMware metadata on merged resource, got %+v", agent)
}
if got := agent.VMware.ConnectionID; got != "vc-1" {
t.Fatalf("vmware connection id = %q, want vc-1", got)
}
if got := agent.Canonical.PrimaryID; got != "vmware:vc-1:host:host-101" {
t.Fatalf("canonical primary id = %q, want vmware:vc-1:host:host-101", got)
}
}
func TestResourceRegistry_PreservesVMwareSourceOwnedDatastoreConsumers(t *testing.T) {
rr := NewRegistry(nil)
now := time.Date(2026, 3, 30, 18, 45, 0, 0, time.UTC)
rr.IngestRecords(SourceVMware, []IngestRecord{
{
SourceID: "vc-1:datastore:datastore-201",
Resource: Resource{
Type: ResourceTypeStorage,
Name: "nvme-primary",
Status: StatusOnline,
LastSeen: now,
Storage: &StorageMeta{
Type: "vmfs",
Platform: "vmware-vsphere",
Topology: "datastore",
ConsumerCount: 2,
ConsumerTypes: []string{string(ResourceTypeVM)},
TopConsumers: []StorageConsumerMeta{
{ResourceType: ResourceTypeVM, Name: "warehouse-api-01"},
{ResourceType: ResourceTypeVM, Name: "etl-batch-01"},
},
},
VMware: &VMwareData{
ConnectionID: "vc-1",
ManagedObjectID: "datastore-201",
EntityType: "datastore",
},
},
},
})
datastores := rr.ListByType(ResourceTypeStorage)
if len(datastores) != 1 {
t.Fatalf("expected 1 VMware datastore, got %d", len(datastores))
}
datastore := datastores[0]
if datastore.Storage == nil {
t.Fatalf("expected VMware datastore storage metadata, got %+v", datastore)
}
if got := datastore.Storage.ConsumerCount; got != 2 {
t.Fatalf("consumer count = %d, want 2", got)
}
if got := datastore.Storage.ConsumerTypes; len(got) != 1 || got[0] != string(ResourceTypeVM) {
t.Fatalf("consumer types = %#v, want [vm]", got)
}
if len(datastore.Storage.TopConsumers) != 2 {
t.Fatalf("top consumers = %#v, want 2 VMware VM consumers", datastore.Storage.TopConsumers)
}
if datastore.Storage.TopConsumers[0].Name != "warehouse-api-01" {
t.Fatalf("first top consumer = %#v, want warehouse-api-01", datastore.Storage.TopConsumers[0])
}
}
func TestBuildMetricsTargetForRegistryFallsBackToStoredMetricsTarget(t *testing.T) {
rr := NewRegistry(nil)
now := time.Date(2026, 3, 31, 10, 0, 0, 0, time.UTC)
resourceID := CanonicalResourceID("app-demo")
rr.resources[resourceID] = &Resource{
ID: resourceID,
Type: ResourceTypeAppContainer,
Name: "Nextcloud",
Status: StatusOnline,
LastSeen: now,
MetricsTarget: &MetricsTarget{
ResourceType: "dockerContainer",
ResourceID: "nextcloud-web-1",
},
}
target := BuildMetricsTargetForRegistry(rr, resourceID)
if target == nil {
t.Fatal("expected stored metrics target fallback")
}
if target.ResourceType != "dockerContainer" || target.ResourceID != "nextcloud-web-1" {
t.Fatalf("unexpected fallback metrics target %+v", target)
}
target.ResourceID = "mutated"
stored, ok := rr.Get(resourceID)
if !ok || stored == nil || stored.MetricsTarget == nil {
t.Fatalf("expected stored app resource with metrics target, got %+v", stored)
}
if stored.MetricsTarget.ResourceID != "nextcloud-web-1" {
t.Fatalf("expected fallback target clone isolation, got %+v", stored.MetricsTarget)
}
}
func TestResourceRegistryStoragePoolViewsCarryResolvedMetricsTarget(t *testing.T) {
rr := NewRegistry(nil)
now := time.Date(2026, 4, 1, 11, 0, 0, 0, time.UTC)
rr.IngestRecords(SourceVMware, []IngestRecord{
{
SourceID: "vc-1:datastore:datastore-202",
Resource: Resource{
ID: "storage-vmware-1",
Type: ResourceTypeStorage,
Name: "archive-tier",
Status: StatusOnline,
LastSeen: now,
Storage: &StorageMeta{
Type: "datastore",
Platform: "vmware",
Nodes: []string{"esxi-01.lab.local"},
},
VMware: &VMwareData{
ConnectionID: "vc-1",
EntityType: "datastore",
ManagedObjectID: "datastore-202",
RuntimeHostName: "esxi-01.lab.local",
},
},
},
})
pools := rr.StoragePools()
if len(pools) != 1 {
t.Fatalf("expected 1 storage pool view, got %d", len(pools))
}
if got := pools[0].SourceID(); got != "vc-1:datastore:datastore-202" {
t.Fatalf("expected storage pool view to expose resolved metrics target, got %q", got)
}
if got := pools[0].Node(); got != "esxi-01.lab.local" {
t.Fatalf("expected storage pool view node hint to remain VMware runtime host, got %q", got)
}
stored, ok := rr.Get(pools[0].ID())
if !ok || stored == nil {
t.Fatalf("expected stored storage resource, got %+v", stored)
}
if stored.MetricsTarget != nil {
t.Fatalf("expected registry storage record to remain raw without persisted metrics target, got %+v", stored.MetricsTarget)
}
}
func TestMergeVMwareDataMergesSignalFieldsWithoutDroppingExistingIdentity(t *testing.T) {
existing := &VMwareData{
ConnectionID: "vc-1",
ConnectionName: "Lab VC",
VCenterHost: "vc.lab.local",
ManagedObjectID: "vm-101",
EntityType: "vm",
HostUUID: "uuid-host-1",
DatacenterName: "DC1",
ClusterName: "Cluster A",
DatastoreNames: []string{"primary-vmfs"},
NetworkHostNames: []string{"esxi-01.lab.local"},
ConnectionState: "connected",
PowerState: "poweredOn",
OverallStatus: "green",
GuestHostname: "app-01.internal",
ActiveAlarmCount: 1,
ActiveAlarmSummary: "old alarm",
RecentTaskCount: 1,
RecentTaskSummary: "old task",
SnapshotCount: 1,
}
accessible := false
clusterHA := true
clusterDRS := false
bootRetry := true
incoming := &VMwareData{
ClusterName: "Cluster A",
ClusterHAEnabled: &clusterHA,
ClusterDRSEnabled: &clusterDRS,
RuntimeHostName: "esxi-01.lab.local",
DatastoreNames: []string{"backup-nfs"},
NetworkType: "STANDARD_PORTGROUP",
NetworkHostNames: []string{"esxi-02.lab.local"},
NetworkVMNames: []string{"app-01"},
DatastoreAccessible: &accessible,
GuestIPAddresses: []string{"10.0.0.21"},
OverallStatus: "yellow",
ActiveAlarmCount: 3,
ActiveAlarmSummary: "Host connection lost; datastore latency elevated",
RecentTaskCount: 2,
RecentTaskSummary: "vMotion task completed",
SnapshotCount: 4,
CurrentSnapshotID: "snapshot-202",
SnapshotTree: []VMwareSnapshotData{{Snapshot: "snapshot-202", Name: "pre-maintenance"}},
NetworkAdapters: []VMwareNetworkAdapterData{{NIC: "4000", NetworkName: "VM Network"}},
VirtualDisks: []VMwareVirtualDiskData{{Disk: "2000", Label: "Hard disk 1"}},
Tools: &VMwareToolsData{RunState: "RUNNING", VersionStatus: "CURRENT"},
Hardware: &VMwareVMHardwareData{
Version: "VMX_20",
BootRetry: &bootRetry,
BootDevices: []VMwareBootDeviceData{{
Type: "DISK",
Disks: []string{"2000"},
}},
},
}
merged := mergeVMwareData(existing, incoming)
if merged == nil {
t.Fatal("expected merged VMware metadata")
}
if got := merged.ConnectionID; got != "vc-1" {
t.Fatalf("connection id = %q, want vc-1", got)
}
if got := merged.ManagedObjectID; got != "vm-101" {
t.Fatalf("managed object id = %q, want vm-101", got)
}
if got := merged.OverallStatus; got != "yellow" {
t.Fatalf("overall status = %q, want yellow", got)
}
if got := merged.ActiveAlarmCount; got != 3 {
t.Fatalf("active alarm count = %d, want 3", got)
}
if got := merged.ActiveAlarmSummary; got != "Host connection lost; datastore latency elevated" {
t.Fatalf("active alarm summary = %q", got)
}
if got := merged.RecentTaskCount; got != 2 {
t.Fatalf("recent task count = %d, want 2", got)
}
if got := merged.RecentTaskSummary; got != "vMotion task completed" {
t.Fatalf("recent task summary = %q", got)
}
if got := merged.SnapshotCount; got != 4 {
t.Fatalf("snapshot count = %d, want 4", got)
}
if got := merged.RuntimeHostName; got != "esxi-01.lab.local" {
t.Fatalf("runtime host name = %q, want esxi-01.lab.local", got)
}
if merged.ClusterHAEnabled == nil || !*merged.ClusterHAEnabled {
t.Fatalf("cluster HA enabled = %#v, want true", merged.ClusterHAEnabled)
}
if merged.ClusterDRSEnabled == nil || *merged.ClusterDRSEnabled {
t.Fatalf("cluster DRS enabled = %#v, want false", merged.ClusterDRSEnabled)
}
if got := merged.DatastoreNames; !reflect.DeepEqual(got, []string{"primary-vmfs", "backup-nfs"}) {
t.Fatalf("datastore names = %#v", got)
}
if got := merged.NetworkType; got != "STANDARD_PORTGROUP" {
t.Fatalf("network type = %q, want STANDARD_PORTGROUP", got)
}
if got := merged.NetworkHostNames; !reflect.DeepEqual(got, []string{"esxi-01.lab.local", "esxi-02.lab.local"}) {
t.Fatalf("network host names = %#v", got)
}
if got := merged.NetworkVMNames; !reflect.DeepEqual(got, []string{"app-01"}) {
t.Fatalf("network VM names = %#v", got)
}
if merged.DatastoreAccessible == nil || *merged.DatastoreAccessible {
t.Fatalf("datastore accessible = %#v, want false", merged.DatastoreAccessible)
}
if got := merged.GuestHostname; got != "app-01.internal" {
t.Fatalf("guest hostname = %q, want app-01.internal", got)
}
if got := merged.GuestIPAddresses; !reflect.DeepEqual(got, []string{"10.0.0.21"}) {
t.Fatalf("guest ip addresses = %#v", got)
}
if got := merged.CurrentSnapshotID; got != "snapshot-202" {
t.Fatalf("current snapshot id = %q, want snapshot-202", got)
}
if got := merged.SnapshotTree; len(got) != 1 || got[0].Name != "pre-maintenance" {
t.Fatalf("snapshot tree = %#v", got)
}
if got := merged.NetworkAdapters; len(got) != 1 || got[0].NetworkName != "VM Network" {
t.Fatalf("network adapters = %#v", got)
}
if got := merged.VirtualDisks; len(got) != 1 || got[0].Label != "Hard disk 1" {
t.Fatalf("virtual disks = %#v", got)
}
if got := merged.Tools; got == nil || got.RunState != "RUNNING" {
t.Fatalf("tools = %#v", got)
}
if got := merged.Hardware; got == nil || got.Version != "VMX_20" || got.BootDevices[0].Disks[0] != "2000" {
t.Fatalf("hardware = %#v", got)
}
}
func TestResourceRegistryClonesCarryPolicyMetadata(t *testing.T) {
rr := NewRegistry(nil)
now := time.Date(2026, 3, 17, 12, 0, 0, 0, time.UTC)
rr.IngestRecords(SourceProxmox, []IngestRecord{
{
SourceID: "vm-900",
Resource: Resource{
ID: "vm-900",
Type: ResourceTypeVM,
Name: "customer-vm",
Status: StatusOnline,
LastSeen: now,
Tags: []string{"customer-data"},
},
Identity: ResourceIdentity{
Hostnames: []string{"customer.internal"},
IPAddresses: []string{"10.0.0.90"},
},
},
})
listed := rr.ListByType(ResourceTypeVM)
if len(listed) != 1 {
t.Fatalf("expected 1 VM, got %d", len(listed))
}
if listed[0].Policy == nil {
t.Fatal("expected list result to include policy metadata")
}
if got := listed[0].Policy.Sensitivity; got != ResourceSensitivityRestricted {
t.Fatalf("policy sensitivity = %q, want %q", got, ResourceSensitivityRestricted)
}
if listed[0].AISafeSummary == "" {
t.Fatal("expected aiSafeSummary on list result")
}
got, ok := rr.Get(listed[0].ID)
if !ok || got == nil {
t.Fatalf("expected Get(%q) to succeed", listed[0].ID)
}
if got.Policy == nil {
t.Fatal("expected get result to include policy metadata")
}
if got.Policy == listed[0].Policy {
t.Fatal("expected policy metadata to be cloned, not shared by pointer")
}
}
func TestResourceRegistry_MonitoredSystemsSummarizeCanonicalTopLevelViews(t *testing.T) {
rr := NewRegistry(nil)
now := time.Date(2026, 3, 17, 12, 0, 0, 0, time.UTC)
rr.IngestRecords(SourceAgent, []IngestRecord{
{
SourceID: "host-1",
Resource: Resource{
ID: "host-1",
Type: ResourceTypeAgent,
Name: "lab-a",
Status: StatusOnline,
LastSeen: now.Add(-2 * time.Minute),
Agent: &AgentData{
AgentID: "agent-1",
Hostname: "lab-a",
MachineID: "machine-1",
},
},
Identity: ResourceIdentity{
MachineID: "machine-1",
Hostnames: []string{"lab-a"},
},
},
})
rr.IngestRecords(SourcePBS, []IngestRecord{
{
SourceID: "pbs-1",
Resource: Resource{
ID: "pbs-1",
Type: ResourceTypePBS,
Name: "pbs-a",
Status: StatusOnline,
LastSeen: now,
PBS: &PBSData{
InstanceID: "pbs-1",
Hostname: "lab-a",
HostURL: "https://lab-a:8007",
},
},
},
})
rr.IngestRecords(SourcePMG, []IngestRecord{
{
SourceID: "pmg-1",
Resource: Resource{
ID: "pmg-1",
Type: ResourceTypePMG,
Name: "pmg-b",
Status: StatusOffline,
LastSeen: now.Add(-10 * time.Minute),
PMG: &PMGData{
InstanceID: "pmg-1",
Hostname: "mail-b",
},
},
},
})
rr.IngestRecords(SourceK8s, []IngestRecord{
{
SourceID: "k8s-1",
Resource: Resource{
ID: "k8s-1",
Type: ResourceTypeK8sCluster,
Name: "cluster-a",
Status: StatusOnline,
LastSeen: now.Add(-30 * time.Second),
Kubernetes: &K8sData{
ClusterID: "cluster-a",
AgentID: "k8s-agent-1",
Server: "https://cluster-a.example:6443",
},
},
},
})
systems := MonitoredSystems(rr)
if len(systems) != 3 {
t.Fatalf("MonitoredSystems() returned %d systems, want 3", len(systems))
}
byName := make(map[string]MonitoredSystemRecord, len(systems))
for _, system := range systems {
byName[system.Name] = system
}
hostSummary, ok := byName["lab-a"]
if !ok {
t.Fatalf("expected monitored systems to include lab-a, got %+v", systems)
}
if hostSummary.Type != "host" || hostSummary.Source != "multiple" {
t.Fatalf("expected deduped host summary for lab-a, got %+v", hostSummary)
}
if !hostSummary.LastSeen.Equal(now) {
t.Fatalf("expected deduped host last_seen %s, got %s", now, hostSummary.LastSeen)
}
k8sSummary, ok := byName["cluster-a"]
if !ok {
t.Fatalf("expected monitored systems to include cluster-a, got %+v", systems)
}
if k8sSummary.Type != "kubernetes-cluster" || k8sSummary.Source != "kubernetes" {
t.Fatalf("expected kubernetes summary for cluster-a, got %+v", k8sSummary)
}
pmgSummary, ok := byName["pmg-b"]
if !ok {
t.Fatalf("expected monitored systems to include pmg-b, got %+v", systems)
}
if pmgSummary.Type != "pmg-server" || pmgSummary.Status != StatusOffline {
t.Fatalf("expected PMG summary for pmg-b, got %+v", pmgSummary)
}
}
func TestMonitoredSystemsExplainsStaleGroupedSourceWhileLastSeenStaysFresh(t *testing.T) {
rr := NewRegistry(nil)
now := time.Date(2026, 3, 23, 12, 0, 0, 0, time.UTC)
agentResource := topLevelTestAgent("agent-host", "tower.local", "machine-1", "agent-1")
agentResource.LastSeen = now.Add(-5 * time.Minute)
dockerResource := topLevelTestDockerHost("docker-host", "tower.local", "docker-runtime-1", "agent-1")
dockerResource.LastSeen = now.Add(-10 * time.Second)
rr.IngestRecords(SourceAgent, []IngestRecord{
{
SourceID: "agent-host",
Resource: agentResource,
},
})
rr.IngestRecords(SourceDocker, []IngestRecord{
{
SourceID: "docker-host",
Resource: dockerResource,
},
})
rr.MarkStale(now, map[DataSource]time.Duration{
SourceAgent: 60 * time.Second,
SourceDocker: 60 * time.Second,
})
systems := MonitoredSystems(rr)
if len(systems) != 1 {
t.Fatalf("MonitoredSystems() returned %d systems, want 1", len(systems))
}
system := systems[0]
if system.Status != StatusWarning {
t.Fatalf("expected grouped monitored system status warning, got %+v", system)
}
if !system.LastSeen.Equal(dockerResource.LastSeen) {
t.Fatalf("expected grouped last_seen %s, got %s", dockerResource.LastSeen, system.LastSeen)
}
if system.LatestIncludedSignal.Source != string(SourceDocker) {
t.Fatalf("expected latest included signal source docker, got %+v", system)
}
if system.LatestIncludedSignal.Name != "tower.local" {
t.Fatalf("expected latest included signal name tower.local, got %+v", system)
}
if system.LatestIncludedSignal.Type != "docker-host" {
t.Fatalf("expected latest included signal type docker-host, got %+v", system)
}
if !system.LatestIncludedSignal.At.Equal(dockerResource.LastSeen) {
t.Fatalf("expected latest included signal at %s, got %+v", dockerResource.LastSeen, system)
}
if system.StatusExplanation.Summary == "" {
t.Fatal("expected grouped monitored system status explanation summary")
}
if system.StatusExplanation.Summary != "Pulse most recently heard from Docker for tower.local at 2026-03-23T11:59:50Z, but Agent data for tower.local is stale (last reported 2026-03-23T11:55:00Z), so this monitored system is warning." {
t.Fatalf("unexpected grouped monitored system summary: %q", system.StatusExplanation.Summary)
}
if len(system.StatusExplanation.Reasons) != 1 {
t.Fatalf("expected one stale grouped-source reason, got %+v", system.StatusExplanation.Reasons)
}
reason := system.StatusExplanation.Reasons[0]
if reason.Kind != "source-stale" {
t.Fatalf("expected stale source reason kind, got %+v", reason)
}
if reason.Source != string(SourceAgent) {
t.Fatalf("expected agent source reason, got %+v", reason)
}
if reason.Status != "stale" {
t.Fatalf("expected stale reason status, got %+v", reason)
}
if !reason.ReportedAt.Equal(agentResource.LastSeen) {
t.Fatalf("expected stale reason reported_at %s, got %s", agentResource.LastSeen, reason.ReportedAt)
}
if reason.Summary == "" {
t.Fatalf("expected stale reason summary, got %+v", reason)
}
}
func TestMonitoredSystemsMergeShortAndFQDNHostnamesForOneHost(t *testing.T) {
rr := NewRegistry(nil)
now := time.Date(2026, 4, 21, 21, 30, 0, 0, time.UTC)
rr.IngestRecords(SourceAgent, []IngestRecord{
{
SourceID: "agent-qnap",
Resource: Resource{
ID: "agent-qnap",
Type: ResourceTypeAgent,
Name: "qnap.local",
Status: StatusOnline,
LastSeen: now.Add(-2 * time.Minute),
Agent: &AgentData{
AgentID: "agent-qnap",
Hostname: "qnap.local",
},
},
Identity: ResourceIdentity{
Hostnames: []string{"qnap.local"},
},
},
})
rr.IngestRecords(SourceDocker, []IngestRecord{
{
SourceID: "docker-qnap",
Resource: Resource{
ID: "docker-qnap",
Type: ResourceTypeAgent,
Name: "qnap",
Status: StatusOnline,
LastSeen: now,
Docker: &DockerData{
HostSourceID: "docker-qnap",
Hostname: "qnap",
},
},
Identity: ResourceIdentity{
Hostnames: []string{"qnap"},
},
},
})
systems := MonitoredSystems(rr)
if len(systems) != 1 {
t.Fatalf("MonitoredSystems() returned %d systems, want 1", len(systems))
}
if systems[0].Source != "multiple" {
t.Fatalf("MonitoredSystems()[0].Source = %q, want %q", systems[0].Source, "multiple")
}
if systems[0].Explanation.Summary == "" {
t.Fatal("expected grouped monitored-system explanation summary")
}
if !systems[0].LastSeen.Equal(now) {
t.Fatalf("MonitoredSystems()[0].LastSeen = %s, want %s", systems[0].LastSeen, now)
}
}
func TestMonitoredSystemsReportOnlineStatusWithoutUnknownBaselineBias(t *testing.T) {
rr := NewRegistry(nil)
proxmoxNode := topLevelTestProxmoxNode("proxmox-node", "tower", "proxmox-1", "https://tower.local:8006")
proxmoxNode.LastSeen = time.Date(2026, 3, 24, 10, 0, 0, 0, time.UTC)
rr.IngestRecords(SourceProxmox, []IngestRecord{
{
SourceID: "proxmox-node",
Resource: proxmoxNode,
},
})
systems := MonitoredSystems(rr)
if len(systems) != 1 {
t.Fatalf("MonitoredSystems() returned %d systems, want 1", len(systems))
}
if systems[0].Status != StatusOnline {
t.Fatalf("MonitoredSystems()[0].Status = %q, want %q", systems[0].Status, StatusOnline)
}
if systems[0].StatusExplanation.Summary != "All included top-level collection paths currently report online status." {
t.Fatalf("unexpected online status summary: %q", systems[0].StatusExplanation.Summary)
}
}
func TestMonitoredSystemsGroupedOnlineSourcesStayOnline(t *testing.T) {
rr := NewRegistry(nil)
now := time.Date(2026, 3, 24, 10, 0, 0, 0, time.UTC)
agentResource := topLevelTestAgent("agent-host", "tower.local", "machine-1", "agent-1")
agentResource.LastSeen = now.Add(-1 * time.Minute)
dockerResource := topLevelTestDockerHost("docker-host", "tower.local", "docker-runtime-1", "agent-1")
dockerResource.LastSeen = now
rr.IngestRecords(SourceAgent, []IngestRecord{
{
SourceID: "agent-host",
Resource: agentResource,
},
})
rr.IngestRecords(SourceDocker, []IngestRecord{
{
SourceID: "docker-host",
Resource: dockerResource,
},
})
systems := MonitoredSystems(rr)
if len(systems) != 1 {
t.Fatalf("MonitoredSystems() returned %d systems, want 1", len(systems))
}
if systems[0].Status != StatusOnline {
t.Fatalf("MonitoredSystems()[0].Status = %q, want %q", systems[0].Status, StatusOnline)
}
}
func TestMonitoredSystemsPreferOfflineOverWarning(t *testing.T) {
rr := NewRegistry(nil)
now := time.Date(2026, 3, 24, 10, 0, 0, 0, time.UTC)
agentResource := topLevelTestAgent("agent-host", "tower.local", "machine-1", "agent-1")
agentResource.Status = StatusWarning
agentResource.LastSeen = now.Add(-1 * time.Minute)
dockerResource := topLevelTestDockerHost("docker-host", "tower.local", "docker-runtime-1", "agent-1")
dockerResource.Status = StatusOffline
dockerResource.LastSeen = now
rr.IngestRecords(SourceAgent, []IngestRecord{
{
SourceID: "agent-host",
Resource: agentResource,
},
})
rr.IngestRecords(SourceDocker, []IngestRecord{
{
SourceID: "docker-host",
Resource: dockerResource,
},
})
systems := MonitoredSystems(rr)
if len(systems) != 1 {
t.Fatalf("MonitoredSystems() returned %d systems, want 1", len(systems))
}
if systems[0].Status != StatusOffline {
t.Fatalf("MonitoredSystems()[0].Status = %q, want %q", systems[0].Status, StatusOffline)
}
}
func TestResourceRegistry_IngestRecords_UnknownSource(t *testing.T) {
rr := NewRegistry(nil)
now := time.Date(2026, 2, 20, 12, 0, 0, 0, time.UTC)
customSource := DataSource("xcp")
rr.IngestRecords(customSource, []IngestRecord{
{
SourceID: "host-1",
Resource: Resource{
Type: ResourceTypeAgent,
Name: "xcp-host-1",
Status: StatusOnline,
LastSeen: now,
},
Identity: ResourceIdentity{Hostnames: []string{"xcp-host-1"}},
},
})
hosts := rr.ListByType(ResourceTypeAgent)
if len(hosts) != 1 {
t.Fatalf("expected 1 host for custom source, got %d", len(hosts))
}
if hosts[0].Name != "xcp-host-1" {
t.Fatalf("expected host name xcp-host-1, got %q", hosts[0].Name)
}
targets := rr.SourceTargets(hosts[0].ID)
if len(targets) != 1 {
t.Fatalf("expected 1 source target, got %d", len(targets))
}
if targets[0].Source != customSource {
t.Fatalf("expected custom source %q, got %q", customSource, targets[0].Source)
}
if targets[0].SourceID != "host-1" {
t.Fatalf("expected source ID host-1, got %q", targets[0].SourceID)
}
}
func TestResourceRegistry_IngestRecords_CanonicalizesSourceIDWhitespace(t *testing.T) {
rr := NewRegistry(nil)
now := time.Date(2026, 2, 20, 12, 0, 0, 0, time.UTC)
rr.IngestRecords(SourceProxmox, []IngestRecord{
{
SourceID: " vm-100 ",
Resource: Resource{
Type: ResourceTypeVM,
Name: "vm-100",
Status: StatusOnline,
LastSeen: now,
},
Identity: ResourceIdentity{Hostnames: []string{"vm-100"}},
},
})
got := rr.sourceResourceID(SourceProxmox, "vm-100")
if got == "" {
t.Fatal("expected trimmed source ID to resolve canonical resource ID")
}
gotTrimmedVariant := rr.sourceResourceID(SourceProxmox, " vm-100 ")
if gotTrimmedVariant != got {
t.Fatalf("expected whitespace variants to resolve same resource ID, got %q want %q", gotTrimmedVariant, got)
}
targets := rr.SourceTargets(got)
if len(targets) != 1 {
t.Fatalf("expected 1 source target, got %d", len(targets))
}
if targets[0].SourceID != "vm-100" {
t.Fatalf("expected canonical trimmed source ID, got %q", targets[0].SourceID)
}
if targets[0].CandidateID != rr.sourceSpecificID(ResourceTypeVM, SourceProxmox, "vm-100") {
t.Fatalf("expected candidate ID to use canonical trimmed source ID, got %q", targets[0].CandidateID)
}
}
func TestResourceRegistry_IngestSnapshotUnifiesLinkedProxmoxNodeViewsByHostIdentity(t *testing.T) {
rr := NewRegistry(nil)
now := time.Date(2026, 3, 7, 12, 0, 0, 0, time.UTC)
rr.IngestSnapshot(models.StateSnapshot{
Nodes: []models.Node{
{
ID: "homelab-minipc",
Name: "minipc",
Instance: "homelab-entry",
ClusterName: "homelab",
IsClusterMember: true,
Host: "https://10.0.0.5:8006",
LinkedAgentID: "host-1",
Status: "online",
LastSeen: now,
},
{
ID: "standalone-minipc",
Name: "minipc",
Instance: "minipc-standalone",
Host: "https://10.0.0.5:8006",
LinkedAgentID: "host-1",
Status: "online",
LastSeen: now,
},
},
Hosts: []models.Host{
{
ID: "host-1",
Hostname: "minipc.local",
MachineID: "machine-1",
ReportIP: "10.0.0.5",
Status: "online",
LastSeen: now,
NetworkInterfaces: []models.HostNetworkInterface{
{Name: "eth0", MAC: "00:11:22:33:44:55", Addresses: []string{"10.0.0.5/24"}},
},
},
},
})
agents := rr.ListByType(ResourceTypeAgent)
if len(agents) != 1 {
t.Fatalf("expected 1 unified agent resource, got %d", len(agents))
}
resource := agents[0]
if resource.Identity.MachineID != "machine-1" {
t.Fatalf("MachineID = %q, want machine-1", resource.Identity.MachineID)
}
if !containsDataSource(resource.Sources, SourceAgent) || !containsDataSource(resource.Sources, SourceProxmox) {
t.Fatalf("expected merged agent+proxmox sources, got %+v", resource.Sources)
}
if resource.Proxmox == nil || resource.Proxmox.NodeName != "minipc" {
t.Fatalf("expected proxmox node metadata for minipc, got %+v", resource.Proxmox)
}
}
func TestResourceRegistry_IngestSnapshotUnifiesAsymmetricLinkedProxmoxNodeViewsByHostIdentity(t *testing.T) {
rr := NewRegistry(nil)
now := time.Date(2026, 3, 7, 12, 0, 0, 0, time.UTC)
rr.IngestSnapshot(models.StateSnapshot{
Nodes: []models.Node{
{
ID: "homelab-minipc",
Name: "minipc",
Instance: "homelab-entry",
ClusterName: "homelab",
IsClusterMember: true,
Host: "https://10.0.0.5:8006",
LinkedAgentID: "host-1",
Status: "online",
LastSeen: now,
},
{
ID: "homelab-minipc-shadow",
Name: "minipc",
Instance: "homelab-shadow",
ClusterName: "homelab",
IsClusterMember: true,
Host: "https://10.0.0.5:8006",
Status: "online",
LastSeen: now.Add(-time.Minute),
},
},
Hosts: []models.Host{
{
ID: "host-1",
Hostname: "minipc.local",
MachineID: "machine-1",
ReportIP: "10.0.0.5",
Status: "online",
LastSeen: now,
LinkedNodeID: "homelab-minipc",
NetworkInterfaces: []models.HostNetworkInterface{
{Name: "eth0", MAC: "00:11:22:33:44:55", Addresses: []string{"10.0.0.5/24"}},
},
},
},
})
agents := rr.ListByType(ResourceTypeAgent)
if len(agents) != 1 {
t.Fatalf("expected 1 unified agent resource, got %d", len(agents))
}
resource := agents[0]
if resource.Identity.MachineID != "machine-1" {
t.Fatalf("MachineID = %q, want machine-1", resource.Identity.MachineID)
}
if resource.Proxmox == nil {
t.Fatalf("expected proxmox metadata")
}
if got := resource.Proxmox.LinkedAgentID; got != "host-1" {
t.Fatalf("LinkedAgentID = %q, want host-1", got)
}
targets := rr.SourceTargets(resource.ID)
if len(targets) != 3 {
t.Fatalf("expected 3 source targets (2 proxmox + 1 agent), got %d", len(targets))
}
}
func TestResourceRegistry_IngestSnapshotUnifiesHostLinkedProxmoxNodeViewsByHostIdentity(t *testing.T) {
rr := NewRegistry(nil)
now := time.Date(2026, 3, 7, 12, 0, 0, 0, time.UTC)
rr.IngestSnapshot(models.StateSnapshot{
Nodes: []models.Node{
{
ID: "homelab-delly",
Name: "delly",
Instance: "homelab-entry",
ClusterName: "homelab",
IsClusterMember: true,
Host: "https://10.0.0.9:8006",
Status: "online",
LastSeen: now,
},
{
ID: "homelab-delly-shadow",
Name: "delly",
Instance: "homelab-shadow",
ClusterName: "homelab",
IsClusterMember: true,
Host: "https://10.0.0.9:8006",
Status: "online",
LastSeen: now.Add(-time.Minute),
},
},
Hosts: []models.Host{
{
ID: "host-1",
Hostname: "delly.local",
MachineID: "machine-delly",
ReportIP: "10.0.0.9",
Status: "online",
LastSeen: now,
LinkedNodeID: "homelab-delly",
NetworkInterfaces: []models.HostNetworkInterface{
{Name: "eth0", MAC: "00:11:22:33:44:66", Addresses: []string{"10.0.0.9/24"}},
},
},
},
})
agents := rr.ListByType(ResourceTypeAgent)
if len(agents) != 1 {
t.Fatalf("expected 1 unified agent resource, got %d", len(agents))
}
resource := agents[0]
if resource.Identity.MachineID != "machine-delly" {
t.Fatalf("MachineID = %q, want machine-delly", resource.Identity.MachineID)
}
if resource.Proxmox == nil {
t.Fatalf("expected proxmox metadata")
}
if got := resource.Proxmox.LinkedAgentID; got != "host-1" {
t.Fatalf("LinkedAgentID = %q, want host-1", got)
}
targets := rr.SourceTargets(resource.ID)
if len(targets) != 3 {
t.Fatalf("expected 3 source targets (2 proxmox + 1 agent), got %d", len(targets))
}
}
func TestResourceRegistry_IngestSnapshotParentsClusterNamedProxmoxGuestsToMergedNode(t *testing.T) {
rr := NewRegistry(nil)
now := time.Date(2026, 5, 14, 10, 0, 0, 0, time.UTC)
rr.IngestSnapshot(models.StateSnapshot{
Nodes: []models.Node{
{
ID: "homelab-delly",
Name: "delly",
Instance: "delly",
ClusterName: "homelab",
IsClusterMember: true,
Host: "https://10.0.0.9:8006",
LinkedAgentID: "host-1",
Status: "online",
LastSeen: now,
},
},
Hosts: []models.Host{
{
ID: "host-1",
Hostname: "delly.local",
MachineID: "machine-delly",
ReportIP: "10.0.0.9",
Status: "online",
LastSeen: now,
LinkedNodeID: "homelab-delly",
NetworkInterfaces: []models.HostNetworkInterface{
{Name: "eth0", MAC: "00:11:22:33:44:66", Addresses: []string{"10.0.0.9/24"}},
},
},
},
VMs: []models.VM{
{
ID: "delly:delly:100",
VMID: 100,
Name: "docker-vm",
Node: "delly",
Instance: "delly",
Status: "running",
LastSeen: now,
},
},
Containers: []models.Container{
{
ID: "delly:delly:104",
VMID: 104,
Name: "cloudflared",
Node: "delly",
Instance: "delly",
Status: "running",
LastSeen: now,
},
},
})
agents := rr.ListByType(ResourceTypeAgent)
if len(agents) != 1 {
t.Fatalf("expected 1 merged delly resource, got %d", len(agents))
}
parentID := agents[0].ID
vms := rr.ListByType(ResourceTypeVM)
if len(vms) != 1 {
t.Fatalf("expected 1 vm, got %d", len(vms))
}
if vms[0].ParentID == nil || *vms[0].ParentID != parentID {
t.Fatalf("expected vm parent %q, got %+v", parentID, vms[0].ParentID)
}
if vms[0].ParentName == "" {
t.Fatal("expected vm parent name to be derived")
}
if vms[0].Proxmox == nil || vms[0].Proxmox.LinkedAgentID != "host-1" {
t.Fatalf("expected vm linked agent host-1, got %+v", vms[0].Proxmox)
}
containers := rr.ListByType(ResourceTypeSystemContainer)
if len(containers) != 1 {
t.Fatalf("expected 1 container, got %d", len(containers))
}
if containers[0].ParentID == nil || *containers[0].ParentID != parentID {
t.Fatalf("expected container parent %q, got %+v", parentID, containers[0].ParentID)
}
if containers[0].Proxmox == nil || containers[0].Proxmox.LinkedAgentID != "host-1" {
t.Fatalf("expected container linked agent host-1, got %+v", containers[0].Proxmox)
}
parent, ok := rr.Get(parentID)
if !ok {
t.Fatalf("expected parent %q", parentID)
}
if parent.ChildCount != 2 {
t.Fatalf("expected parent child count 2, got %d", parent.ChildCount)
}
}
func TestResourceRegistry_MergedNodeAgentMetricsTargetMatchesAgentStoreWriteKey(t *testing.T) {
// Real-mode agent metrics land in the metrics store keyed by host.ID
// (monitor_agents.go: m.metricsStore.Write("agent", host.ID, ...), the
// write key is pinned by a canonical guardrail in internal/monitoring).
// The registry's metrics target for the merged node+agent resource must
// resolve to that same key, or every store reader that trusts the
// target — performance reports via MetricsResourceID, the drawer
// history endpoint — queries an ID with zero rows.
rr := NewRegistry(nil)
now := time.Date(2026, 6, 10, 10, 0, 0, 0, time.UTC)
rr.IngestSnapshot(models.StateSnapshot{
Nodes: []models.Node{
{
ID: "homelab-delly",
Name: "delly",
Instance: "delly",
ClusterName: "homelab",
IsClusterMember: true,
Host: "https://10.0.0.9:8006",
LinkedAgentID: "host-1",
Status: "online",
LastSeen: now,
},
},
Hosts: []models.Host{
{
ID: "host-1",
Hostname: "delly.local",
MachineID: "machine-delly",
ReportIP: "10.0.0.9",
Status: "online",
LastSeen: now,
LinkedNodeID: "homelab-delly",
NetworkInterfaces: []models.HostNetworkInterface{
{Name: "eth0", MAC: "00:11:22:33:44:66", Addresses: []string{"10.0.0.9/24"}},
},
},
},
})
agents := rr.ListByType(ResourceTypeAgent)
if len(agents) != 1 {
t.Fatalf("expected 1 merged delly resource, got %d", len(agents))
}
resource := agents[0]
targets := rr.SourceTargets(resource.ID)
if len(targets) != 2 {
t.Fatalf("expected merged proxmox+agent source targets, got %+v", targets)
}
target := BuildMetricsTargetForRegistry(rr, resource.ID)
if target == nil {
t.Fatal("BuildMetricsTargetForRegistry() returned nil")
}
if target.ResourceType != "agent" {
t.Fatalf("ResourceType = %q, want agent", target.ResourceType)
}
if target.ResourceID != "host-1" {
t.Fatalf("ResourceID = %q, want host-1 (the host.ID key the agent metrics writer uses)", target.ResourceID)
}
}
func TestResourceRegistry_ManualNodeMergeRewritesProxmoxGuestParentAndActionAgent(t *testing.T) {
store := NewMemoryStore()
if err := store.AddLink(ResourceLink{
ResourceA: "agent-old-proxmox-node",
ResourceB: "agent-current",
PrimaryID: "agent-current",
}); err != nil {
t.Fatalf("add link: %v", err)
}
rr := NewRegistry(store)
now := time.Date(2026, 5, 14, 10, 0, 0, 0, time.UTC)
staleParentID := "agent-old-proxmox-node"
rr.IngestResources([]Resource{
{
ID: staleParentID,
Type: ResourceTypeAgent,
Name: "delly",
Status: "online",
LastSeen: now,
Sources: []DataSource{SourceProxmox},
SourceStatus: map[DataSource]SourceStatus{
SourceProxmox: {Status: "online", LastSeen: now},
},
Proxmox: &ProxmoxData{
SourceID: "homelab-delly",
NodeName: "delly",
ClusterName: "homelab",
Instance: "delly",
},
},
{
ID: "agent-current",
Type: ResourceTypeAgent,
Name: "delly",
Status: "online",
LastSeen: now,
Sources: []DataSource{SourceAgent},
SourceStatus: map[DataSource]SourceStatus{
SourceAgent: {Status: "online", LastSeen: now},
},
Agent: &AgentData{
AgentID: "agent-delly",
Hostname: "delly",
},
},
{
ID: "system-container-grafana",
Type: ResourceTypeSystemContainer,
Name: "grafana",
Status: "online",
LastSeen: now,
ParentID: &staleParentID,
Sources: []DataSource{SourceProxmox},
SourceStatus: map[DataSource]SourceStatus{
SourceProxmox: {Status: "online", LastSeen: now},
},
Proxmox: &ProxmoxData{
SourceID: "delly:delly:124",
NodeName: "delly",
ClusterName: "homelab",
Instance: "delly",
VMID: 124,
},
},
})
containers := rr.ListByType(ResourceTypeSystemContainer)
if len(containers) != 1 {
t.Fatalf("expected 1 container, got %d", len(containers))
}
if containers[0].ParentID == nil || *containers[0].ParentID != "agent-current" {
t.Fatalf("expected container parent agent-current, got %+v", containers[0].ParentID)
}
if containers[0].ParentName != "delly" {
t.Fatalf("expected container parent name delly, got %q", containers[0].ParentName)
}
if containers[0].Proxmox == nil || containers[0].Proxmox.LinkedAgentID != "agent-delly" {
t.Fatalf("expected container linked agent agent-delly, got %+v", containers[0].Proxmox)
}
}
func TestResourceRegistry_ProxmoxGuestParentPrefersAgentBackedNodeDuplicate(t *testing.T) {
rr := NewRegistry(nil)
now := time.Date(2026, 5, 14, 10, 0, 0, 0, time.UTC)
rr.IngestResources([]Resource{
{
ID: "agent-current",
Type: ResourceTypeAgent,
Name: "delly",
Status: "online",
LastSeen: now,
Sources: []DataSource{SourceAgent, SourceProxmox},
SourceStatus: map[DataSource]SourceStatus{
SourceAgent: {Status: "online", LastSeen: now},
SourceProxmox: {Status: "online", LastSeen: now},
},
Agent: &AgentData{
AgentID: "agent-delly",
Hostname: "delly",
},
Proxmox: &ProxmoxData{
SourceID: "homelab-delly",
NodeName: "delly",
ClusterName: "homelab",
Instance: "delly",
},
},
{
ID: "agent-old-proxmox-node",
Type: ResourceTypeAgent,
Name: "delly",
Status: "online",
LastSeen: now,
Sources: []DataSource{SourceProxmox},
SourceStatus: map[DataSource]SourceStatus{
SourceProxmox: {Status: "online", LastSeen: now},
},
Proxmox: &ProxmoxData{
SourceID: "homelab-delly",
NodeName: "delly",
ClusterName: "homelab",
Instance: "delly",
},
},
})
rr.IngestRecords(SourceProxmox, []IngestRecord{
{
SourceID: "delly:delly:124",
ParentSourceID: "homelab-delly",
Resource: Resource{
Type: ResourceTypeSystemContainer,
Name: "grafana",
Status: "online",
LastSeen: now,
Proxmox: &ProxmoxData{
SourceID: "delly:delly:124",
NodeName: "delly",
ClusterName: "homelab",
Instance: "delly",
VMID: 124,
},
},
Identity: ResourceIdentity{
Hostnames: []string{"grafana"},
},
},
})
containers := rr.ListByType(ResourceTypeSystemContainer)
if len(containers) != 1 {
t.Fatalf("expected 1 container, got %d", len(containers))
}
if containers[0].ParentID == nil || *containers[0].ParentID != "agent-current" {
t.Fatalf("expected container parent agent-current, got %+v", containers[0].ParentID)
}
if containers[0].Proxmox == nil || containers[0].Proxmox.LinkedAgentID != "agent-delly" {
t.Fatalf("expected container linked agent agent-delly, got %+v", containers[0].Proxmox)
}
}
func TestResourceRegistry_IngestSnapshotDerivesProxmoxWorkloadParentWhenNodeSourceIDUsesPreviousClusterAlias(t *testing.T) {
rr := NewRegistry(nil)
now := time.Date(2026, 5, 14, 10, 0, 0, 0, time.UTC)
rr.IngestSnapshot(models.StateSnapshot{
Nodes: []models.Node{
{
ID: "mock-cluster-pve1",
Name: "pve1",
DisplayName: "West Production A",
Instance: "Core Fabric",
ClusterName: "Core Fabric",
IsClusterMember: true,
Status: "online",
LastSeen: now,
},
},
VMs: []models.VM{
{
ID: "Core Fabric:pve1:100",
VMID: 100,
Name: "checkout-web-01",
Node: "pve1",
Instance: "Core Fabric",
Status: "running",
LastSeen: now,
},
},
Containers: []models.Container{
{
ID: "Core Fabric:pve1:104",
VMID: 104,
Name: "auth-service-01",
Node: "pve1",
Instance: "Core Fabric",
Status: "running",
LastSeen: now,
},
},
})
agents := rr.ListByType(ResourceTypeAgent)
if len(agents) != 1 {
t.Fatalf("expected 1 Proxmox parent resource, got %d", len(agents))
}
parentID := agents[0].ID
vms := rr.ListByType(ResourceTypeVM)
if len(vms) != 1 {
t.Fatalf("expected 1 vm, got %d", len(vms))
}
if vms[0].ParentID == nil || *vms[0].ParentID != parentID {
t.Fatalf("expected vm parent %q from node metadata fallback, got %+v", parentID, vms[0].ParentID)
}
if vms[0].ParentName != "West Production A" {
t.Fatalf("expected vm parent name West Production A, got %q", vms[0].ParentName)
}
containers := rr.ListByType(ResourceTypeSystemContainer)
if len(containers) != 1 {
t.Fatalf("expected 1 container, got %d", len(containers))
}
if containers[0].ParentID == nil || *containers[0].ParentID != parentID {
t.Fatalf("expected container parent %q from node metadata fallback, got %+v", parentID, containers[0].ParentID)
}
parent, ok := rr.Get(parentID)
if !ok {
t.Fatalf("expected parent %q", parentID)
}
if parent.ChildCount != 2 {
t.Fatalf("expected parent child count 2, got %d", parent.ChildCount)
}
}
func TestResourceRegistry_IngestSnapshotUnifiesHostLinkedProxmoxNodeViewsAcrossEndpointForms(t *testing.T) {
rr := NewRegistry(nil)
now := time.Date(2026, 3, 7, 12, 0, 0, 0, time.UTC)
rr.IngestSnapshot(models.StateSnapshot{
Nodes: []models.Node{
{
ID: "minipc-ip-view",
Name: "minipc",
Instance: "standalone-ip",
Host: "https://10.0.0.5:8006",
Status: "online",
LastSeen: now,
},
{
ID: "minipc-hostname-view",
Name: "minipc",
Instance: "standalone-hostname",
Host: "https://minipc.local:8006",
Status: "online",
LastSeen: now.Add(-time.Minute),
},
},
Hosts: []models.Host{
{
ID: "host-1",
Hostname: "minipc.local",
MachineID: "machine-minipc",
ReportIP: "10.0.0.5",
Status: "online",
LastSeen: now,
LinkedNodeID: "minipc-ip-view",
NetworkInterfaces: []models.HostNetworkInterface{
{Name: "eth0", MAC: "00:11:22:33:44:55", Addresses: []string{"10.0.0.5/24"}},
},
},
},
})
agents := rr.ListByType(ResourceTypeAgent)
if len(agents) != 1 {
t.Fatalf("expected 1 unified agent resource, got %d", len(agents))
}
resource := agents[0]
if resource.Identity.MachineID != "machine-minipc" {
t.Fatalf("MachineID = %q, want machine-minipc", resource.Identity.MachineID)
}
if resource.Proxmox == nil {
t.Fatalf("expected proxmox metadata")
}
targets := rr.SourceTargets(resource.ID)
if len(targets) != 3 {
t.Fatalf("expected 3 source targets (2 proxmox + 1 agent), got %d", len(targets))
}
}
func TestResourceRegistry_IngestSnapshotCreatesPhysicalDisksFromHostSMART(t *testing.T) {
rr := NewRegistry(nil)
now := time.Date(2026, 3, 7, 12, 0, 0, 0, time.UTC)
rr.IngestSnapshot(models.StateSnapshot{
Hosts: []models.Host{
{
ID: "host-tower",
Hostname: "tower",
Status: "online",
LastSeen: now,
Disks: []models.Disk{
{Device: "/dev/sdb", Total: 12 * 1024, Mountpoint: "/mnt/disk1"},
},
Sensors: models.HostSensorSummary{
SMART: []models.HostDiskSMART{
{
Device: "/dev/sdb",
Model: "Seagate IronWolf",
Serial: "SERIAL-TOWER-1",
Type: "sata",
Temperature: 37,
Health: "PASSED",
},
},
},
},
},
})
disks := rr.ListByType(ResourceTypePhysicalDisk)
if len(disks) != 1 {
t.Fatalf("expected 1 physical disk resource, got %d", len(disks))
}
disk := disks[0]
if !containsDataSource(disk.Sources, SourceAgent) {
t.Fatalf("expected agent-backed physical disk source, got %+v", disk.Sources)
}
if disk.ParentID == nil {
t.Fatalf("expected host parent on physical disk")
}
parent, ok := rr.Get(*disk.ParentID)
if !ok || parent == nil || parent.Name != "tower" {
t.Fatalf("expected disk parent to resolve to tower host, got %+v", parent)
}
if disk.PhysicalDisk == nil || disk.PhysicalDisk.Serial != "SERIAL-TOWER-1" {
t.Fatalf("expected SMART-backed disk metadata, got %+v", disk.PhysicalDisk)
}
if disk.Identity.MachineID != "SERIAL-TOWER-1" {
t.Fatalf("MachineID = %q, want SERIAL-TOWER-1", disk.Identity.MachineID)
}
if disk.PhysicalDisk.Risk != nil {
t.Fatalf("expected healthy disk to have no risk payload, got %+v", disk.PhysicalDisk.Risk)
}
}
func TestResourceRegistry_IngestSnapshotSkipsVirtualBlockDevicesFromHostSMART(t *testing.T) {
rr := NewRegistry(nil)
now := time.Date(2026, 4, 23, 12, 0, 0, 0, time.UTC)
rr.IngestSnapshot(models.StateSnapshot{
Hosts: []models.Host{
{
ID: "host-minipc",
Hostname: "minipc",
Status: "online",
LastSeen: now,
Sensors: models.HostSensorSummary{
SMART: []models.HostDiskSMART{
{Device: "/dev/nvme0n1", Model: "Samsung 970 EVO", Serial: "REAL-DISK", Type: "nvme", Health: "PASSED"},
{Device: "/dev/zd0", Model: "", Serial: "", Type: ""},
{Device: "/dev/zd16", Model: "", Serial: "", Type: ""},
{Device: "zram0", Model: "", Serial: "", Type: ""},
{Device: "/dev/loop3", Model: "", Serial: "", Type: ""},
{Device: "/dev/dm-1", Model: "", Serial: "", Type: ""},
},
},
},
},
})
disks := rr.ListByType(ResourceTypePhysicalDisk)
if len(disks) != 1 {
t.Fatalf("expected 1 physical disk resource (virtual devices filtered), got %d", len(disks))
}
if disks[0].PhysicalDisk == nil || disks[0].PhysicalDisk.Serial != "REAL-DISK" {
t.Fatalf("expected only the real nvme disk, got %+v", disks[0].PhysicalDisk)
}
}
func TestResourceRegistry_IngestSnapshotMergesAgentAndProxmoxPhysicalDisksByIdentity(t *testing.T) {
rr := NewRegistry(nil)
now := time.Date(2026, 3, 7, 12, 0, 0, 0, time.UTC)
rr.IngestSnapshot(models.StateSnapshot{
PhysicalDisks: []models.PhysicalDisk{
{
ID: "pve-disk-1",
Node: "tower",
Instance: "pve-tower",
DevPath: "/dev/sdb",
Model: "Seagate IronWolf",
Serial: "SERIAL-TOWER-1",
Type: "sata",
Health: "PASSED",
Temperature: 34,
LastChecked: now,
},
},
Hosts: []models.Host{
{
ID: "host-tower",
Hostname: "tower",
Status: "online",
LastSeen: now,
Sensors: models.HostSensorSummary{
SMART: []models.HostDiskSMART{
{
Device: "/dev/sdb",
Model: "Seagate IronWolf",
Serial: "SERIAL-TOWER-1",
Type: "sata",
Temperature: 37,
Health: "PASSED",
},
},
},
},
},
})
disks := rr.ListByType(ResourceTypePhysicalDisk)
if len(disks) != 1 {
t.Fatalf("expected 1 merged physical disk resource, got %d", len(disks))
}
disk := disks[0]
if !containsDataSource(disk.Sources, SourceAgent) || !containsDataSource(disk.Sources, SourceProxmox) {
t.Fatalf("expected merged proxmox+agent disk sources, got %+v", disk.Sources)
}
if disk.PhysicalDisk == nil || disk.PhysicalDisk.Temperature != 37 {
t.Fatalf("expected merged SMART temperature from agent disk, got %+v", disk.PhysicalDisk)
}
if disk.Proxmox == nil || disk.Proxmox.NodeName != "tower" || disk.Proxmox.Instance != "pve-tower" {
t.Fatalf("expected merged disk to retain Proxmox node identity, got %+v", disk.Proxmox)
}
}
// Regression for issue #1483: a legacy host agent reports an NVMe disk keyed by
// its controller ("nvme0 [nvme]") with no size, while the Proxmox disks/list poll
// reports the canonical namespace ("/dev/nvme0n1") with the real capacity. The
// merge must keep the authoritative Proxmox devPath and size, and only enrich
// with the agent's SMART temperature.
func TestResourceRegistry_AgentDiskDoesNotClobberProxmoxDevPath(t *testing.T) {
rr := NewRegistry(nil)
now := time.Date(2026, 5, 29, 12, 0, 0, 0, time.UTC)
const nvmeSize = int64(2000398934016)
rr.IngestSnapshot(models.StateSnapshot{
PhysicalDisks: []models.PhysicalDisk{
{
ID: "pve-disk-1",
Node: "pve",
Instance: "pve",
DevPath: "/dev/nvme0n1",
Model: "KINGSTON SNV3S2000G",
Serial: "SERIAL-NVME-0",
Type: "nvme",
Size: nvmeSize,
Health: "PASSED",
LastChecked: now,
},
},
Hosts: []models.Host{
{
ID: "host-pve",
Hostname: "pve",
Status: "online",
LastSeen: now,
Sensors: models.HostSensorSummary{
SMART: []models.HostDiskSMART{
{
Device: "nvme0 [nvme]", // legacy controller label, no size
Model: "KINGSTON SNV3S2000G",
Serial: "SERIAL-NVME-0",
Type: "nvme",
Temperature: 37,
Health: "PASSED",
},
},
},
},
},
})
disks := rr.ListByType(ResourceTypePhysicalDisk)
if len(disks) != 1 {
t.Fatalf("expected 1 merged physical disk resource, got %d", len(disks))
}
disk := disks[0]
if disk.PhysicalDisk == nil {
t.Fatal("expected physical disk metadata")
}
if disk.PhysicalDisk.DevPath != "/dev/nvme0n1" {
t.Fatalf("devPath = %q, want canonical /dev/nvme0n1 (agent label must not clobber)", disk.PhysicalDisk.DevPath)
}
if disk.PhysicalDisk.SizeBytes != nvmeSize {
t.Fatalf("sizeBytes = %d, want authoritative Proxmox capacity %d", disk.PhysicalDisk.SizeBytes, nvmeSize)
}
if disk.PhysicalDisk.Temperature != 37 {
t.Fatalf("temperature = %d, want enriched agent SMART value 37", disk.PhysicalDisk.Temperature)
}
}
func TestShouldReplacePhysicalDiskDevPath(t *testing.T) {
cases := []struct {
name string
existing string
incoming string
want bool
}{
{"canonical not downgraded by scan label", "/dev/nvme0n1", "nvme0 [nvme]", false},
{"canonical not downgraded by bare token", "/dev/nvme0n1", "nvme0n1", false},
{"scan label upgraded to canonical", "nvme0 [nvme]", "/dev/nvme0n1", true},
{"empty existing always replaced", "", "nvme0 [nvme]", true},
{"empty incoming never replaces", "/dev/nvme0n1", "", false},
{"both canonical last writer wins", "/dev/sda", "/dev/sdb", true},
}
for _, tc := range cases {
if got := shouldReplacePhysicalDiskDevPath(tc.existing, tc.incoming); got != tc.want {
t.Errorf("%s: shouldReplacePhysicalDiskDevPath(%q, %q) = %v, want %v", tc.name, tc.existing, tc.incoming, got, tc.want)
}
}
}
func TestResourceRegistry_IngestSnapshotPropagatesUnraidDiskRole(t *testing.T) {
rr := NewRegistry(nil)
now := time.Date(2026, 3, 7, 12, 0, 0, 0, time.UTC)
rr.IngestSnapshot(models.StateSnapshot{
Hosts: []models.Host{
{
ID: "host-tower",
Hostname: "tower",
Status: "online",
LastSeen: now,
Unraid: &models.HostUnraidStorage{
ArrayStarted: true,
Disks: []models.HostUnraidDisk{
{Name: "parity", Device: "/dev/sdb", Role: "parity", Status: "online", Serial: "SERIAL-TOWER-1"},
},
},
Sensors: models.HostSensorSummary{
SMART: []models.HostDiskSMART{
{
Device: "/dev/sdb",
Model: "Seagate IronWolf",
Serial: "SERIAL-TOWER-1",
Type: "sata",
Temperature: 37,
Health: "PASSED",
},
},
},
},
},
})
disks := rr.ListByType(ResourceTypePhysicalDisk)
if len(disks) != 1 {
t.Fatalf("expected 1 physical disk resource, got %d", len(disks))
}
if disks[0].PhysicalDisk == nil {
t.Fatal("expected physical disk metadata")
}
if got := disks[0].PhysicalDisk.StorageRole; got != "parity" {
t.Fatalf("storageRole = %q, want parity", got)
}
if got := disks[0].PhysicalDisk.StorageGroup; got != "unraid-array" {
t.Fatalf("storageGroup = %q, want unraid-array", got)
}
}
func TestResourceRegistry_IngestSnapshotCreatesUnraidDisksWithoutSMART(t *testing.T) {
rr := NewRegistry(nil)
now := time.Date(2026, 3, 7, 12, 0, 0, 0, time.UTC)
rr.IngestSnapshot(models.StateSnapshot{
Hosts: []models.Host{
{
ID: "host-tower",
Hostname: "tower",
Status: "online",
LastSeen: now,
Disks: []models.Disk{
{Mountpoint: "/mnt/user", Total: 10_000, Used: 7_000, Free: 3_000, Usage: 70},
},
Unraid: &models.HostUnraidStorage{
ArrayStarted: true,
ArrayState: "STARTED",
Disks: []models.HostUnraidDisk{
{
Name: "disk1",
Device: "/dev/sdb",
Role: "data",
Status: "online",
Model: "WDC WD60EFRX",
Serial: "DATA-1",
Filesystem: "xfs",
Transport: "sata",
SizeBytes: 6_000_000_000_000,
UsedBytes: 4_000,
FreeBytes: 2_000,
Temperature: 31,
SpunDown: true,
ReadCount: 11,
WriteCount: 12,
ErrorCount: 16,
},
{
Name: "cachepool",
Device: "/dev/sdc",
Role: "cache",
Status: "online",
Model: "SSD 2000GB",
Serial: "CACHE-1",
Filesystem: "btrfs",
Transport: "sata",
SizeBytes: 2_000_000_000_000,
UsedBytes: 200,
FreeBytes: 800,
},
},
},
},
},
})
disks := rr.ListByType(ResourceTypePhysicalDisk)
if len(disks) != 2 {
t.Fatalf("expected 2 Unraid physical disks, got %d: %+v", len(disks), disks)
}
bySerial := map[string]Resource{}
for _, disk := range disks {
if disk.PhysicalDisk != nil {
bySerial[disk.PhysicalDisk.Serial] = disk
}
}
data := bySerial["DATA-1"]
if data.PhysicalDisk == nil || data.PhysicalDisk.StorageRole != "data" || data.PhysicalDisk.StorageGroup != "unraid-array" {
t.Fatalf("expected data disk to belong to unraid array, got %+v", data.PhysicalDisk)
}
if data.PhysicalDisk.SizeBytes != 6_000_000_000_000 || data.PhysicalDisk.Temperature != 31 {
t.Fatalf("expected Unraid size/temp on data disk, got %+v", data.PhysicalDisk)
}
if !data.PhysicalDisk.SpunDown || data.PhysicalDisk.ReadCount != 11 || data.PhysicalDisk.WriteCount != 12 || data.PhysicalDisk.ErrorCount != 16 {
t.Fatalf("expected Unraid disk counters on data disk, got %+v", data.PhysicalDisk)
}
cache := bySerial["CACHE-1"]
if cache.PhysicalDisk == nil || cache.PhysicalDisk.StorageRole != "cache" || cache.PhysicalDisk.StorageGroup != "cachepool" {
t.Fatalf("expected cache disk to belong to cachepool, got %+v", cache.PhysicalDisk)
}
storage := rr.ListByType(ResourceTypeStorage)
var sawArray, sawCache bool
for _, resource := range storage {
if resource.Storage == nil {
continue
}
switch resource.Storage.Type {
case "unraid-array":
sawArray = true
case "unraid-cache-pool":
sawCache = true
if resource.Name != "cachepool" || resource.Metrics == nil || resource.Metrics.Disk == nil || resource.Metrics.Disk.Percent != 20 {
t.Fatalf("unexpected cache storage resource: %+v", resource)
}
}
}
if !sawArray || !sawCache {
t.Fatalf("expected array and cache storage resources, got %+v", storage)
}
}
func TestResourceRegistry_IngestSnapshotCreatesUnraidStorageResource(t *testing.T) {
rr := NewRegistry(nil)
now := time.Date(2026, 3, 7, 12, 0, 0, 0, time.UTC)
rr.IngestSnapshot(models.StateSnapshot{
Hosts: []models.Host{
{
ID: "host-tower",
Hostname: "tower",
Status: "online",
LastSeen: now,
MachineID: "machine-tower",
Disks: []models.Disk{
{Mountpoint: "/mnt/user", Total: 1000, Used: 400, Free: 600, Usage: 40},
},
Unraid: &models.HostUnraidStorage{
ArrayStarted: true,
ArrayState: "STARTED",
NumProtected: 1,
Disks: []models.HostUnraidDisk{
{Name: "parity", Role: "parity", Status: "online"},
{Name: "disk1", Role: "data", Status: "online"},
},
},
},
},
})
storage := rr.ListByType(ResourceTypeStorage)
if len(storage) != 1 {
t.Fatalf("expected 1 unraid storage resource, got %d", len(storage))
}
resource := storage[0]
if !containsDataSource(resource.Sources, SourceAgent) {
t.Fatalf("expected agent-backed storage source, got %+v", resource.Sources)
}
if resource.ParentID == nil {
t.Fatalf("expected host parent on unraid storage")
}
if resource.Storage == nil || resource.Storage.Type != "unraid-array" || resource.Storage.Platform != "unraid" {
t.Fatalf("expected unraid storage metadata, got %+v", resource.Storage)
}
if resource.Metrics == nil || resource.Metrics.Disk == nil || resource.Metrics.Disk.Percent != 40 {
t.Fatalf("expected disk metrics from unraid storage, got %+v", resource.Metrics)
}
}
func TestResourceRegistry_IngestSnapshotParentsUnraidArrayDisksUnderStorage(t *testing.T) {
rr := NewRegistry(nil)
now := time.Date(2026, 3, 7, 12, 0, 0, 0, time.UTC)
rr.IngestSnapshot(models.StateSnapshot{
Hosts: []models.Host{
{
ID: "host-tower",
Hostname: "tower",
Status: "online",
LastSeen: now,
MachineID: "machine-tower",
Unraid: &models.HostUnraidStorage{
ArrayStarted: true,
NumProtected: 1,
Disks: []models.HostUnraidDisk{
{Name: "parity", Device: "/dev/sdb", Role: "parity", Status: "online", Serial: "SERIAL-TOWER-1"},
},
},
Sensors: models.HostSensorSummary{
SMART: []models.HostDiskSMART{
{
Device: "/dev/sdb",
Model: "Seagate IronWolf",
Serial: "SERIAL-TOWER-1",
Type: "sata",
Temperature: 37,
Health: "PASSED",
},
},
},
},
},
})
storage := rr.ListByType(ResourceTypeStorage)
disks := rr.ListByType(ResourceTypePhysicalDisk)
if len(storage) != 1 || len(disks) != 1 {
t.Fatalf("expected 1 storage and 1 disk resource, got storage=%d disk=%d", len(storage), len(disks))
}
if disks[0].ParentID == nil || *disks[0].ParentID != storage[0].ID {
t.Fatalf("expected unraid disk parent to be storage resource %q, got %+v", storage[0].ID, disks[0].ParentID)
}
if storage[0].ChildCount != 1 {
t.Fatalf("expected storage child count 1, got %d", storage[0].ChildCount)
}
}
func TestResourceRegistry_IngestSnapshotParentsInferredUnraidArrayDisksUnderStorage(t *testing.T) {
rr := NewRegistry(nil)
now := time.Date(2026, 3, 7, 12, 0, 0, 0, time.UTC)
rr.IngestSnapshot(models.StateSnapshot{
Hosts: []models.Host{
{
ID: "host-tower",
Hostname: "tower",
Status: "online",
LastSeen: now,
MachineID: "machine-tower",
Unraid: &models.HostUnraidStorage{
ArrayStarted: true,
Disks: []models.HostUnraidDisk{
{Name: "md1p1", Device: "/dev/sde", Status: "online", Slot: 1},
},
},
Sensors: models.HostSensorSummary{
SMART: []models.HostDiskSMART{
{
Device: "sde [sat]",
Model: "Array Disk",
Type: "sata",
Health: "UNKNOWN",
},
},
},
},
},
})
storage := rr.ListByType(ResourceTypeStorage)
disks := rr.ListByType(ResourceTypePhysicalDisk)
if len(storage) != 1 || len(disks) != 1 {
t.Fatalf("expected 1 storage and 1 disk resource, got storage=%d disk=%d", len(storage), len(disks))
}
if disks[0].ParentID == nil || *disks[0].ParentID != storage[0].ID {
t.Fatalf("expected inferred unraid disk parent to be storage resource %q, got %+v", storage[0].ID, disks[0].ParentID)
}
if disks[0].PhysicalDisk == nil || disks[0].PhysicalDisk.StorageRole != "data" || disks[0].PhysicalDisk.StorageGroup != "unraid-array" {
t.Fatalf("expected inferred unraid data disk metadata, got %+v", disks[0].PhysicalDisk)
}
if storage[0].ChildCount != 1 {
t.Fatalf("expected storage child count 1, got %d", storage[0].ChildCount)
}
}
func TestResourceRegistry_IngestSnapshotDerivesStorageConsumers(t *testing.T) {
rr := NewRegistry(nil)
now := time.Date(2026, 3, 8, 12, 0, 0, 0, time.UTC)
rr.IngestSnapshot(models.StateSnapshot{
Storage: []models.Storage{
{
ID: "cluster-a-pve1-local-lvm",
Name: "local-lvm",
Node: "pve1",
Instance: "cluster-a",
Type: "lvmthin",
Status: "available",
Enabled: true,
Active: true,
},
{
ID: "cluster-a-pve2-local-lvm",
Name: "local-lvm",
Node: "pve2",
Instance: "cluster-a",
Type: "lvmthin",
Status: "available",
Enabled: true,
Active: true,
},
{
ID: "cluster-a-cluster-ceph",
Name: "ceph",
Node: "cluster",
Instance: "cluster-a",
Type: "rbd",
Status: "available",
Enabled: true,
Active: true,
Shared: true,
Nodes: []string{"pve1", "pve2"},
},
{
ID: "cluster-a-pve1-media",
Name: "media",
Node: "pve1",
Instance: "cluster-a",
Type: "dir",
Status: "available",
Enabled: true,
Active: true,
Path: "/mnt/pve/media",
},
},
VMs: []models.VM{
{
ID: "vm-100",
Name: "app01",
Node: "pve1",
Instance: "cluster-a",
Status: "running",
LastSeen: now,
Disks: []models.Disk{
{Device: "local-lvm:vm-100-disk-0"},
{Device: "ceph:vm-100-disk-1"},
},
},
},
Containers: []models.Container{
{
ID: "ct-200",
Name: "media01",
Node: "pve1",
Instance: "cluster-a",
Status: "running",
LastSeen: now,
Disks: []models.Disk{
{Device: "/mnt/pve/media/subvol-200-disk-1"},
{Device: "local-lvm:vm-200-disk-0"},
},
},
},
})
storageResources := rr.ListByType(ResourceTypeStorage)
local := findStorageResource(storageResources, "local-lvm", "pve1")
if local.Storage == nil {
t.Fatalf("expected local-lvm storage metadata, got %+v", local)
}
if got := local.Storage.ConsumerCount; got != 2 {
t.Fatalf("local-lvm consumerCount = %d, want 2", got)
}
if got := local.Storage.ConsumerTypes; len(got) != 2 || got[0] != "system-container" || got[1] != "vm" {
t.Fatalf("local-lvm consumerTypes = %v, want [system-container vm]", got)
}
if len(local.Storage.TopConsumers) != 2 {
t.Fatalf("local-lvm topConsumers length = %d, want 2", len(local.Storage.TopConsumers))
}
if !hasStorageConsumer(local.Storage.TopConsumers, "app01", ResourceTypeVM, 1) {
t.Fatalf("expected vm consumer on local-lvm, got %+v", local.Storage.TopConsumers)
}
if !hasStorageConsumer(local.Storage.TopConsumers, "media01", ResourceTypeSystemContainer, 1) {
t.Fatalf("expected container consumer on local-lvm, got %+v", local.Storage.TopConsumers)
}
if got := local.Storage.ConsumerImpactSummary; got != "Affects 2 dependent resources: media01, app01" {
t.Fatalf("local-lvm consumerImpactSummary = %q", got)
}
ceph := findStorageResource(storageResources, "ceph", "cluster")
if ceph.Storage == nil || ceph.Storage.ConsumerCount != 1 {
t.Fatalf("ceph consumerCount = %+v, want 1", ceph.Storage)
}
if !hasStorageConsumer(ceph.Storage.TopConsumers, "app01", ResourceTypeVM, 1) {
t.Fatalf("expected vm consumer on ceph, got %+v", ceph.Storage.TopConsumers)
}
media := findStorageResource(storageResources, "media", "pve1")
if media.Storage == nil || media.Storage.ConsumerCount != 1 {
t.Fatalf("media consumerCount = %+v, want 1", media.Storage)
}
if !hasStorageConsumer(media.Storage.TopConsumers, "media01", ResourceTypeSystemContainer, 1) {
t.Fatalf("expected container consumer on media, got %+v", media.Storage.TopConsumers)
}
otherLocal := findStorageResource(storageResources, "local-lvm", "pve2")
if otherLocal.Storage == nil {
t.Fatalf("expected second local-lvm storage metadata")
}
if otherLocal.Storage.ConsumerCount != 0 {
t.Fatalf("expected pve2 local-lvm to remain without consumers, got %+v", otherLocal.Storage)
}
}
func TestResourceRegistry_IngestSnapshotProjectsCephPoolsAsStorage(t *testing.T) {
now := time.Date(2026, 3, 8, 12, 0, 0, 0, time.UTC)
rr := NewRegistry(nil)
rr.IngestSnapshot(models.StateSnapshot{
CephClusters: []models.CephCluster{
{
ID: "Main-fsid123",
Instance: "Main",
Name: "Main Ceph",
FSID: "fsid123",
Health: "HEALTH_OK",
LastUpdated: now,
Pools: []models.CephPool{
{
ID: 1,
Name: "data_replication",
StoredBytes: 70,
AvailableBytes: 30,
PercentUsed: 70,
},
},
},
},
})
storageResources := rr.ListByType(ResourceTypeStorage)
pool := findStorageResource(storageResources, "data_replication", "cluster")
if pool.Storage == nil {
t.Fatalf("expected Ceph pool storage metadata, got %+v", pool)
}
if !pool.Storage.IsCeph || !pool.Storage.Shared || pool.Storage.Type != "ceph" {
t.Fatalf("unexpected Ceph pool storage metadata: %+v", pool.Storage)
}
if pool.Metrics == nil || pool.Metrics.Disk == nil || pool.Metrics.Disk.Percent != 70 {
t.Fatalf("expected Ceph pool disk metrics, got %+v", pool.Metrics)
}
target := BuildMetricsTargetForRegistry(rr, pool.ID)
if target == nil {
t.Fatalf("expected metrics target for Ceph pool storage")
}
wantID := models.CephPoolStorageID("Main", "data_replication")
if target.ResourceType != "storage" || target.ResourceID != wantID {
t.Fatalf("metrics target = %+v, want storage/%s", target, wantID)
}
}
func TestResourceRegistry_IngestSnapshotDerivesPBSDatastoreConsumers(t *testing.T) {
rr := NewRegistry(nil)
now := time.Date(2026, 3, 8, 12, 0, 0, 0, time.UTC)
rr.IngestSnapshot(models.StateSnapshot{
PBSInstances: []models.PBSInstance{
{
ID: "pbs-1",
Name: "pbs-main",
Host: "https://pbs-main.local:8007",
Status: "online",
LastSeen: now,
Datastores: []models.PBSDatastore{
{Name: "backup-store", Status: "online"},
},
},
},
PBSBackups: []models.PBSBackup{
{
ID: "pbs-1/backup-store/vm/100",
Instance: "pbs-main",
Datastore: "backup-store",
Namespace: "pve",
BackupType: "vm",
VMID: "100",
BackupTime: now,
},
{
ID: "pbs-1/backup-store/ct/200",
Instance: "pbs-main",
Datastore: "backup-store",
Namespace: "nat",
BackupType: "ct",
VMID: "200",
BackupTime: now,
},
},
VMs: []models.VM{
{
ID: "vm-100",
Name: "app01",
Node: "pve-1",
Instance: "pve",
Status: "running",
LastSeen: now,
VMID: 100,
},
},
Containers: []models.Container{
{
ID: "ct-200",
Name: "media01",
Node: "pve-2",
Instance: "pve-nat",
Status: "running",
LastSeen: now,
VMID: 200,
},
},
})
storageResources := rr.ListByType(ResourceTypeStorage)
datastore := findStorageResourceByPlatform(storageResources, "backup-store", "pbs", "datastore")
if datastore.Storage == nil {
t.Fatalf("expected PBS datastore storage metadata, got %+v", datastore)
}
if got := datastore.Storage.ConsumerCount; got != 2 {
t.Fatalf("backup-store consumerCount = %d, want 2", got)
}
if got := datastore.Storage.ConsumerTypes; len(got) != 2 || got[0] != "system-container" || got[1] != "vm" {
t.Fatalf("backup-store consumerTypes = %v, want [system-container vm]", got)
}
if len(datastore.Storage.TopConsumers) != 2 {
t.Fatalf("backup-store topConsumers length = %d, want 2", len(datastore.Storage.TopConsumers))
}
if !hasStorageConsumer(datastore.Storage.TopConsumers, "app01", ResourceTypeVM, 1) {
t.Fatalf("expected vm consumer on backup-store, got %+v", datastore.Storage.TopConsumers)
}
if !hasStorageConsumer(datastore.Storage.TopConsumers, "media01", ResourceTypeSystemContainer, 1) {
t.Fatalf("expected container consumer on backup-store, got %+v", datastore.Storage.TopConsumers)
}
if got := datastore.Storage.ConsumerImpactSummary; got != "Puts backups for 2 protected workloads at risk: media01, app01" {
t.Fatalf("backup-store consumerImpactSummary = %q", got)
}
pbsResources := rr.ListByType(ResourceTypePBS)
if len(pbsResources) != 1 {
t.Fatalf("expected 1 PBS resource, got %d", len(pbsResources))
}
pbs := pbsResources[0]
if pbs.PBS == nil {
t.Fatalf("expected PBS payload, got %+v", pbs)
}
if got := pbs.PBS.ProtectedWorkloadCount; got != 2 {
t.Fatalf("protectedWorkloadCount = %d, want 2", got)
}
if got := pbs.PBS.ProtectedWorkloadTypes; len(got) != 2 || got[0] != "system-container" || got[1] != "vm" {
t.Fatalf("protectedWorkloadTypes = %v, want [system-container vm]", got)
}
if got := pbs.PBS.ProtectedWorkloadNames; len(got) != 2 || got[0] != "media01" || got[1] != "app01" {
t.Fatalf("protectedWorkloadNames = %v, want [media01 app01] in sorted rollup order", got)
}
if got := pbs.PBS.AffectedDatastoreCount; got != 0 {
t.Fatalf("affectedDatastoreCount = %d, want 0 without datastore risk", got)
}
if got := pbs.PBS.ProtectedWorkloadSummary; got != "Puts backups for 2 protected workloads at risk: media01, app01" {
t.Fatalf("protectedWorkloadSummary = %q", got)
}
if got := pbs.PBS.PostureSummary; got != "Puts backups for 2 protected workloads at risk: media01, app01" {
t.Fatalf("postureSummary = %q", got)
}
}
func TestResourceRegistry_IngestResourcesDerivesPrimaryIncidentRollups(t *testing.T) {
rr := NewRegistry(nil)
rr.IngestResources([]Resource{
{
ID: "storage:tank",
Type: ResourceTypeStorage,
Name: "tank",
Status: StatusWarning,
Storage: &StorageMeta{
Platform: "truenas",
Topology: "pool",
Protection: "zfs",
Risk: &StorageRisk{
Level: storagehealth.RiskWarning,
Reasons: []StorageRiskReason{
{Code: "capacity_runway_low", Severity: storagehealth.RiskWarning, Summary: "Storage tank is 92% full"},
{Code: "zfs_pool_state", Severity: storagehealth.RiskWarning, Summary: "ZFS pool tank is DEGRADED"},
},
},
},
Incidents: []ResourceIncident{
{Code: "capacity_runway_low", Severity: storagehealth.RiskWarning, Summary: "Storage tank is 92% full"},
{Code: "zfs_pool_state", Severity: storagehealth.RiskWarning, Summary: "ZFS pool tank is DEGRADED"},
},
},
{
ID: "pbs:main",
Type: ResourceTypePBS,
Name: "pbs-main",
Status: StatusWarning,
PBS: &PBSData{
StorageRisk: &StorageRisk{
Level: storagehealth.RiskWarning,
Reasons: []StorageRiskReason{
{Code: "pbs_datastore_state", Severity: storagehealth.RiskWarning, Summary: "PBS datastore archive is READ_ONLY"},
{Code: "capacity_runway_low", Severity: storagehealth.RiskWarning, Summary: "PBS datastore fast is 96% full"},
},
},
AffectedDatastoreSummary: "Affects 1 backup datastore: fast",
ProtectedWorkloadSummary: "Puts backups for 2 protected workloads at risk: media01, app01",
PostureSummary: "Affects 1 backup datastore: fast. Puts backups for 2 protected workloads at risk: media01, app01",
},
Incidents: []ResourceIncident{
{Code: "pbs_datastore_state", Severity: storagehealth.RiskWarning, Summary: "PBS datastore archive is READ_ONLY"},
{Code: "capacity_runway_low", Severity: storagehealth.RiskWarning, Summary: "PBS datastore fast is 96% full"},
},
},
})
storage, ok := rr.Get("storage:tank")
if !ok {
t.Fatal("expected storage resource")
}
if storage.IncidentCount != 2 || storage.IncidentCode != "zfs_pool_state" || storage.IncidentSeverity != storagehealth.RiskWarning || storage.IncidentSummary != "ZFS pool tank is DEGRADED" || storage.IncidentCategory != IncidentCategoryProtection || storage.IncidentLabel != "Protection Reduced" || storage.IncidentPriority != 3400 || storage.IncidentImpactSummary != "" || storage.IncidentUrgency != IncidentUrgencyToday || storage.IncidentAction != "Investigate degraded protection and schedule maintenance to restore redundancy" {
t.Fatalf("unexpected storage incident rollup %+v", storage)
}
pbs, ok := rr.Get("pbs:main")
if !ok {
t.Fatal("expected pbs resource")
}
if pbs.IncidentCount != 2 || pbs.IncidentCode != "pbs_datastore_state" || pbs.IncidentSeverity != storagehealth.RiskWarning || pbs.IncidentSummary != "PBS datastore archive is READ_ONLY" || pbs.IncidentCategory != IncidentCategoryRecoverability || pbs.IncidentLabel != "Backup Coverage At Risk" || pbs.IncidentPriority != 3500 || pbs.IncidentImpactSummary != "" || pbs.IncidentUrgency != IncidentUrgencyToday || pbs.IncidentAction != "Investigate backup target health and preserve backup coverage" {
t.Fatalf("unexpected pbs incident rollup %+v", pbs)
}
}
func TestResourceRegistry_IngestSnapshotDerivesPhysicalDiskRisk(t *testing.T) {
rr := NewRegistry(nil)
now := time.Date(2026, 3, 7, 12, 0, 0, 0, time.UTC)
pending := int64(2)
rr.IngestSnapshot(models.StateSnapshot{
Hosts: []models.Host{
{
ID: "host-risky",
Hostname: "tower",
Status: "online",
LastSeen: now,
Sensors: models.HostSensorSummary{
SMART: []models.HostDiskSMART{
{
Device: "/dev/sdc",
Model: "Seagate IronWolf",
Serial: "SERIAL-RISK-1",
Type: "sata",
Temperature: 64,
Health: "PASSED",
Attributes: &models.SMARTAttributes{
PendingSectors: &pending,
},
},
},
},
},
},
})
disks := rr.ListByType(ResourceTypePhysicalDisk)
if len(disks) != 1 {
t.Fatalf("expected 1 physical disk resource, got %d", len(disks))
}
disk := disks[0]
if disk.Status != StatusWarning {
t.Fatalf("Status = %q, want %q", disk.Status, StatusWarning)
}
if disk.PhysicalDisk == nil || disk.PhysicalDisk.Risk == nil {
t.Fatalf("expected disk risk payload, got %+v", disk.PhysicalDisk)
}
if disk.PhysicalDisk.Risk.Level != storagehealth.RiskCritical {
t.Fatalf("risk level = %q, want %q", disk.PhysicalDisk.Risk.Level, storagehealth.RiskCritical)
}
if len(disk.PhysicalDisk.Risk.Reasons) == 0 || disk.PhysicalDisk.Risk.Reasons[0].Code != "pending_sectors" {
t.Fatalf("expected pending sectors reason, got %+v", disk.PhysicalDisk.Risk.Reasons)
}
}
func TestResourceRegistry_MergePhysicalDiskKeepsIncidentBackedRisk(t *testing.T) {
rr := NewRegistry(nil)
now := time.Date(2026, 3, 30, 12, 0, 0, 0, time.UTC)
rr.IngestRecords(SourceAgent, []IngestRecord{
{
SourceID: "agent-disk-sdc",
Resource: Resource{
Type: ResourceTypePhysicalDisk,
Name: "sdc",
Status: StatusOnline,
LastSeen: now,
PhysicalDisk: &PhysicalDiskMeta{
DevPath: "/dev/sdc",
Model: "Seagate IronWolf",
Serial: "SERIAL-RISK-2",
DiskType: "sata",
SizeBytes: 4_000_000_000_000,
Health: "PASSED",
Temperature: 38,
Wearout: -1,
},
},
Identity: ResourceIdentity{
MachineID: "SERIAL-RISK-2",
Hostnames: []string{"truenas-main"},
},
},
})
rr.IngestRecords(SourceTrueNAS, []IngestRecord{
{
SourceID: "truenas-disk-sdc",
Resource: Resource{
Type: ResourceTypePhysicalDisk,
Name: "sdc",
Status: StatusWarning,
LastSeen: now,
PhysicalDisk: &PhysicalDiskMeta{
DevPath: "/dev/sdc",
Model: "Seagate IronWolf",
Serial: "SERIAL-RISK-2",
DiskType: "sata",
SizeBytes: 4_000_000_000_000,
Health: "UNKNOWN",
Temperature: 63,
Wearout: -1,
},
Incidents: []ResourceIncident{
{
Code: "truenas_smart",
Severity: storagehealth.RiskCritical,
Summary: "Device /dev/sdc has SMART test failures.",
},
},
},
Identity: ResourceIdentity{
MachineID: "SERIAL-RISK-2",
Hostnames: []string{"truenas-main"},
},
},
})
disks := rr.ListByType(ResourceTypePhysicalDisk)
if len(disks) != 1 {
t.Fatalf("expected 1 merged physical disk, got %d", len(disks))
}
disk := disks[0]
if disk.PhysicalDisk == nil || disk.PhysicalDisk.Risk == nil {
t.Fatalf("expected merged disk risk, got %+v", disk.PhysicalDisk)
}
if disk.PhysicalDisk.Risk.Level != storagehealth.RiskCritical {
t.Fatalf("risk level = %q, want %q", disk.PhysicalDisk.Risk.Level, storagehealth.RiskCritical)
}
foundSmartReason := false
for _, reason := range disk.PhysicalDisk.Risk.Reasons {
if reason.Code == "truenas_smart" {
foundSmartReason = true
break
}
}
if !foundSmartReason {
t.Fatalf("expected SMART-backed risk reason after merge, got %+v", disk.PhysicalDisk.Risk.Reasons)
}
}
func TestResourceRegistry_MergeTrueNASPayloadOnIdentityMatch(t *testing.T) {
rr := NewRegistry(nil)
now := time.Date(2026, 5, 8, 12, 0, 0, 0, time.UTC)
rr.IngestRecords(SourceAgent, []IngestRecord{
{
SourceID: "agent-archive",
Resource: Resource{
Type: ResourceTypeAgent,
Name: "archive",
Status: StatusOnline,
LastSeen: now,
Agent: &AgentData{
Hostname: "archive.local",
OSName: "Debian GNU/Linux",
},
},
Identity: ResourceIdentity{
MachineID: "archive-machine",
Hostnames: []string{"archive.local"},
},
},
})
rr.IngestRecords(SourceTrueNAS, []IngestRecord{
{
SourceID: "truenas-archive",
Resource: Resource{
Type: ResourceTypeAgent,
Name: "archive.local",
Status: StatusWarning,
LastSeen: now.Add(time.Minute),
TrueNAS: &TrueNASData{
Hostname: "archive.local",
Version: "24.10",
UptimeSeconds: 86400,
StorageRisk: &StorageRisk{Level: storagehealth.RiskWarning},
StorageRiskSummary: "Pool tank is DEGRADED",
StoragePostureSummary: "Pool tank is DEGRADED",
ProtectionReduced: true,
ProtectionSummary: "Pool redundancy is reduced",
},
Agent: &AgentData{
Hostname: "archive.local",
OSName: "TrueNAS SCALE",
},
},
Identity: ResourceIdentity{
MachineID: "archive-machine",
Hostnames: []string{"archive.local"},
},
},
})
resources := rr.ListByType(ResourceTypeAgent)
if len(resources) != 1 {
t.Fatalf("ListByType(ResourceTypeAgent) returned %d resources, want 1", len(resources))
}
resource := resources[0]
if !hasDataSource(resource.Sources, SourceAgent) || !hasDataSource(resource.Sources, SourceTrueNAS) {
t.Fatalf("sources = %#v, want agent and truenas", resource.Sources)
}
if resource.TrueNAS == nil {
t.Fatal("expected TrueNAS payload to merge onto matched agent resource")
}
if resource.TrueNAS.Version != "24.10" || resource.TrueNAS.UptimeSeconds != 86400 {
t.Fatalf("TrueNAS payload not refreshed: %+v", resource.TrueNAS)
}
if resource.TrueNAS.StorageRisk == nil || resource.TrueNAS.StorageRisk.Level != storagehealth.RiskWarning {
t.Fatalf("TrueNAS storage risk not merged: %+v", resource.TrueNAS.StorageRisk)
}
if resource.Agent == nil || resource.Agent.OSName != "Debian GNU/Linux" {
t.Fatalf("SourceAgent facet should remain authoritative, got %+v", resource.Agent)
}
}
func hasStorageConsumer(consumers []StorageConsumerMeta, name string, resourceType ResourceType, diskCount int) bool {
for _, consumer := range consumers {
if consumer.Name == name && consumer.ResourceType == resourceType && consumer.DiskCount == diskCount {
return true
}
}
return false
}
func findStorageResource(resources []Resource, name, node string) Resource {
for _, resource := range resources {
if resource.Name != name || resource.Proxmox == nil || resource.Proxmox.NodeName != node {
continue
}
return resource
}
return Resource{}
}
func findStorageResourceByPlatform(resources []Resource, name, platform, topology string) Resource {
for _, resource := range resources {
if resource.Name != name || resource.Storage == nil {
continue
}
if resource.Storage.Platform != platform || resource.Storage.Topology != topology {
continue
}
return resource
}
return Resource{}
}
func TestResourceRegistry_BuildChildCounts_ReparentClearsOldParentCount(t *testing.T) {
rr := NewRegistry(nil)
now := time.Date(2026, 2, 12, 1, 0, 0, 0, time.UTC)
rr.IngestRecords(SourceProxmox, []IngestRecord{
{
SourceID: "node-a",
Resource: Resource{
Type: ResourceTypeAgent,
Name: "node-a",
Status: StatusOnline,
LastSeen: now,
},
Identity: ResourceIdentity{Hostnames: []string{"node-a"}},
},
{
SourceID: "node-b",
Resource: Resource{
Type: ResourceTypeAgent,
Name: "node-b",
Status: StatusOnline,
LastSeen: now,
},
Identity: ResourceIdentity{Hostnames: []string{"node-b"}},
},
{
SourceID: "vm-100",
ParentSourceID: "node-a",
Resource: Resource{
Type: ResourceTypeVM,
Name: "vm-100",
Status: StatusOnline,
LastSeen: now,
},
Identity: ResourceIdentity{Hostnames: []string{"vm-100"}},
},
})
parentAID := rr.sourceSpecificID(ResourceTypeAgent, SourceProxmox, "node-a")
parentBID := rr.sourceSpecificID(ResourceTypeAgent, SourceProxmox, "node-b")
vmID := rr.sourceSpecificID(ResourceTypeVM, SourceProxmox, "vm-100")
parentA, ok := rr.Get(parentAID)
if !ok {
t.Fatalf("expected parent A %q to exist", parentAID)
}
parentB, ok := rr.Get(parentBID)
if !ok {
t.Fatalf("expected parent B %q to exist", parentBID)
}
vm, ok := rr.Get(vmID)
if !ok {
t.Fatalf("expected vm %q to exist", vmID)
}
if parentA.ChildCount != 1 || parentB.ChildCount != 0 {
t.Fatalf("expected initial child counts parentA=1 parentB=0, got parentA=%d parentB=%d", parentA.ChildCount, parentB.ChildCount)
}
if vm.ParentName != "node-a" {
t.Fatalf("expected vm parent name %q, got %q", "node-a", vm.ParentName)
}
rr.IngestRecords(SourceProxmox, []IngestRecord{
{
SourceID: "node-a",
Resource: Resource{
Type: ResourceTypeAgent,
Name: "node-a",
Status: StatusOnline,
LastSeen: now.Add(30 * time.Second),
},
Identity: ResourceIdentity{Hostnames: []string{"node-a"}},
},
{
SourceID: "node-b",
Resource: Resource{
Type: ResourceTypeAgent,
Name: "node-b",
Status: StatusOnline,
LastSeen: now.Add(30 * time.Second),
},
Identity: ResourceIdentity{Hostnames: []string{"node-b"}},
},
{
SourceID: "vm-100",
ParentSourceID: "node-b",
Resource: Resource{
Type: ResourceTypeVM,
Name: "vm-100",
Status: StatusOnline,
LastSeen: now.Add(30 * time.Second),
},
Identity: ResourceIdentity{Hostnames: []string{"vm-100"}},
},
})
parentA, ok = rr.Get(parentAID)
if !ok {
t.Fatalf("expected parent A %q to exist after reparent", parentAID)
}
parentB, ok = rr.Get(parentBID)
if !ok {
t.Fatalf("expected parent B %q to exist after reparent", parentBID)
}
vm, ok = rr.Get(vmID)
if !ok {
t.Fatalf("expected vm %q to exist after reparent", vmID)
}
if parentA.ChildCount != 0 || parentB.ChildCount != 1 {
t.Fatalf("expected child counts parentA=0 parentB=1 after reparent, got parentA=%d parentB=%d", parentA.ChildCount, parentB.ChildCount)
}
if vm.ParentName != "node-b" {
t.Fatalf("expected vm parent name %q after reparent, got %q", "node-b", vm.ParentName)
}
}
func TestResourceRegistry_BuildChildCounts_SourceUpdateClearsRemovedParent(t *testing.T) {
rr := NewRegistry(nil)
now := time.Date(2026, 2, 12, 1, 0, 0, 0, time.UTC)
rr.IngestRecords(SourceProxmox, []IngestRecord{
{
SourceID: "node-a",
Resource: Resource{
Type: ResourceTypeAgent,
Name: "node-a",
Status: StatusOnline,
LastSeen: now,
},
Identity: ResourceIdentity{Hostnames: []string{"node-a"}},
},
{
SourceID: "vm-100",
ParentSourceID: "node-a",
Resource: Resource{
Type: ResourceTypeVM,
Name: "vm-100",
Status: StatusOnline,
LastSeen: now,
},
Identity: ResourceIdentity{Hostnames: []string{"vm-100"}},
},
})
parentID := rr.sourceSpecificID(ResourceTypeAgent, SourceProxmox, "node-a")
vmID := rr.sourceSpecificID(ResourceTypeVM, SourceProxmox, "vm-100")
parent, ok := rr.Get(parentID)
if !ok {
t.Fatalf("expected parent %q to exist", parentID)
}
vm, ok := rr.Get(vmID)
if !ok {
t.Fatalf("expected vm %q to exist", vmID)
}
if parent.ChildCount != 1 {
t.Fatalf("expected parent child count 1 before clearing parent, got %d", parent.ChildCount)
}
if vm.ParentID == nil || *vm.ParentID != parentID {
t.Fatalf("expected vm parent id %q before clearing parent, got %+v", parentID, vm.ParentID)
}
rr.IngestRecords(SourceProxmox, []IngestRecord{
{
SourceID: "node-a",
Resource: Resource{
Type: ResourceTypeAgent,
Name: "node-a",
Status: StatusOnline,
LastSeen: now.Add(30 * time.Second),
},
Identity: ResourceIdentity{Hostnames: []string{"node-a"}},
},
{
SourceID: "vm-100",
Resource: Resource{
Type: ResourceTypeVM,
Name: "vm-100",
Status: StatusOnline,
LastSeen: now.Add(30 * time.Second),
},
Identity: ResourceIdentity{Hostnames: []string{"vm-100"}},
},
})
parent, ok = rr.Get(parentID)
if !ok {
t.Fatalf("expected parent %q to exist after clearing parent", parentID)
}
vm, ok = rr.Get(vmID)
if !ok {
t.Fatalf("expected vm %q to exist after clearing parent", vmID)
}
if parent.ChildCount != 0 {
t.Fatalf("expected parent child count 0 after clearing parent, got %d", parent.ChildCount)
}
if vm.ParentID != nil {
t.Fatalf("expected vm parent id to clear after source update, got %+v", vm.ParentID)
}
if vm.ParentName != "" {
t.Fatalf("expected vm parent name to clear after source update, got %q", vm.ParentName)
}
}
func TestRegistryMonitoredSystemCountDedupesAcrossTopLevelViews(t *testing.T) {
rr := NewRegistry(nil)
rr.IngestRecords(SourceAgent, []IngestRecord{
{
SourceID: "host-1",
Resource: Resource{
ID: "host-1",
Type: ResourceTypeAgent,
Name: "lab-a",
Status: StatusOnline,
Agent: &AgentData{
AgentID: "agent-1",
Hostname: "lab-a",
MachineID: "machine-1",
},
Identity: ResourceIdentity{
MachineID: "machine-1",
Hostnames: []string{"lab-a"},
},
},
},
})
rr.IngestRecords(SourcePBS, []IngestRecord{
{
SourceID: "pbs-1",
Resource: Resource{
ID: "pbs-1",
Type: ResourceTypePBS,
Name: "pbs-a",
Status: StatusOnline,
PBS: &PBSData{
InstanceID: "pbs-1",
Hostname: "lab-a",
HostURL: "https://lab-a:8007",
},
},
},
})
rr.IngestRecords(SourcePMG, []IngestRecord{
{
SourceID: "pmg-1",
Resource: Resource{
ID: "pmg-1",
Type: ResourceTypePMG,
Name: "pmg-b",
Status: StatusOnline,
PMG: &PMGData{
InstanceID: "pmg-1",
Hostname: "mail-b",
},
},
},
})
rr.IngestRecords(SourceK8s, []IngestRecord{
{
SourceID: "k8s-1",
Resource: Resource{
ID: "k8s-1",
Type: ResourceTypeK8sCluster,
Name: "cluster-a",
Status: StatusOnline,
Kubernetes: &K8sData{
ClusterID: "cluster-a",
AgentID: "k8s-agent-1",
Server: "https://cluster-a.example:6443",
},
},
},
})
if got := MonitoredSystemCount(rr); got != 3 {
t.Fatalf("MonitoredSystemCount() = %d, want 3", got)
}
}
func TestRegistryHasMatchingMonitoredSystemUsesCanonicalHostIdentity(t *testing.T) {
rr := NewRegistry(nil)
rr.IngestRecords(SourceTrueNAS, []IngestRecord{
{
SourceID: "truenas-1",
Resource: Resource{
ID: "truenas-1",
Type: ResourceTypeAgent,
Name: "archive",
Status: StatusOnline,
TrueNAS: &TrueNASData{
Hostname: "archive.local",
},
Identity: ResourceIdentity{
Hostnames: []string{"archive.local"},
},
},
},
})
if !HasMatchingMonitoredSystem(rr, MonitoredSystemCandidate{
Type: ResourceTypeAgent,
Hostname: "archive.local",
HostURL: "https://archive.local",
}) {
t.Fatal("expected candidate to match existing counted system")
}
if HasMatchingMonitoredSystem(rr, MonitoredSystemCandidate{
Type: ResourceTypePBS,
Hostname: "other.local",
HostURL: "https://other.local:8007",
}) {
t.Fatal("expected unrelated candidate not to match existing counted system")
}
}
func TestRegistryIngestSnapshotPublishesCanonicalKubernetesMetricsTargets(t *testing.T) {
rr := NewRegistry(nil)
rr.IngestSnapshot(models.StateSnapshot{
KubernetesClusters: []models.KubernetesCluster{
{
ID: "cluster-1",
Name: "cluster-a",
DisplayName: "Cluster A",
AgentID: "agent-cluster-1",
Context: "cluster-a",
Nodes: []models.KubernetesNode{
{
UID: "node-1",
Name: "worker-1",
},
},
Pods: []models.KubernetesPod{
{
UID: "pod-1",
Namespace: "default",
Name: "api-7dd5f6c7f9-jx2ql",
},
},
Deployments: []models.KubernetesDeployment{
{
UID: "deploy-1",
Namespace: "default",
Name: "api",
},
},
},
},
})
assertMetricsTarget := func(resourceType ResourceType, want string) {
t.Helper()
resources := rr.ListByType(resourceType)
if len(resources) != 1 {
t.Fatalf("ListByType(%q) len = %d, want 1", resourceType, len(resources))
}
target := BuildMetricsTargetForRegistry(rr, resources[0].ID)
if target == nil {
t.Fatalf("BuildMetricsTargetForRegistry(%q) returned nil MetricsTarget", resources[0].ID)
}
if target.ResourceID != want {
t.Fatalf("ListByType(%q) metrics target = %q, want %q", resourceType, target.ResourceID, want)
}
}
assertMetricsTarget(ResourceTypeK8sCluster, "cluster-1")
assertMetricsTarget(ResourceTypeK8sNode, "cluster-1:node:node-1")
assertMetricsTarget(ResourceTypePod, "k8s:cluster-1:pod:pod-1")
assertMetricsTarget(ResourceTypeK8sDeployment, "cluster-1:deployment:deploy-1")
}
func TestRegistryIngestSnapshotMergesLinkedKubernetesNodeIntoAgentResource(t *testing.T) {
rr := NewRegistry(nil)
rr.IngestSnapshot(models.StateSnapshot{
Hosts: []models.Host{
{
ID: "host-1",
Hostname: "prod-euw1-k8s-01",
MachineID: "machine-k8s-1",
Status: "online",
},
},
KubernetesClusters: []models.KubernetesCluster{
{
ID: "cluster-1",
Name: "production",
AgentID: "agent-cluster-1",
Nodes: []models.KubernetesNode{
{
UID: "node-1",
Name: "prod-euw1-k8s-01",
Ready: true,
},
},
},
},
})
resources := rr.List()
if got := len(resources); got != 2 {
t.Fatalf("expected cluster + merged agent resource, got %d resources", got)
}
var cluster *Resource
var host *Resource
for i := range resources {
switch resources[i].Type {
case ResourceTypeK8sCluster:
cluster = &resources[i]
case ResourceTypeAgent:
host = &resources[i]
case ResourceTypeK8sNode:
t.Fatalf("expected linked kubernetes node to merge into backing agent, got standalone node %+v", resources[i])
}
}
if cluster == nil {
t.Fatal("expected kubernetes cluster resource")
}
if host == nil {
t.Fatal("expected merged agent resource")
}
if !hasDataSource(host.Sources, SourceAgent) || !hasDataSource(host.Sources, SourceK8s) {
t.Fatalf("expected merged host sources to include agent+kubernetes, got %+v", host.Sources)
}
if host.Kubernetes == nil {
t.Fatal("expected merged host to retain kubernetes node payload")
}
if got := strings.TrimSpace(host.Kubernetes.NodeName); got != "prod-euw1-k8s-01" {
t.Fatalf("kubernetes node name = %q, want prod-euw1-k8s-01", got)
}
if host.ParentID == nil || strings.TrimSpace(*host.ParentID) != cluster.ID {
t.Fatalf("expected merged host parent to point at kubernetes cluster %q, got %+v", cluster.ID, host.ParentID)
}
}
func TestRegistryHasMatchingMonitoredSystemRejectsInvalidCandidate(t *testing.T) {
rr := NewRegistry(nil)
rr.IngestRecords(SourceAgent, []IngestRecord{
{
SourceID: "host-1",
Resource: Resource{
ID: "host-1",
Type: ResourceTypeAgent,
Name: "lab-a",
Status: StatusOnline,
Agent: &AgentData{
AgentID: "agent-1",
Hostname: "lab-a",
MachineID: "machine-1",
},
Identity: ResourceIdentity{
MachineID: "machine-1",
Hostnames: []string{"lab-a"},
},
},
},
})
if HasMatchingMonitoredSystem(rr, MonitoredSystemCandidate{}) {
t.Fatal("expected empty candidate not to match an existing counted system")
}
if HasMatchingMonitoredSystem(rr, MonitoredSystemCandidate{
Source: SourceAgent,
Name: " ",
Hostname: " ",
HostURL: " ",
}) {
t.Fatal("expected whitespace-only candidate not to match an existing counted system")
}
}
func TestProjectMonitoredSystemRecords_DedupesVMwareHostAgainstExistingAgentCount(t *testing.T) {
rr := NewRegistry(nil)
now := time.Date(2026, 4, 7, 12, 0, 0, 0, time.UTC)
rr.IngestRecords(SourceAgent, []IngestRecord{
{
SourceID: "agent-host-1",
Resource: Resource{
Type: ResourceTypeAgent,
Name: "esxi-01.lab.local",
Status: StatusOnline,
LastSeen: now,
},
Identity: ResourceIdentity{
DMIUUID: "uuid-host-1",
Hostnames: []string{"esxi-01.lab.local"},
},
},
})
projection := ProjectMonitoredSystemRecords(rr, map[DataSource][]IngestRecord{
SourceVMware: {
{
SourceID: "vc-1:host:host-101",
Resource: Resource{
Type: ResourceTypeAgent,
Name: "esxi-01.lab.local",
Status: StatusOnline,
LastSeen: now,
VMware: &VMwareData{
ConnectionID: "vc-1",
ConnectionName: "Lab VC",
ManagedObjectID: "host-101",
EntityType: "host",
HostUUID: "uuid-host-1",
},
},
Identity: ResourceIdentity{
DMIUUID: "uuid-host-1",
Hostnames: []string{"esxi-01.lab.local"},
},
},
},
})
if projection.CurrentCount != 1 {
t.Fatalf("CurrentCount = %d, want 1", projection.CurrentCount)
}
if projection.ProjectedCount != 1 {
t.Fatalf("ProjectedCount = %d, want 1", projection.ProjectedCount)
}
if projection.AdditionalCount != 0 {
t.Fatalf("AdditionalCount = %d, want 0", projection.AdditionalCount)
}
}
func TestPreviewMonitoredSystemRecords_ReturnsCurrentAndProjectedSystemsForVMwareAdmission(t *testing.T) {
rr := NewRegistry(nil)
now := time.Date(2026, 4, 7, 12, 0, 0, 0, time.UTC)
rr.IngestRecords(SourceAgent, []IngestRecord{
{
SourceID: "agent-host-1",
Resource: Resource{
Type: ResourceTypeAgent,
Name: "esxi-01.lab.local",
Status: StatusOnline,
LastSeen: now,
},
Identity: ResourceIdentity{
DMIUUID: "uuid-host-1",
Hostnames: []string{"esxi-01.lab.local"},
},
},
})
preview := PreviewMonitoredSystemRecords(rr, map[DataSource][]IngestRecord{
SourceVMware: {
{
SourceID: "vc-1:host:host-101",
Resource: Resource{
Type: ResourceTypeAgent,
Name: "esxi-01.lab.local",
Status: StatusOnline,
LastSeen: now,
VMware: &VMwareData{
ConnectionID: "vc-1",
ConnectionName: "Lab VC",
ManagedObjectID: "host-101",
EntityType: "host",
HostUUID: "uuid-host-1",
},
},
Identity: ResourceIdentity{
DMIUUID: "uuid-host-1",
Hostnames: []string{"esxi-01.lab.local"},
},
},
{
SourceID: "vc-1:host:host-102",
Resource: Resource{
Type: ResourceTypeAgent,
Name: "esxi-02.lab.local",
Status: StatusOnline,
LastSeen: now,
VMware: &VMwareData{
ConnectionID: "vc-1",
ConnectionName: "Lab VC",
ManagedObjectID: "host-102",
EntityType: "host",
HostUUID: "uuid-host-2",
},
},
Identity: ResourceIdentity{
DMIUUID: "uuid-host-2",
Hostnames: []string{"esxi-02.lab.local"},
},
},
},
})
if preview.CurrentCount != 1 {
t.Fatalf("CurrentCount = %d, want 1", preview.CurrentCount)
}
if preview.ProjectedCount != 2 {
t.Fatalf("ProjectedCount = %d, want 2", preview.ProjectedCount)
}
if preview.AdditionalCount != 1 {
t.Fatalf("AdditionalCount = %d, want 1", preview.AdditionalCount)
}
if len(preview.CurrentSystems) != 1 {
t.Fatalf("len(CurrentSystems) = %d, want 1", len(preview.CurrentSystems))
}
if len(preview.ProjectedSystems) != 2 {
t.Fatalf("len(ProjectedSystems) = %d, want 2", len(preview.ProjectedSystems))
}
if preview.CurrentSystems[0].Source != "agent" {
t.Fatalf("CurrentSystems[0].Source = %q, want agent", preview.CurrentSystems[0].Source)
}
}
func TestPreviewMonitoredSystemRecordsReplacementOffsetsReplacedVMwareConnection(t *testing.T) {
rr := NewRegistry(nil)
now := time.Date(2026, 4, 7, 12, 0, 0, 0, time.UTC)
rr.IngestRecords(SourceAgent, []IngestRecord{
{
SourceID: "agent-host-1",
Resource: Resource{
Type: ResourceTypeAgent,
Name: "esxi-01.lab.local",
Status: StatusOnline,
LastSeen: now,
},
Identity: ResourceIdentity{
DMIUUID: "uuid-host-1",
Hostnames: []string{"esxi-01.lab.local"},
},
},
})
rr.IngestRecords(SourceVMware, []IngestRecord{
{
SourceID: "vc-1:host:host-101",
Resource: Resource{
Type: ResourceTypeAgent,
Name: "esxi-01.lab.local",
Status: StatusOnline,
LastSeen: now,
VMware: &VMwareData{
ConnectionID: "vc-1",
ConnectionName: "Lab VC",
ManagedObjectID: "host-101",
EntityType: "host",
HostUUID: "uuid-host-1",
},
},
Identity: ResourceIdentity{
DMIUUID: "uuid-host-1",
Hostnames: []string{"esxi-01.lab.local"},
},
},
{
SourceID: "vc-1:host:host-102",
Resource: Resource{
Type: ResourceTypeAgent,
Name: "esxi-02.lab.local",
Status: StatusOnline,
LastSeen: now,
VMware: &VMwareData{
ConnectionID: "vc-1",
ConnectionName: "Lab VC",
ManagedObjectID: "host-102",
EntityType: "host",
HostUUID: "uuid-host-2",
},
},
Identity: ResourceIdentity{
DMIUUID: "uuid-host-2",
Hostnames: []string{"esxi-02.lab.local"},
},
},
})
preview := PreviewMonitoredSystemRecordsReplacement(rr, MonitoredSystemReplacement{
Source: SourceVMware,
Selector: MonitoredSystemReplacementSelector{
ResourceID: "vc-1",
},
}, map[DataSource][]IngestRecord{
SourceVMware: {
{
SourceID: "vc-1:host:host-101",
Resource: Resource{
Type: ResourceTypeAgent,
Name: "esxi-01.lab.local",
Status: StatusOnline,
LastSeen: now,
VMware: &VMwareData{
ConnectionID: "vc-1",
ConnectionName: "Lab VC",
ManagedObjectID: "host-101",
EntityType: "host",
HostUUID: "uuid-host-1",
},
},
Identity: ResourceIdentity{
DMIUUID: "uuid-host-1",
Hostnames: []string{"esxi-01.lab.local"},
},
},
{
SourceID: "vc-1:host:host-103",
Resource: Resource{
Type: ResourceTypeAgent,
Name: "esxi-03.lab.local",
Status: StatusOnline,
LastSeen: now,
VMware: &VMwareData{
ConnectionID: "vc-1",
ConnectionName: "Lab VC",
ManagedObjectID: "host-103",
EntityType: "host",
HostUUID: "uuid-host-3",
},
},
Identity: ResourceIdentity{
DMIUUID: "uuid-host-3",
Hostnames: []string{"esxi-03.lab.local"},
},
},
},
})
if preview.CurrentCount != 2 {
t.Fatalf("CurrentCount = %d, want 2", preview.CurrentCount)
}
if preview.ProjectedCount != 2 {
t.Fatalf("ProjectedCount = %d, want 2", preview.ProjectedCount)
}
if preview.AdditionalCount != 0 {
t.Fatalf("AdditionalCount = %d, want 0", preview.AdditionalCount)
}
if len(preview.CurrentSystems) != 2 {
t.Fatalf("len(CurrentSystems) = %d, want 2", len(preview.CurrentSystems))
}
if len(preview.ProjectedSystems) != 2 {
t.Fatalf("len(ProjectedSystems) = %d, want 2", len(preview.ProjectedSystems))
}
if preview.CurrentSystem != nil {
t.Fatalf("CurrentSystem = %+v, want nil for multi-system replacement", preview.CurrentSystem)
}
if preview.ProjectedSystem != nil {
t.Fatalf("ProjectedSystem = %+v, want nil for multi-system replacement", preview.ProjectedSystem)
}
}
func TestPreviewMonitoredSystemRecordsReplacementScopesVMwareResourceIDToConnection(t *testing.T) {
rr := NewRegistry(nil)
now := time.Date(2026, 4, 7, 12, 0, 0, 0, time.UTC)
rr.IngestRecords(SourceVMware, []IngestRecord{
{
SourceID: "vc-edit:host:host-101",
Resource: Resource{
Type: ResourceTypeAgent,
Name: "esxi-01.lab.local",
Status: StatusOnline,
LastSeen: now,
VMware: &VMwareData{
ConnectionID: "vc-edit",
ConnectionName: "Edited VC",
ManagedObjectID: "host-101",
EntityType: "host",
HostUUID: "uuid-host-1",
},
},
Identity: ResourceIdentity{
DMIUUID: "uuid-host-1",
Hostnames: []string{"esxi-01.lab.local"},
},
},
{
SourceID: "vc-other:host:host-201",
Resource: Resource{
Type: ResourceTypeAgent,
Name: "esxi-collision.lab.local",
Status: StatusOnline,
LastSeen: now,
VMware: &VMwareData{
ConnectionID: "vc-other",
ConnectionName: "Other VC",
ManagedObjectID: "host-201",
EntityType: "host",
HostUUID: "vc-edit",
},
},
Identity: ResourceIdentity{
DMIUUID: "vc-edit",
Hostnames: []string{"esxi-collision.lab.local"},
},
},
})
preview := PreviewMonitoredSystemRecordsReplacement(rr, MonitoredSystemReplacement{
Source: SourceVMware,
Selector: MonitoredSystemReplacementSelector{
ResourceID: "vc-edit",
},
}, map[DataSource][]IngestRecord{
SourceVMware: {
{
SourceID: "vc-edit:host:host-101",
Resource: Resource{
Type: ResourceTypeAgent,
Name: "esxi-01.lab.local",
Status: StatusOnline,
LastSeen: now,
VMware: &VMwareData{
ConnectionID: "vc-edit",
ConnectionName: "Edited VC",
ManagedObjectID: "host-101",
EntityType: "host",
HostUUID: "uuid-host-1",
},
},
Identity: ResourceIdentity{
DMIUUID: "uuid-host-1",
Hostnames: []string{"esxi-01.lab.local"},
},
},
},
})
if preview.CurrentCount != 2 {
t.Fatalf("CurrentCount = %d, want 2", preview.CurrentCount)
}
if preview.ProjectedCount != 2 {
t.Fatalf("ProjectedCount = %d, want 2", preview.ProjectedCount)
}
if preview.AdditionalCount != 0 {
t.Fatalf("AdditionalCount = %d, want 0", preview.AdditionalCount)
}
if len(preview.CurrentSystems) != 1 {
t.Fatalf("len(CurrentSystems) = %d, want 1", len(preview.CurrentSystems))
}
if preview.CurrentSystems[0].Name != "esxi-01.lab.local" {
t.Fatalf("CurrentSystems[0].Name = %q, want esxi-01.lab.local", preview.CurrentSystems[0].Name)
}
if len(preview.ProjectedSystems) != 1 {
t.Fatalf("len(ProjectedSystems) = %d, want 1", len(preview.ProjectedSystems))
}
if preview.ProjectedSystems[0].Name != "esxi-01.lab.local" {
t.Fatalf("ProjectedSystems[0].Name = %q, want esxi-01.lab.local", preview.ProjectedSystems[0].Name)
}
}
func TestPreviewMonitoredSystemCandidateInactiveKeepsRegistryCountUnchanged(t *testing.T) {
rr := NewRegistry(nil)
rr.IngestRecords(SourceAgent, []IngestRecord{
{
SourceID: "host-1",
Resource: Resource{
ID: "host-1",
Type: ResourceTypeAgent,
Name: "tower.local",
Status: StatusOnline,
Agent: &AgentData{
AgentID: "agent-1",
Hostname: "tower.local",
MachineID: "machine-1",
},
Identity: ResourceIdentity{
MachineID: "machine-1",
Hostnames: []string{"tower.local"},
},
},
},
})
preview := PreviewMonitoredSystemCandidate(rr, MonitoredSystemCandidate{
Source: SourceTrueNAS,
Type: ResourceTypeAgent,
Name: "tower",
Hostname: "tower.local",
HostURL: "https://tower.local",
State: MonitoredSystemCandidateStateInactive,
})
if preview.CurrentCount != 1 || preview.ProjectedCount != 1 || preview.AdditionalCount != 0 {
t.Fatalf("unexpected inactive candidate counts: %+v", preview)
}
if len(preview.CurrentSystems) != 0 || len(preview.ProjectedSystems) != 0 {
t.Fatalf("inactive candidate should not materialize preview systems: %+v", preview)
}
}
func TestPreviewMonitoredSystemCandidateReplacementInactiveRemovesVMwareConnectionFromRegistry(t *testing.T) {
rr := NewRegistry(nil)
rr.IngestRecords(SourceVMware, []IngestRecord{
{
SourceID: "vc-1:host:host-101",
Resource: Resource{
ID: "vmware-host-101",
Type: ResourceTypeAgent,
Name: "esxi-01.lab.local",
Status: StatusOnline,
VMware: &VMwareData{
ConnectionID: "vc-1",
ConnectionName: "Lab VC",
VCenterHost: "vcsa.lab.local",
ManagedObjectID: "host-101",
EntityType: "host",
HostUUID: "host-uuid-101",
},
},
Identity: ResourceIdentity{
DMIUUID: "host-uuid-101",
Hostnames: []string{"esxi-01.lab.local"},
},
},
})
preview := PreviewMonitoredSystemCandidateReplacement(rr, MonitoredSystemReplacement{
Source: SourceVMware,
Selector: MonitoredSystemReplacementSelector{
ResourceID: "vc-1",
},
}, MonitoredSystemCandidate{
Source: SourceVMware,
Type: ResourceTypeAgent,
Name: "Lab VC",
Hostname: "vcsa.lab.local",
HostURL: "https://vcsa.lab.local",
State: MonitoredSystemCandidateStateInactive,
})
if preview.CurrentCount != 1 || preview.ProjectedCount != 0 || preview.AdditionalCount != 0 {
t.Fatalf("unexpected inactive replacement counts: %+v", preview)
}
if preview.CurrentSystem == nil {
t.Fatal("expected current monitored system preview")
}
if preview.ProjectedSystem != nil {
t.Fatalf("inactive replacement should not leave a projected system: %+v", preview.ProjectedSystem)
}
}
func TestResourceRegistry_WorkloadsIncludeCanonicalAppContainers(t *testing.T) {
rr := NewRegistry(nil)
now := time.Date(2026, 3, 29, 12, 0, 0, 0, time.UTC)
rr.IngestRecords(SourceProxmox, []IngestRecord{
{
SourceID: "vm-101",
Resource: Resource{
Type: ResourceTypeVM,
Name: "db-01",
Status: StatusOnline,
LastSeen: now,
},
},
})
rr.IngestRecords(SourceDocker, []IngestRecord{
{
SourceID: "ctr-201",
Resource: Resource{
Type: ResourceTypeAppContainer,
Name: "metrics-sidecar",
Status: StatusOnline,
LastSeen: now,
},
},
})
rr.IngestRecords(SourceTrueNAS, []IngestRecord{
{
SourceID: "app:nextcloud",
Resource: Resource{
Type: ResourceTypeAppContainer,
Name: "Nextcloud",
Status: StatusOnline,
LastSeen: now,
TrueNAS: &TrueNASData{Hostname: "truenas-main"},
Docker: &DockerData{
ContainerID: "nextcloud",
DisplayName: "Nextcloud",
Image: "library/nextcloud:latest",
Runtime: "docker",
ContainerState: "running",
},
},
},
})
workloads := rr.Workloads()
if len(workloads) != 3 {
t.Fatalf("Workloads() returned %d records, want 3", len(workloads))
}
typesByName := make(map[string]ResourceType, len(workloads))
for _, workload := range workloads {
typesByName[workload.Name()] = workload.Type()
}
if got := typesByName["db-01"]; got != ResourceTypeVM {
t.Fatalf("expected VM workload for db-01, got %q", got)
}
if got := typesByName["metrics-sidecar"]; got != ResourceTypeAppContainer {
t.Fatalf("expected app-container workload for metrics-sidecar, got %q", got)
}
if got := typesByName["Nextcloud"]; got != ResourceTypeAppContainer {
t.Fatalf("expected app-container workload for Nextcloud, got %q", got)
}
}
// TestRegistryIngestKeepsDockerContainersScopedToTheirHost pins the fix for the
// "frigate@141" host re-attribution flicker on the Docker page. The bug was
// that app-container source keys did not include the docker host, so any
// scenario producing a colliding container source ID (a docker ps that
// briefly returns an empty ID and parseDockerInventoryContainerLine falls
// back to the container name, a future short-ID truncation, daemon-side
// identifier reset across recreate cycles, ...) routed two containers
// reported by different hosts to the same registry resource. mergeInto
// then overwrote one host's Docker payload with the other's, flipping
// HostSourceID and ParentID on every projection rebuild until the colliding
// inputs diverged again.
func TestRegistryIngestKeepsDockerContainersScopedToTheirHost(t *testing.T) {
rr := NewRegistry(nil)
// Two distinct docker hosts each reporting a container whose docker
// source ID happens to collide (here: both fell back to the "frigate"
// name because their docker ps lines were missing an ID).
rr.IngestSnapshot(models.StateSnapshot{
DockerHosts: []models.DockerHost{
{
ID: "proxmox-lxc-docker:pve-a:node-a:105",
Hostname: "frigate.lab",
Status: "online",
Containers: []models.DockerContainer{{
ID: "frigate",
Name: "frigate",
State: "running",
}},
},
{
ID: "proxmox-lxc-docker:pve-a:node-a:141",
Hostname: "homepage-docker.lab",
Status: "online",
Containers: []models.DockerContainer{{
ID: "frigate",
Name: "frigate",
State: "running",
}},
},
},
})
resources := rr.ListByType(ResourceTypeAppContainer)
if len(resources) != 2 {
t.Fatalf("expected two distinct docker container resources (one per host); got %d", len(resources))
}
hostsSeen := make(map[string]struct{}, len(resources))
for _, resource := range resources {
if resource.Docker == nil {
t.Fatalf("docker container resource missing Docker payload: %#v", resource)
}
if resource.Name != "frigate" {
t.Fatalf("unexpected container name: %q", resource.Name)
}
if !reflect.DeepEqual(resource.PlatformScopes, []string{"proxmox-pve", "docker"}) {
t.Fatalf("platform scopes = %#v, want proxmox-pve + docker", resource.PlatformScopes)
}
hostsSeen[strings.TrimSpace(resource.Docker.HostSourceID)] = struct{}{}
}
for _, want := range []string{
"proxmox-lxc-docker:pve-a:node-a:105",
"proxmox-lxc-docker:pve-a:node-a:141",
} {
if _, ok := hostsSeen[want]; !ok {
t.Fatalf("expected a container attributed to host %q; got hosts %v", want, hostsSeen)
}
}
}
func TestRegistryIngestSnapshotPublishesDockerNativeInventory(t *testing.T) {
now := time.Date(2026, 5, 24, 8, 0, 0, 0, time.UTC)
rr := NewRegistry(NewMemoryStore())
rr.IngestSnapshot(models.StateSnapshot{
DockerHosts: []models.DockerHost{{
ID: "docker-host-1",
Hostname: "edge",
Runtime: "docker",
Status: "online",
LastSeen: now,
Images: []models.DockerImage{{
ID: "sha256:image1", RepoTags: []string{"repo/app:latest"}, SizeBytes: 1024,
}},
Volumes: []models.DockerVolume{{Name: "app-data", Driver: "local", SizeBytes: 2048}},
Networks: []models.DockerNetwork{{
ID: "net1", Name: "app-net", Driver: "bridge", Subnets: []models.DockerNetworkSubnet{{Subnet: "10.88.0.0/24", Gateway: "10.88.0.1"}},
}},
Tasks: []models.DockerTask{{
ID: "task-1", ServiceID: "svc-1", ServiceName: "api", Slot: 1, DesiredState: "running", CurrentState: "running",
}},
Secrets: []models.DockerSecret{{ID: "secret-1", Name: "db-password", DriverName: "builtin"}},
Configs: []models.DockerConfig{{ID: "config-1", Name: "nginx-conf", TemplatingDriver: "golang"}},
Swarm: &models.DockerSwarmInfo{
NodeID: "node-1",
NodeRole: "manager",
LocalState: "active",
},
Nodes: []models.DockerNode{{
ID: "node-1", Hostname: "manager-1", Role: "manager", State: "ready",
}},
}},
})
counts := map[ResourceType]int{}
for _, resource := range rr.List() {
counts[resource.Type]++
}
for _, resourceType := range []ResourceType{
ResourceTypeDockerImage,
ResourceTypeDockerVolume,
ResourceTypeDockerNetwork,
ResourceTypeDockerTask,
ResourceTypeDockerSwarmNode,
ResourceTypeDockerSecret,
ResourceTypeDockerConfig,
} {
if counts[resourceType] != 1 {
t.Fatalf("expected one %s resource, got counts %#v", resourceType, counts)
}
}
}
func TestRegistryIngestSnapshotLinksDockerContainersToNetworks(t *testing.T) {
now := time.Date(2026, 6, 3, 9, 0, 0, 0, time.UTC)
rr := NewRegistry(NewMemoryStore())
rr.IngestSnapshot(models.StateSnapshot{
DockerHosts: []models.DockerHost{{
ID: "docker-host-1",
Hostname: "edge",
Runtime: "docker",
Status: "online",
LastSeen: now,
Containers: []models.DockerContainer{{
ID: "container-1",
Name: "api",
Image: "repo/api:latest",
State: "running",
Networks: []models.DockerContainerNetworkLink{{
Name: "frontend",
IPv4: "10.88.0.12",
}},
}},
Networks: []models.DockerNetwork{{
ID: "network-1",
Name: "frontend",
Driver: "bridge",
}},
}},
})
containers := rr.ListByType(ResourceTypeAppContainer)
networks := rr.ListByType(ResourceTypeDockerNetwork)
if len(containers) != 1 || len(networks) != 1 {
t.Fatalf("expected one container and one network, got containers=%d networks=%d", len(containers), len(networks))
}
container := containers[0]
network := networks[0]
assertDockerNetworkAttachment := func(resource Resource) {
t.Helper()
for _, relationship := range resource.Relationships {
if relationship.Type != RelAttachedTo {
continue
}
if relationship.SourceID != container.ID || relationship.TargetID != network.ID {
t.Fatalf("unexpected attachment relationship endpoints on %s: %+v", resource.ID, relationship)
}
if relationship.Discoverer != dockerAdapterRelationshipDiscoverer {
t.Fatalf("relationship discoverer = %q, want %q", relationship.Discoverer, dockerAdapterRelationshipDiscoverer)
}
if relationship.Metadata["networkName"] != "frontend" || relationship.Metadata["ipv4"] != "10.88.0.12" {
t.Fatalf("relationship metadata = %#v", relationship.Metadata)
}
return
}
t.Fatalf("expected docker network attachment relationship on %s, got %+v", resource.ID, resource.Relationships)
}
assertDockerNetworkAttachment(container)
assertDockerNetworkAttachment(network)
}
// A live source must override a metric held by a stale higher-priority source.
func TestMergeMetric_LiveSourceOverridesStaleHigherPriority(t *testing.T) {
now := time.Now().UTC()
status := map[DataSource]SourceStatus{
SourceAgent: {Status: "stale", LastSeen: now.Add(-2 * time.Hour)},
SourceProxmox: {Status: "online", LastSeen: now},
}
existing := &MetricValue{Value: 0, Percent: 0, Source: SourceAgent}
incoming := &MetricValue{Value: 6.2, Percent: 6.2, Source: SourceProxmox}
got := mergeMetric(existing, incoming, SourceProxmox, now, status)
if got == nil || got.Percent != 6.2 || got.Source != SourceProxmox {
t.Fatalf("expected live proxmox CPU to override stale agent CPU, got %+v", got)
}
}
// When both sources are fresh, static priority still decides: the agent wins.
func TestMergeMetric_FreshHigherPriorityStillWins(t *testing.T) {
now := time.Now().UTC()
status := map[DataSource]SourceStatus{
SourceAgent: {Status: "online", LastSeen: now},
SourceProxmox: {Status: "online", LastSeen: now},
}
existing := &MetricValue{Percent: 6.2, Source: SourceProxmox}
incoming := &MetricValue{Percent: 12.5, Source: SourceAgent}
got := mergeMetric(existing, incoming, SourceAgent, now, status)
if got == nil || got.Percent != 12.5 || got.Source != SourceAgent {
t.Fatalf("expected fresh agent CPU to win by priority, got %+v", got)
}
}
// A stale source must not clobber a metric currently held by a live source.
func TestMergeMetric_StaleSourceDoesNotClobberLive(t *testing.T) {
now := time.Now().UTC()
status := map[DataSource]SourceStatus{
SourceAgent: {Status: "stale", LastSeen: now.Add(-2 * time.Hour)},
SourceProxmox: {Status: "online", LastSeen: now},
}
existing := &MetricValue{Percent: 6.2, Source: SourceProxmox}
incoming := &MetricValue{Percent: 0, Source: SourceAgent}
got := mergeMetric(existing, incoming, SourceAgent, now, status)
if got == nil || got.Percent != 6.2 || got.Source != SourceProxmox {
t.Fatalf("expected live proxmox CPU to survive stale agent merge, got %+v", got)
}
}
// The merged-source host shape from the homelab canonical-ID bug: a PVE node
// record that only knows cluster+hostname, and a pulse-agent record that
// knows the machine ID. Whichever record mints the canonical resource decides
// which identity key gets hashed, so without durable identity pins the
// canonical ID flips between boot windows (node-only) and steady state
// (agent present), fragmenting the change journal into per-boot eras.
func mergedHostNodeResource() Resource {
return Resource{
Type: ResourceTypeAgent,
Name: "delly",
Status: StatusOnline,
Proxmox: &ProxmoxData{SourceID: "homelab-delly", NodeName: "delly", ClusterName: "homelab"},
}
}
func mergedHostAgentResource(machineID string) Resource {
return Resource{
Type: ResourceTypeAgent,
Name: "delly",
Status: StatusOnline,
Agent: &AgentData{AgentID: machineID, Hostname: "delly", MachineID: machineID},
}
}
func TestMergedHostCanonicalIDStableAcrossRestartsAndIngestOrders(t *testing.T) {
store := NewMemoryStore()
const machineID = "7d465a78-test-machine-id"
nodeIdentity := ResourceIdentity{Hostnames: []string{"delly"}, ClusterName: "homelab"}
agentIdentity := ResourceIdentity{MachineID: machineID, Hostnames: []string{"delly"}}
// IngestSnapshot merges the linked host's identity into the node record
// when the agent is present in the snapshot.
steadyIdentity := mergeIdentity(nodeIdentity, agentIdentity)
steadyID := buildHashID(ResourceTypeAgent, "machine:"+machineID)
bootEraID := buildHashID(ResourceTypeAgent, "cluster:homelab:delly")
if steadyID == bootEraID {
t.Fatalf("test setup broken: era IDs must differ")
}
// Steady-state boot: node record carries the merged identity and mints
// the machine-keyed canonical ID; the agent record merges into it.
steady := NewRegistry(store)
if id := steady.ingest(SourceProxmox, "homelab-delly", mergedHostNodeResource(), steadyIdentity); id != steadyID {
t.Fatalf("steady-state node ingest minted %q, want machine-keyed %q", id, steadyID)
}
if id := steady.ingest(SourceAgent, machineID, mergedHostAgentResource(machineID), agentIdentity); id != steadyID {
t.Fatalf("steady-state agent ingest resolved %q, want %q", id, steadyID)
}
steady.PersistIdentityPins()
// Restart into a boot window: the agent has not checked in yet, so the
// node record only knows cluster+hostname. Before identity pins this
// minted bootEraID and the change journal fragmented into a second era.
bootWindow := NewRegistry(store)
if id := bootWindow.ingest(SourceProxmox, "homelab-delly", mergedHostNodeResource(), nodeIdentity); id != steadyID {
t.Fatalf("boot-window node ingest minted %q, want pinned %q", id, steadyID)
}
// Restart with the opposite ingest order: agent record first into an
// empty registry, node record after.
reversed := NewRegistry(store)
if id := reversed.ingest(SourceAgent, machineID, mergedHostAgentResource(machineID), agentIdentity); id != steadyID {
t.Fatalf("agent-first ingest minted %q, want %q", id, steadyID)
}
if id := reversed.ingest(SourceProxmox, "homelab-delly", mergedHostNodeResource(), nodeIdentity); id != steadyID {
t.Fatalf("node-after-agent ingest resolved %q, want %q", id, steadyID)
}
if got := len(reversed.List()); got != 1 {
t.Fatalf("expected one merged resource after reversed-order ingest, got %d", got)
}
}
// A synthesized placeholder (e.g. an offline PVE node for an instance that
// has never completed a poll) carries a zero LastSeen. Ingest must preserve
// that zero instead of fabricating a fresh sighting, and the per-source
// delivery status must be "unknown", not "online" — an online stamp with a
// zero LastSeen is permanently exempt from stale-marking and masks the
// source's real delivery state (the loop behind commit 8372a22c5).
func TestIngestPreservesNeverSeenSightingAndStampsUnknownSourceStatus(t *testing.T) {
rr := NewRegistry(nil)
placeholder := topLevelTestProxmoxNode("proxmox-node", "tower", "proxmox-1", "https://tower.local:8006")
placeholder.Status = StatusOffline
placeholder.LastSeen = time.Time{}
rr.IngestRecords(SourceProxmox, []IngestRecord{
{SourceID: "proxmox-node", Resource: placeholder},
})
resources := rr.List()
if len(resources) != 1 {
t.Fatalf("List() returned %d resources, want 1", len(resources))
}
resource := resources[0]
if !resource.LastSeen.IsZero() {
t.Fatalf("resource.LastSeen = %s, want zero: never-seen resources must not get a fabricated sighting", resource.LastSeen)
}
status, ok := resource.SourceStatus[SourceProxmox]
if !ok {
t.Fatal("expected a proxmox source status entry")
}
if status.Status != "unknown" {
t.Fatalf("SourceStatus[proxmox].Status = %q, want %q: a source that never delivered the resource must not be stamped online", status.Status, "unknown")
}
if !status.LastSeen.IsZero() {
t.Fatalf("SourceStatus[proxmox].LastSeen = %s, want zero", status.LastSeen)
}
// Stale marking must leave a never-seen source alone in both directions.
rr.MarkStale(time.Now().UTC(), nil)
if got := rr.List()[0].SourceStatus[SourceProxmox].Status; got != "unknown" {
t.Fatalf("SourceStatus[proxmox].Status after MarkStale = %q, want %q", got, "unknown")
}
}
// A real sighting keeps the online stamp and the genuine timestamp on both
// the resource and the per-source delivery status.
func TestIngestStampsOnlineSourceStatusForRealSighting(t *testing.T) {
rr := NewRegistry(nil)
seen := time.Now().UTC().Add(-5 * time.Second).Truncate(time.Millisecond)
node := topLevelTestProxmoxNode("proxmox-node", "tower", "proxmox-1", "https://tower.local:8006")
node.LastSeen = seen
rr.IngestRecords(SourceProxmox, []IngestRecord{
{SourceID: "proxmox-node", Resource: node},
})
resource := rr.List()[0]
if !resource.LastSeen.Equal(seen) {
t.Fatalf("resource.LastSeen = %s, want %s", resource.LastSeen, seen)
}
status := resource.SourceStatus[SourceProxmox]
if status.Status != "online" {
t.Fatalf("SourceStatus[proxmox].Status = %q, want online", status.Status)
}
if !status.LastSeen.Equal(seen) {
t.Fatalf("SourceStatus[proxmox].LastSeen = %s, want %s", status.LastSeen, seen)
}
}
// Datastore-derived storage entries written by the PBS poller (instance
// "pbs-<name>", type "pbs") are delivered on the PBS cadence, so they must be
// keyed to SourcePBS — judging them against the faster Proxmox stale
// threshold would flap healthy datastores stale between PBS polls. PVE also
// reports pbs-typed storage.cfg backends, and those stay SourceProxmox: they
// really are delivered by the PVE storage poll.
func TestIngestStorageRoutesPBSPollerEntriesToPBSSource(t *testing.T) {
seen := time.Now().UTC().Add(-30 * time.Second).Truncate(time.Millisecond)
snapshot := models.StateSnapshot{
PBSInstances: []models.PBSInstance{{
ID: "pbs-backup",
Name: "backup",
Host: "https://pbs.local:8007",
Status: "online",
LastSeen: seen,
}},
Storage: []models.Storage{
{
ID: "pbs-backup-main",
Name: "main",
Node: "backup",
Instance: "pbs-backup",
Type: "pbs",
Status: "available",
LastSeen: seen,
},
{
ID: "home-pve1-pbs-remote",
Name: "pbs-remote",
Node: "pve1",
Instance: "home",
Type: "pbs",
Status: "available",
LastSeen: seen,
},
},
}
rr := NewRegistry(nil)
rr.IngestSnapshot(snapshot)
var pbsInstanceID string
var pollerStorage, pveStorage *Resource
for _, resource := range rr.List() {
resource := resource
switch {
case resource.Type == ResourceTypePBS:
pbsInstanceID = resource.ID
case resource.Type == ResourceTypeStorage && resource.Proxmox != nil && resource.Proxmox.SourceID == "pbs-backup-main":
pollerStorage = &resource
case resource.Type == ResourceTypeStorage && resource.Proxmox != nil && resource.Proxmox.SourceID == "home-pve1-pbs-remote":
pveStorage = &resource
}
}
if pbsInstanceID == "" {
t.Fatal("expected a PBS instance resource")
}
if pollerStorage == nil || pveStorage == nil {
t.Fatalf("expected both storage resources (pollerStorage=%v, pveStorage=%v)", pollerStorage != nil, pveStorage != nil)
}
if len(pollerStorage.Sources) != 1 || pollerStorage.Sources[0] != SourcePBS {
t.Fatalf("PBS poller storage Sources = %v, want [%s]", pollerStorage.Sources, SourcePBS)
}
status, ok := pollerStorage.SourceStatus[SourcePBS]
if !ok {
t.Fatal("expected a pbs source status entry on the PBS poller storage")
}
if !status.LastSeen.Equal(seen) {
t.Fatalf("SourceStatus[pbs].LastSeen = %s, want %s", status.LastSeen, seen)
}
if pollerStorage.ParentID == nil || *pollerStorage.ParentID != pbsInstanceID {
t.Fatalf("PBS poller storage ParentID = %v, want the PBS instance %q", pollerStorage.ParentID, pbsInstanceID)
}
if len(pveStorage.Sources) != 1 || pveStorage.Sources[0] != SourceProxmox {
t.Fatalf("PVE-reported pbs-typed storage Sources = %v, want [%s]", pveStorage.Sources, SourceProxmox)
}
}
func TestRegistryAvailabilityLinkAttachesFacetToKnownResource(t *testing.T) {
rr := NewRegistry(nil)
now := time.Now().UTC()
rr.IngestRecords(SourceAgent, []IngestRecord{{
SourceID: "host-1",
Resource: Resource{Type: ResourceTypeAgent, Name: "host-1", Status: StatusOnline, LastSeen: now},
Identity: ResourceIdentity{MachineID: "machine-1"},
}})
hostID := rr.ListByType(ResourceTypeAgent)[0].ID
rr.IngestRecords(SourceAvailability, []IngestRecord{{
SourceID: "probe-1",
Resource: Resource{
Type: ResourceTypeNetworkEndpoint, Name: "probe-1", Status: StatusOnline, LastSeen: now,
Sources: []DataSource{SourceAvailability},
Availability: &AvailabilityData{
TargetID: "probe-1", LinkedResourceID: hostID, Address: "192.0.2.10",
Protocol: "icmp", Enabled: true, Available: true,
},
},
Identity: ResourceIdentity{IPAddresses: []string{"192.0.2.10"}},
}})
if got := rr.ListByType(ResourceTypeNetworkEndpoint); len(got) != 0 {
t.Fatalf("expected 0 standalone network endpoints, got %d", len(got))
}
host, ok := rr.Get(hostID)
if !ok || host.Availability == nil || host.Availability.TargetID != "probe-1" {
t.Fatalf("expected availability facet probe-1 on host, got %+v", host.Availability)
}
}
func TestChooseStatusAvailabilityDoesNotOverrideHigherPriority(t *testing.T) {
rr := NewRegistry(nil)
now := time.Now().UTC()
rr.IngestRecords(SourceProxmox, []IngestRecord{{
SourceID: "delly:minipc:135",
Resource: Resource{
Type: ResourceTypeSystemContainer, Name: "grafana",
Status: StatusOnline, LastSeen: now,
},
Identity: ResourceIdentity{IPAddresses: []string{"192.0.2.130"}},
}})
container := rr.ListByType(ResourceTypeSystemContainer)[0]
rr.IngestRecords(SourceAvailability, []IngestRecord{{
SourceID: "probe-grafana",
Resource: Resource{
Type: ResourceTypeNetworkEndpoint, Name: "probe-grafana",
Status: StatusOffline, LastSeen: now,
Sources: []DataSource{SourceAvailability},
Availability: &AvailabilityData{
TargetID: "probe-grafana", LinkedResourceID: container.ID,
Address: "192.0.2.130", Protocol: "http", Port: 3000,
Enabled: true, Available: false, ConsecutiveFailures: 3,
FailureThreshold: 2,
},
},
Identity: ResourceIdentity{IPAddresses: []string{"192.0.2.130"}},
}})
got, ok := rr.Get(container.ID)
if !ok {
t.Fatal("resource not found after availability ingest")
}
if got.Status != StatusOnline {
t.Fatalf("availability source (offline) should not override proxmox (online); got %s", got.Status)
}
if got.Availability == nil || got.Availability.Available != false {
t.Fatalf("availability facet should record unavailable, got %+v", got.Availability)
}
}
func TestMarkStaleRecomputesFromRemainingFreshSources(t *testing.T) {
rr := NewRegistry(nil)
oldNow := time.Now().UTC().Add(-5 * time.Minute)
recentNow := time.Now().UTC()
rr.IngestRecords(SourceProxmox, []IngestRecord{{
SourceID: "delly:delly",
Resource: Resource{
Type: ResourceTypeAgent, Name: "delly",
Status: StatusOnline, LastSeen: oldNow,
},
Identity: ResourceIdentity{MachineID: "delly"},
}})
host := rr.ListByType(ResourceTypeAgent)[0]
rr.IngestRecords(SourceAvailability, []IngestRecord{{
SourceID: "probe-delly",
Resource: Resource{
Type: ResourceTypeNetworkEndpoint, Name: "probe-delly",
Status: StatusOnline, LastSeen: recentNow,
Sources: []DataSource{SourceAvailability},
Availability: &AvailabilityData{
TargetID: "probe-delly", LinkedResourceID: host.ID,
Address: "192.0.2.5", Protocol: "tcp", Port: 8006,
Enabled: true, Available: true,
},
},
Identity: ResourceIdentity{IPAddresses: []string{"192.0.2.5"}},
}})
check, ok := rr.Get(host.ID)
if !ok || check.Status != StatusOnline {
t.Fatalf("expected online before stale, got %s", check.Status)
}
rr.MarkStale(recentNow, nil)
got, ok := rr.Get(host.ID)
if !ok {
t.Fatal("resource not found after MarkStale")
}
proxmoxStatus := got.SourceStatus[SourceProxmox]
if proxmoxStatus.Status != "stale" {
t.Fatalf("expected proxmox source to be stale, got %s", proxmoxStatus.Status)
}
availStatus := got.SourceStatus[SourceAvailability]
if availStatus.Status != "online" {
t.Fatalf("expected availability source to still be fresh/online, got %s", availStatus.Status)
}
if got.Status != StatusOnline {
t.Fatalf("status should be recomputed from remaining fresh availability source; got %s, want online", got.Status)
}
}
func TestResourceRegistryUsesConfiguredProxmoxStaleThresholds(t *testing.T) {
seen := time.Now().UTC().Add(-90 * time.Second).Truncate(time.Millisecond)
snapshot := models.StateSnapshot{
VMs: []models.VM{{
ID: "cluster-a:pve-a:101",
Name: "db",
Node: "pve-a",
Instance: "cluster-a",
VMID: 101,
Status: "running",
Type: "qemu",
LastSeen: seen,
}},
}
defaultRegistry := NewRegistry(nil)
defaultRegistry.IngestSnapshot(snapshot)
defaultVMs := defaultRegistry.ListByType(ResourceTypeVM)
if len(defaultVMs) != 1 {
t.Fatalf("default VM count = %d, want 1", len(defaultVMs))
}
if defaultVMs[0].Status != StatusWarning {
t.Fatalf("default VM status = %q, want warning", defaultVMs[0].Status)
}
thresholds := map[DataSource]time.Duration{
SourceProxmox: 10 * time.Minute,
}
configuredRegistry := NewRegistry(nil)
configuredRegistry.IngestSnapshotWithStaleThresholds(snapshot, thresholds)
configuredVMs := configuredRegistry.ListByType(ResourceTypeVM)
if len(configuredVMs) != 1 {
t.Fatalf("configured VM count = %d, want 1", len(configuredVMs))
}
if configuredVMs[0].Status != StatusOnline {
t.Fatalf("configured VM status = %q, want online", configuredVMs[0].Status)
}
clonedRegistry := NewRegistry(nil)
clonedRegistry.IngestResourcesWithStaleThresholds(configuredRegistry.List(), thresholds)
clonedVMs := clonedRegistry.ListByType(ResourceTypeVM)
if len(clonedVMs) != 1 {
t.Fatalf("cloned VM count = %d, want 1", len(clonedVMs))
}
if clonedVMs[0].Status != StatusOnline {
t.Fatalf("cloned VM status = %q, want online", clonedVMs[0].Status)
}
if status := clonedVMs[0].SourceStatus[SourceProxmox]; status.Status != "online" {
t.Fatalf("cloned SourceStatus[proxmox] = %+v, want online", status)
}
}