Files
pulse/internal/unifiedresources/registry_test.go
T
rcourtman 5d4f51c027 Key TrueNAS systems by configured connection, not reported hostname
Two TrueNAS systems that report the same hostname collapsed into one
flapping resource (#1573, #1575): systemSourceID keyed the system by the
snapshot-reported hostname, every child pool/dataset/app/VM/share/disk
was scoped under it, and the client minted the system's machine key from
the DMI serial with a hostname fallback, so serial-less systems sharing
a hostname (and DR clones sharing a serial) also fully merged in the
identity matcher.

The system source ID now scopes to the connection ID the poller passes
through NewLiveProviderForConnection; the hostname arm survives only for
fixture snapshots, which carry no connection. The ingest identity drops
the machine key entirely (DR clones share DMI serials, and vendor
placeholder serials collide across unrelated machines), the client no
longer falls back to the hostname for MachineID, and ingest skips
identity-pin completion for SourceTrueNAS so a stale pre-fix pin or a
same-named agent host's pin cannot lend the system a machine key and
re-merge what connection scoping keeps apart. Agent.AgentID and the
native metric history keys follow the source ID minus its system:
prefix, so BuildMetricsTarget keeps resolving one series.

Rows minted under the retired hostname-keyed derivation re-key once via
record-declared succession: records name their old canonical IDs in
IngestRecord.SupersededCanonicalIDs and IngestRecords applies the
existing ApplyCanonicalIDSuccessions semantics (operator state and
action audits re-key, the superseded pin drops, never while the old ID
still belongs to a live resource, journal rows are never rewritten).
Alert identities and persisted metric series under old child IDs are
not re-keyed: active alerts re-arm under the new IDs and TrueNAS host
charts are backed by native read-through history.
2026-07-14 11:51:55 +01:00

5177 lines
161 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)
}
}
// The end-to-end #1559 scenario: multiple standalone agents with dotted
// hostnames (cloud.rnd-lax1, cloud.gce-or1, cloud.dmi-lax1) must mint
// distinct canonical resources, persist full dotted hostnames in their
// identity pins instead of collapsing them all to "cloud", and boot-window
// records that only know the hostname must resolve to the right machine
// instead of the first pinned one.
func TestStandaloneDottedHostnameAgentsStayDistinct(t *testing.T) {
store := NewMemoryStore()
hosts := []struct {
hostname string
machineID string
}{
{"cloud.rnd-lax1", "machine-rnd"},
{"cloud.gce-or1", "machine-gce"},
{"cloud.dmi-lax1", "machine-dmi"},
}
agentResource := func(hostname, machineID string) Resource {
return Resource{
Type: ResourceTypeAgent,
Name: hostname,
Status: StatusOnline,
Agent: &AgentData{AgentID: machineID, Hostname: hostname, MachineID: machineID},
}
}
steady := NewRegistry(store)
idByHostname := make(map[string]string, len(hosts))
for _, host := range hosts {
identity := ResourceIdentity{MachineID: host.machineID, Hostnames: []string{host.hostname}}
id := steady.ingest(SourceAgent, host.machineID, agentResource(host.hostname, host.machineID), identity)
if id == "" {
t.Fatalf("agent ingest for %q returned no ID", host.hostname)
}
for hostname, existingID := range idByHostname {
if existingID == id {
t.Fatalf("hosts %q and %q collapsed to canonical ID %q", hostname, host.hostname, id)
}
}
idByHostname[host.hostname] = id
}
if got := len(steady.List()); got != len(hosts) {
t.Fatalf("expected %d distinct resources, got %d", len(hosts), got)
}
steady.PersistIdentityPins()
pins, err := store.ListResourceIdentityPins()
if err != nil {
t.Fatalf("ListResourceIdentityPins: %v", err)
}
pinnedHostnames := make(map[string]struct{}, len(pins))
for _, pin := range pins {
pinnedHostnames[pin.Hostname] = struct{}{}
}
for _, host := range hosts {
if _, ok := pinnedHostnames[host.hostname]; !ok {
t.Fatalf("expected pinned primary hostname %q, got %v", host.hostname, pinnedHostnames)
}
}
// Boot window: runtime records that only know the hostname (no machine
// ID yet) arrive before the agents. Each must complete from its own pin
// and land on its own canonical ID.
boot := NewRegistry(store)
for _, host := range hosts {
record := Resource{
Type: ResourceTypeAgent,
Name: host.hostname,
Status: StatusOnline,
Docker: &DockerData{HostSourceID: "docker:" + host.hostname, Hostname: host.hostname},
}
identity := ResourceIdentity{Hostnames: []string{host.hostname}}
id := boot.ingest(SourceDocker, "docker:"+host.hostname, record, identity)
if want := idByHostname[host.hostname]; id != want {
t.Fatalf("boot-window record for %q resolved %q, want its own canonical ID %q", host.hostname, id, want)
}
}
for _, host := range hosts {
identity := ResourceIdentity{MachineID: host.machineID, Hostnames: []string{host.hostname}}
id := boot.ingest(SourceAgent, host.machineID, agentResource(host.hostname, host.machineID), identity)
if want := idByHostname[host.hostname]; id != want {
t.Fatalf("agent ingest for %q resolved %q, want %q", host.hostname, id, want)
}
}
if got := len(boot.List()); got != len(hosts) {
t.Fatalf("expected %d merged resources after boot-window ingest, got %d", len(hosts), 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)
}
}
// Two Docker Swarm members with FQDN hostnames and no machine keys
// (containerized agents without /etc/machine-id) must mint distinct
// cluster-scoped canonical IDs. The historical derivation hashed the short
// hostname, so cloud.a and cloud.b both derived cluster:<swarm>:cloud and
// fully merged in the registry (sibling of #1559).
func TestSwarmFQDNHostsWithoutMachineIDsStayDistinct(t *testing.T) {
store := NewMemoryStore()
registry := NewRegistry(store)
swarmHostIdentity := func(hostname string) ResourceIdentity {
return ResourceIdentity{
ClusterName: "prod-swarm",
Hostnames: []string{hostname, "prod-swarm:" + hostname},
}
}
swarmHostResource := func(hostname string) Resource {
return Resource{
Type: ResourceTypeAgent,
Name: hostname,
Status: StatusOnline,
Docker: &DockerData{HostSourceID: "docker:" + hostname, Hostname: hostname},
}
}
idA := registry.ingest(SourceDocker, "docker:cloud.a", swarmHostResource("cloud.a"), swarmHostIdentity("cloud.a"))
idB := registry.ingest(SourceDocker, "docker:cloud.b", swarmHostResource("cloud.b"), swarmHostIdentity("cloud.b"))
if idA == "" || idB == "" {
t.Fatalf("swarm host ingest returned empty IDs: %q, %q", idA, idB)
}
if idA == idB {
t.Fatalf("swarm members cloud.a and cloud.b collapsed to canonical ID %q", idA)
}
if got := len(registry.List()); got != 2 {
t.Fatalf("expected 2 distinct swarm host resources, got %d", got)
}
// Journal rows recorded under the collapsed short-hostname era must stay
// part of the timeline read through the new full-hostname ID.
shortEraID := buildHashID(ResourceTypeAgent, "cluster:prod-swarm:cloud")
if shortEraID == idA || shortEraID == idB {
t.Fatalf("full-hostname derivation still matches the short era ID %q", shortEraID)
}
if err := store.RecordChange(ResourceChange{
ID: "change-short-era",
ResourceID: shortEraID,
ObservedAt: time.Now().Add(-time.Hour),
Kind: ChangeStateTransition,
}); err != nil {
t.Fatalf("RecordChange: %v", err)
}
registry.PersistIdentityPins()
changes, err := store.GetRecentChanges(idA, time.Time{}, 10)
if err != nil {
t.Fatalf("GetRecentChanges: %v", err)
}
found := false
for _, change := range changes {
if change.ID == "change-short-era" {
found = true
}
}
if !found {
t.Fatalf("short-era journal row missing from timeline of %q; got %d changes", idA, len(changes))
}
}
// A machine-keyless swarm host whose canonical ID was minted under the
// collapsed short-hostname derivation must carry its operator-owned rows to
// the full-hostname ID: the superseded pin row goes, operator state
// (never-auto-remediate) is re-keyed, and the new pin preserves the full
// dotted hostname.
func TestCanonicalIDSuccessionMovesOperatorState(t *testing.T) {
store := NewMemoryStore()
shortEraID := buildHashID(ResourceTypeAgent, "cluster:prod-swarm:cloud")
if err := store.UpsertResourceIdentityPins([]ResourceIdentityPin{{
CanonicalID: shortEraID,
ResourceType: ResourceTypeAgent,
ClusterName: "prod-swarm",
Hostname: "cloud",
}}); err != nil {
t.Fatalf("seed identity pin: %v", err)
}
if err := store.SetResourceOperatorState(ResourceOperatorState{
CanonicalID: shortEraID,
NeverAutoRemediate: true,
}); err != nil {
t.Fatalf("seed operator state: %v", err)
}
registry := NewRegistry(store)
identity := ResourceIdentity{
ClusterName: "prod-swarm",
Hostnames: []string{"cloud.a", "prod-swarm:cloud.a"},
}
resource := Resource{
Type: ResourceTypeAgent,
Name: "cloud.a",
Status: StatusOnline,
Docker: &DockerData{HostSourceID: "docker:cloud.a", Hostname: "cloud.a"},
}
newID := registry.ingest(SourceDocker, "docker:cloud.a", resource, identity)
if newID == "" || newID == shortEraID {
t.Fatalf("expected a fresh full-hostname canonical ID, got %q (short era %q)", newID, shortEraID)
}
registry.PersistIdentityPins()
if _, found, err := store.GetResourceOperatorState(shortEraID); err != nil || found {
t.Fatalf("operator state still keyed by superseded ID (found=%v, err=%v)", found, err)
}
state, found, err := store.GetResourceOperatorState(newID)
if err != nil || !found {
t.Fatalf("operator state missing under successor ID (found=%v, err=%v)", found, err)
}
if !state.NeverAutoRemediate {
t.Fatalf("never-auto-remediate flag lost across canonical ID succession")
}
pins, err := store.ListResourceIdentityPins()
if err != nil {
t.Fatalf("ListResourceIdentityPins: %v", err)
}
byID := make(map[string]ResourceIdentityPin, len(pins))
for _, pin := range pins {
byID[pin.CanonicalID] = pin
}
if _, stale := byID[shortEraID]; stale {
t.Fatalf("superseded short-era pin row still present")
}
successor, ok := byID[newID]
if !ok {
t.Fatalf("successor pin missing; pins: %+v", pins)
}
if successor.Hostname != "cloud.a" {
t.Fatalf("successor pin hostname = %q, want full dotted cloud.a", successor.Hostname)
}
}
// A genuinely short-named host that is alive in the registry owns its
// canonical ID; an FQDN sibling sharing the short name must not steal its
// operator rows through succession.
func TestCanonicalIDSuccessionSkipsLiveShortNamedHost(t *testing.T) {
store := NewMemoryStore()
registry := NewRegistry(store)
ingestSwarmHost := func(hostname string) string {
return registry.ingest(SourceDocker, "docker:"+hostname, Resource{
Type: ResourceTypeAgent,
Name: hostname,
Status: StatusOnline,
Docker: &DockerData{HostSourceID: "docker:" + hostname, Hostname: hostname},
}, ResourceIdentity{
ClusterName: "prod-swarm",
Hostnames: []string{hostname, "prod-swarm:" + hostname},
})
}
shortID := ingestSwarmHost("cloud")
registry.PersistIdentityPins()
if err := store.SetResourceOperatorState(ResourceOperatorState{
CanonicalID: shortID,
NeverAutoRemediate: true,
}); err != nil {
t.Fatalf("seed operator state: %v", err)
}
fqdnID := ingestSwarmHost("cloud.a")
if fqdnID == shortID {
t.Fatalf("cloud and cloud.a collapsed to canonical ID %q", fqdnID)
}
registry.PersistIdentityPins()
state, found, err := store.GetResourceOperatorState(shortID)
if err != nil || !found || !state.NeverAutoRemediate {
t.Fatalf("live short-named host lost its operator state (found=%v, err=%v)", found, err)
}
if _, stolen, _ := store.GetResourceOperatorState(fqdnID); stolen {
t.Fatalf("FQDN sibling stole the live short-named host's operator state")
}
}
// Record-declared successions (IngestRecord.SupersededCanonicalIDs) must
// re-key operator rows to the record's minted canonical ID, but never while
// the superseded ID still belongs to a live resource in the registry: a
// distinct sibling's rows must not be stolen.
func TestIngestRecordsRecordDeclaredSuccessions(t *testing.T) {
store := NewMemoryStore()
oldID := SourceSpecificID(ResourceTypeStorage, SourceTrueNAS, "system:legacy-host/pool:tank")
if err := store.SetResourceOperatorState(ResourceOperatorState{
CanonicalID: oldID,
NeverAutoRemediate: true,
}); err != nil {
t.Fatalf("seed operator state: %v", err)
}
registry := NewRegistry(store)
registry.IngestRecords(SourceTrueNAS, []IngestRecord{{
SourceID: "system:conn-a/pool:tank",
Resource: Resource{
Type: ResourceTypeStorage,
Name: "tank",
LastSeen: time.Now().UTC(),
},
SupersededCanonicalIDs: []string{oldID},
}})
newID := SourceSpecificID(ResourceTypeStorage, SourceTrueNAS, "system:conn-a/pool:tank")
if _, found, err := store.GetResourceOperatorState(oldID); err != nil || found {
t.Fatalf("expected operator state to leave superseded ID (found=%v err=%v)", found, err)
}
state, found, err := store.GetResourceOperatorState(newID)
if err != nil || !found || !state.NeverAutoRemediate {
t.Fatalf("expected operator state re-keyed to %s (found=%v err=%v state=%+v)", newID, found, err, state)
}
}
func TestIngestRecordsSkipRecordDeclaredSuccessionForLiveOldID(t *testing.T) {
store := NewMemoryStore()
oldID := SourceSpecificID(ResourceTypeStorage, SourceTrueNAS, "system:legacy-host/pool:tank")
if err := store.SetResourceOperatorState(ResourceOperatorState{
CanonicalID: oldID,
NeverAutoRemediate: true,
}); err != nil {
t.Fatalf("seed operator state: %v", err)
}
registry := NewRegistry(store)
// The old derivation is still alive in this registry: another record
// legitimately owns it, so the succession must not run.
registry.IngestRecords(SourceTrueNAS, []IngestRecord{
{
SourceID: "system:legacy-host/pool:tank",
Resource: Resource{
Type: ResourceTypeStorage,
Name: "tank",
LastSeen: time.Now().UTC(),
},
},
{
SourceID: "system:conn-a/pool:tank",
Resource: Resource{
Type: ResourceTypeStorage,
Name: "tank",
LastSeen: time.Now().UTC(),
},
SupersededCanonicalIDs: []string{oldID},
},
})
if _, found, err := store.GetResourceOperatorState(oldID); err != nil || !found {
t.Fatalf("expected operator state to stay under live old ID (found=%v err=%v)", found, err)
}
}