Files
pulse/internal/api/connections_grouping_test.go
T
rcourtman d6fa623035 Compose ESXi hosts as members of their owning vCenter connection
One vCenter connection spans many ESXi hosts, so the Infrastructure source
row now lists them the way Proxmox cluster rows list their nodes: an
expandable member list with per-host state, aliases, and last-seen. Members
are API-side composition only — no primary marker, no agent connection, and
the member subtitle reads 'vSphere host' instead of cluster-node wording.
TrueNAS keeps no member composition because that connection monitors exactly
one machine.

Mock mode now feeds its vCenter and TrueNAS fixtures into the connections
aggregator (only when no real instances are configured), so the mock ledger
shows the same platform source rows a real deployment would instead of
omitting vSphere and TrueNAS entirely.
2026-07-21 23:09:29 +01:00

561 lines
20 KiB
Go

package api
import (
"reflect"
"testing"
"time"
"github.com/rcourtman/pulse-go-rewrite/internal/config"
"github.com/rcourtman/pulse-go-rewrite/internal/monitoring"
unified "github.com/rcourtman/pulse-go-rewrite/internal/unifiedresources"
)
func TestBuildConnectionSystems_AttachesMergedAgentToOwningProxmoxSource(t *testing.T) {
cfg := &config.Config{DataPath: t.TempDir()}
monitor, err := monitoring.New(cfg)
if err != nil {
t.Fatalf("monitoring.New: %v", err)
}
t.Cleanup(func() { monitor.Stop() })
now := time.Date(2026, 4, 22, 12, 0, 0, 0, time.UTC)
adapter := unified.NewMonitorAdapter(nil)
adapter.PopulateSupplementalRecords(unified.SourceProxmox, []unified.IngestRecord{
{
SourceID: "node-pi",
Resource: unified.Resource{
ID: "resource-node-pi",
Type: unified.ResourceTypeAgent,
Name: "pi",
Status: unified.StatusOnline,
LastSeen: now,
Sources: []unified.DataSource{unified.SourceProxmox, unified.SourceAgent},
Identity: unified.ResourceIdentity{
MachineID: "machine-pi",
Hostnames: []string{"pi"},
},
Proxmox: &unified.ProxmoxData{
Instance: "pi",
NodeName: "pi",
LinkedAgentID: "agent-pi",
},
Agent: &unified.AgentData{
AgentID: "agent-pi",
Hostname: "pi",
MachineID: "machine-pi",
LinkedNodeID: "node-pi",
},
},
},
})
setTestUnexportedField(t, monitor, "resourceStore", monitoring.ResourceStoreInterface(adapter))
connections := []Connection{
{
ID: "pve:pi",
Type: ConnectionTypePVE,
Name: "pi",
Address: "https://192.168.0.2:8006",
State: ConnectionStateActive,
Enabled: true,
Surfaces: []string{"vms", "containers", "storage", "backups"},
Scope: map[string]bool{"vms": true, "containers": true, "storage": true, "backups": true},
Source: ConnectionSourceAgent,
Capabilities: ConnectionCapabilities{SupportsPause: true, SupportsScope: true, SupportsTest: true},
},
{
ID: "agent:agent-pi",
Type: ConnectionTypeAgent,
Name: "pi",
Address: "pi",
State: ConnectionStateActive,
Enabled: true,
Surfaces: []string{"host"},
Scope: map[string]bool{"host": true},
LastSeen: &now,
Source: ConnectionSourceAgent,
Capabilities: ConnectionCapabilities{SupportsPause: false, SupportsScope: false, SupportsTest: false},
},
}
systems := buildConnectionSystems(connections, monitor)
if len(systems) != 1 {
t.Fatalf("expected 1 grouped system, got %d (%+v)", len(systems), systems)
}
system := systems[0]
if system.ID != "pve:pi" {
t.Fatalf("system id = %q, want %q", system.ID, "pve:pi")
}
if system.Type != ConnectionTypePVE {
t.Fatalf("system type = %q, want %q", system.Type, ConnectionTypePVE)
}
if len(system.Components) != 2 {
t.Fatalf("expected 2 system components, got %+v", system.Components)
}
if system.Components[0].ConnectionID != "pve:pi" || system.Components[0].Role != ConnectionSystemComponentRolePrimary {
t.Fatalf("unexpected primary component: %+v", system.Components[0])
}
if system.Components[1].ConnectionID != "agent:agent-pi" || system.Components[1].Role != ConnectionSystemComponentRoleAttachment {
t.Fatalf("unexpected attachment component: %+v", system.Components[1])
}
}
func TestBuildConnectionSystems_ClusterMemberAgentsAttachToOwningProxmoxSystem(t *testing.T) {
cfg := &config.Config{DataPath: t.TempDir()}
monitor, err := monitoring.New(cfg)
if err != nil {
t.Fatalf("monitoring.New: %v", err)
}
t.Cleanup(func() { monitor.Stop() })
now := time.Date(2026, 4, 23, 12, 0, 0, 0, time.UTC)
adapter := unified.NewMonitorAdapter(nil)
adapter.PopulateSupplementalRecords(unified.SourceProxmox, []unified.IngestRecord{
{
SourceID: "node-delly",
Resource: unified.Resource{
ID: "resource-node-delly",
Type: unified.ResourceTypeAgent,
Name: "delly",
Status: unified.StatusOnline,
LastSeen: now,
Sources: []unified.DataSource{unified.SourceProxmox, unified.SourceAgent},
Identity: unified.ResourceIdentity{
MachineID: "machine-delly",
Hostnames: []string{"delly"},
IPAddresses: []string{"192.168.0.10"},
},
Proxmox: &unified.ProxmoxData{
Instance: "delly",
NodeName: "delly",
ClusterName: "homelab",
IsClusterMember: true,
HostURL: "https://delly:8006",
LinkedAgentID: "agent-delly",
},
Agent: &unified.AgentData{
AgentID: "agent-delly",
Hostname: "delly",
MachineID: "machine-delly",
LinkedNodeID: "node-delly",
},
},
Identity: unified.ResourceIdentity{
MachineID: "machine-delly",
Hostnames: []string{"delly"},
IPAddresses: []string{"192.168.0.10"},
},
},
{
SourceID: "node-minipc",
Resource: unified.Resource{
ID: "resource-node-minipc",
Type: unified.ResourceTypeAgent,
Name: "minipc",
Status: unified.StatusOnline,
LastSeen: now,
Sources: []unified.DataSource{unified.SourceProxmox, unified.SourceAgent},
Identity: unified.ResourceIdentity{
MachineID: "machine-minipc",
Hostnames: []string{"minipc"},
IPAddresses: []string{"192.168.0.11"},
},
Proxmox: &unified.ProxmoxData{
Instance: "delly",
NodeName: "minipc",
ClusterName: "homelab",
IsClusterMember: true,
HostURL: "https://minipc:8006",
LinkedAgentID: "agent-minipc",
},
Agent: &unified.AgentData{
AgentID: "agent-minipc",
Hostname: "minipc",
MachineID: "machine-minipc",
LinkedNodeID: "node-minipc",
},
},
Identity: unified.ResourceIdentity{
MachineID: "machine-minipc",
Hostnames: []string{"minipc"},
IPAddresses: []string{"192.168.0.11"},
},
},
})
setTestUnexportedField(t, monitor, "resourceStore", monitoring.ResourceStoreInterface(adapter))
connections := []Connection{
{
ID: "pve:delly",
Type: ConnectionTypePVE,
Name: "delly",
Address: "https://delly:8006",
State: ConnectionStateActive,
Enabled: true,
Surfaces: []string{"vms", "containers", "storage", "backups"},
Scope: map[string]bool{"vms": true, "containers": true, "storage": true, "backups": true},
Source: ConnectionSourceAgent,
Capabilities: ConnectionCapabilities{SupportsPause: true, SupportsScope: true, SupportsTest: true},
},
{
ID: "agent:agent-delly",
Type: ConnectionTypeAgent,
Name: "delly",
Address: "delly",
State: ConnectionStateActive,
Enabled: true,
Surfaces: []string{"host"},
Scope: map[string]bool{"host": true},
LastSeen: &now,
Source: ConnectionSourceAgent,
Capabilities: ConnectionCapabilities{SupportsPause: false, SupportsScope: false, SupportsTest: false},
},
{
ID: "agent:agent-minipc",
Type: ConnectionTypeAgent,
Name: "minipc",
Address: "minipc",
State: ConnectionStateActive,
Enabled: true,
Surfaces: []string{"host"},
Scope: map[string]bool{"host": true},
LastSeen: &now,
Source: ConnectionSourceAgent,
Capabilities: ConnectionCapabilities{SupportsPause: false, SupportsScope: false, SupportsTest: false},
},
}
systems := buildConnectionSystems(connections, monitor)
if len(systems) != 1 {
t.Fatalf("expected 1 grouped cluster system, got %d (%+v)", len(systems), systems)
}
system := systems[0]
if system.ID != "pve:delly" {
t.Fatalf("system id = %q, want %q", system.ID, "pve:delly")
}
if system.Type != ConnectionTypePVE {
t.Fatalf("system type = %q, want %q", system.Type, ConnectionTypePVE)
}
if system.ClusterName != "homelab" {
t.Fatalf("system clusterName = %q, want %q", system.ClusterName, "homelab")
}
if len(system.Components) != 3 {
t.Fatalf("expected 3 system components, got %+v", system.Components)
}
if len(system.Members) != 2 {
t.Fatalf("expected 2 system members, got %+v", system.Members)
}
componentRoles := make(map[string]ConnectionSystemComponentRole, len(system.Components))
for _, component := range system.Components {
componentRoles[component.ConnectionID] = component.Role
}
if componentRoles["pve:delly"] != ConnectionSystemComponentRolePrimary {
t.Fatalf("pve:delly role = %q, want %q", componentRoles["pve:delly"], ConnectionSystemComponentRolePrimary)
}
if componentRoles["agent:agent-delly"] != ConnectionSystemComponentRoleAttachment {
t.Fatalf("agent:agent-delly role = %q, want %q", componentRoles["agent:agent-delly"], ConnectionSystemComponentRoleAttachment)
}
if componentRoles["agent:agent-minipc"] != ConnectionSystemComponentRoleAttachment {
t.Fatalf("agent:agent-minipc role = %q, want %q", componentRoles["agent:agent-minipc"], ConnectionSystemComponentRoleAttachment)
}
membersByName := make(map[string]ConnectionSystemMember, len(system.Members))
for _, member := range system.Members {
membersByName[member.Name] = member
}
dellyMember, ok := membersByName["delly"]
if !ok {
t.Fatalf("expected delly member, got %+v", system.Members)
}
if !dellyMember.Primary {
t.Fatalf("expected delly to be the primary cluster member, got %+v", dellyMember)
}
if dellyMember.Endpoint != "https://delly:8006" {
t.Fatalf("delly endpoint = %q, want %q", dellyMember.Endpoint, "https://delly:8006")
}
if !reflect.DeepEqual(dellyMember.HostAliases, []string{"delly", "192.168.0.10"}) {
t.Fatalf("delly host aliases = %+v, want %+v", dellyMember.HostAliases, []string{"delly", "192.168.0.10"})
}
if dellyMember.AgentConnectionID != "agent:agent-delly" {
t.Fatalf("delly agent connection = %q, want %q", dellyMember.AgentConnectionID, "agent:agent-delly")
}
if dellyMember.State != ConnectionStateActive {
t.Fatalf("delly state = %q, want %q", dellyMember.State, ConnectionStateActive)
}
minipcMember, ok := membersByName["minipc"]
if !ok {
t.Fatalf("expected minipc member, got %+v", system.Members)
}
if minipcMember.Primary {
t.Fatalf("minipc should not be marked primary: %+v", minipcMember)
}
if minipcMember.Endpoint != "https://minipc:8006" {
t.Fatalf("minipc endpoint = %q, want %q", minipcMember.Endpoint, "https://minipc:8006")
}
if !reflect.DeepEqual(minipcMember.HostAliases, []string{"minipc", "192.168.0.11"}) {
t.Fatalf("minipc host aliases = %+v, want %+v", minipcMember.HostAliases, []string{"minipc", "192.168.0.11"})
}
if minipcMember.AgentConnectionID != "agent:agent-minipc" {
t.Fatalf("minipc agent connection = %q, want %q", minipcMember.AgentConnectionID, "agent:agent-minipc")
}
if minipcMember.State != ConnectionStateActive {
t.Fatalf("minipc state = %q, want %q", minipcMember.State, ConnectionStateActive)
}
}
func TestMergeConnectionSystemMembersKeepsMostSevereState(t *testing.T) {
activeMember := ConnectionSystemMember{
ID: "node-minipc",
Name: "minipc",
State: ConnectionStateActive,
Primary: true,
}
offlineMember := ConnectionSystemMember{
ID: "resource-node-minipc",
Name: "minipc",
State: ConnectionStateUnreachable,
}
merged := mergeConnectionSystemMembers(activeMember, offlineMember)
if merged.State != ConnectionStateUnreachable {
t.Fatalf("merged state = %q, want %q", merged.State, ConnectionStateUnreachable)
}
if !merged.Primary {
t.Fatalf("expected primary marker to survive merge: %+v", merged)
}
staleMember := ConnectionSystemMember{Name: "minipc", State: ConnectionStateStale}
freshMember := ConnectionSystemMember{Name: "minipc", State: ConnectionStateActive}
merged = mergeConnectionSystemMembers(staleMember, freshMember)
if merged.State != ConnectionStateStale {
t.Fatalf("merged state = %q, want %q", merged.State, ConnectionStateStale)
}
}
func TestBuildConnectionSystems_GuestAgentStaysStandaloneWhenOnlyClusterInstanceMatches(t *testing.T) {
cfg := &config.Config{DataPath: t.TempDir()}
monitor, err := monitoring.New(cfg)
if err != nil {
t.Fatalf("monitoring.New: %v", err)
}
t.Cleanup(func() { monitor.Stop() })
now := time.Date(2026, 4, 23, 12, 0, 0, 0, time.UTC)
adapter := unified.NewMonitorAdapter(nil)
adapter.PopulateSupplementalRecords(unified.SourceProxmox, []unified.IngestRecord{
{
SourceID: "guest-100",
Resource: unified.Resource{
ID: "resource-guest-100",
Type: unified.ResourceTypeAgent,
Name: "docker-vm",
Status: unified.StatusOnline,
LastSeen: now,
Sources: []unified.DataSource{unified.SourceProxmox, unified.SourceAgent},
Identity: unified.ResourceIdentity{
MachineID: "machine-guest-100",
Hostnames: []string{"docker-vm"},
},
Proxmox: &unified.ProxmoxData{
Instance: "delly",
NodeName: "minipc",
ClusterName: "homelab",
IsClusterMember: true,
VMID: 100,
},
Agent: &unified.AgentData{
AgentID: "agent-guest-100",
Hostname: "docker-vm",
MachineID: "machine-guest-100",
LinkedVMID: "guest-100",
LinkedNodeID: "",
},
},
},
})
setTestUnexportedField(t, monitor, "resourceStore", monitoring.ResourceStoreInterface(adapter))
connections := []Connection{
{
ID: "pve:delly",
Type: ConnectionTypePVE,
Name: "delly",
Address: "https://delly:8006",
State: ConnectionStateActive,
Enabled: true,
Surfaces: []string{"vms", "containers", "storage", "backups"},
Scope: map[string]bool{"vms": true, "containers": true, "storage": true, "backups": true},
Source: ConnectionSourceAgent,
Capabilities: ConnectionCapabilities{SupportsPause: true, SupportsScope: true, SupportsTest: true},
},
{
ID: "agent:agent-guest-100",
Type: ConnectionTypeAgent,
Name: "docker-vm",
Address: "docker-vm",
State: ConnectionStateActive,
Enabled: true,
Surfaces: []string{"host"},
Scope: map[string]bool{"host": true},
LastSeen: &now,
Source: ConnectionSourceAgent,
Capabilities: ConnectionCapabilities{SupportsPause: false, SupportsScope: false, SupportsTest: false},
},
}
systems := buildConnectionSystems(connections, monitor)
if len(systems) != 2 {
t.Fatalf("expected 2 grouped systems, got %d (%+v)", len(systems), systems)
}
systemsByID := make(map[string]ConnectionSystem, len(systems))
for _, system := range systems {
systemsByID[system.ID] = system
}
if len(systemsByID["pve:delly"].Components) != 1 {
t.Fatalf("pve:delly should keep only its primary component, got %+v", systemsByID["pve:delly"].Components)
}
guestSystem := systemsByID["agent:agent-guest-100"]
if len(guestSystem.Components) != 1 || guestSystem.Components[0].Role != ConnectionSystemComponentRolePrimary {
t.Fatalf("guest agent should remain standalone, got %+v", guestSystem.Components)
}
}
func TestBuildConnectionSystems_AttachesHostAgentToMatchingProxmoxSourceWithoutNodeInventory(t *testing.T) {
cfg := &config.Config{DataPath: t.TempDir()}
monitor, err := monitoring.New(cfg)
if err != nil {
t.Fatalf("monitoring.New: %v", err)
}
t.Cleanup(func() { monitor.Stop() })
now := time.Date(2026, 5, 13, 23, 45, 0, 0, time.UTC)
connections := []Connection{
{
ID: "pve:delly",
Type: ConnectionTypePVE,
Name: "delly",
Address: "https://delly:8006",
HostAliases: []string{"delly"},
State: ConnectionStateUnauthorized,
Enabled: true,
Surfaces: []string{"vms", "containers", "storage", "backups"},
Scope: map[string]bool{"vms": true, "containers": true, "storage": true, "backups": true},
Source: ConnectionSourceAgent,
Capabilities: ConnectionCapabilities{SupportsPause: true, SupportsScope: true, SupportsTest: true},
},
{
ID: "agent:agent-delly",
Type: ConnectionTypeAgent,
Name: "delly",
Address: "delly",
HostAliases: []string{"delly", "192.168.0.5"},
State: ConnectionStateActive,
Enabled: true,
Surfaces: []string{"host"},
Scope: map[string]bool{"host": true},
LastSeen: &now,
Source: ConnectionSourceAgent,
Capabilities: ConnectionCapabilities{SupportsPause: false, SupportsScope: false, SupportsTest: false},
},
{
ID: "agent:agent-minipc",
Type: ConnectionTypeAgent,
Name: "minipc",
Address: "minipc",
HostAliases: []string{"minipc", "192.168.0.134"},
State: ConnectionStateActive,
Enabled: true,
Surfaces: []string{"host"},
Scope: map[string]bool{"host": true},
LastSeen: &now,
Source: ConnectionSourceAgent,
Capabilities: ConnectionCapabilities{SupportsPause: false, SupportsScope: false, SupportsTest: false},
},
}
systems := buildConnectionSystems(connections, monitor)
systemsByID := make(map[string]ConnectionSystem, len(systems))
for _, system := range systems {
systemsByID[system.ID] = system
}
dellySystem := systemsByID["pve:delly"]
if len(dellySystem.Components) != 2 {
t.Fatalf("expected delly API and agent components, got %+v", dellySystem.Components)
}
componentRoles := make(map[string]ConnectionSystemComponentRole, len(dellySystem.Components))
for _, component := range dellySystem.Components {
componentRoles[component.ConnectionID] = component.Role
}
if componentRoles["pve:delly"] != ConnectionSystemComponentRolePrimary {
t.Fatalf("pve:delly role = %q, want %q", componentRoles["pve:delly"], ConnectionSystemComponentRolePrimary)
}
if componentRoles["agent:agent-delly"] != ConnectionSystemComponentRoleAttachment {
t.Fatalf("agent:agent-delly role = %q, want %q", componentRoles["agent:agent-delly"], ConnectionSystemComponentRoleAttachment)
}
minipcSystem := systemsByID["agent:agent-minipc"]
if len(minipcSystem.Components) != 1 || minipcSystem.Components[0].Role != ConnectionSystemComponentRolePrimary {
t.Fatalf("minipc should remain standalone without a direct source host match, got %+v", minipcSystem.Components)
}
}
// One vCenter connection spans many ESXi hosts, so vSphere host resources
// compose as members of their owning vmware connection the way Proxmox
// cluster nodes compose under their cluster source. Resources pointing at an
// unknown or differently-typed connection produce no member.
func TestConnectionSystemMemberFromVMwareHost(t *testing.T) {
lastSeen := time.Date(2026, 7, 21, 12, 0, 0, 0, time.UTC)
hostResource := unified.Resource{
ID: "vc-1:host:host-101",
Name: "esxi-01.lab.local",
Status: unified.StatusOnline,
LastSeen: lastSeen,
VMware: &unified.VMwareData{
ConnectionID: "vc-1",
EntityType: "host",
ManagedObjectID: "host-101",
},
}
connections := map[string]Connection{
"vmware:vc-1": {ID: "vmware:vc-1", Type: ConnectionTypeVMware, Name: "Lab vCenter"},
}
member, primaryID, ok := connectionSystemMemberFromVMwareHost(hostResource, connections)
if !ok {
t.Fatal("expected ESXi host resource to produce a vmware system member")
}
if primaryID != "vmware:vc-1" {
t.Fatalf("primary = %q, want %q", primaryID, "vmware:vc-1")
}
if member.Name != "esxi-01.lab.local" || member.State != ConnectionStateActive {
t.Fatalf("unexpected member projection: %+v", member)
}
if member.LastSeen == nil || !member.LastSeen.Equal(lastSeen) {
t.Fatalf("expected member lastSeen %v, got %+v", lastSeen, member.LastSeen)
}
if member.Primary {
t.Fatalf("ESXi hosts must not claim the primary marker: %+v", member)
}
vmResource := hostResource
vmResource.VMware = &unified.VMwareData{ConnectionID: "vc-1", EntityType: "vm"}
if _, _, ok := connectionSystemMemberFromVMwareHost(vmResource, connections); ok {
t.Fatal("VM resources must not become system members")
}
orphanResource := hostResource
orphanResource.VMware = &unified.VMwareData{ConnectionID: "vc-unknown", EntityType: "host"}
if _, _, ok := connectionSystemMemberFromVMwareHost(orphanResource, connections); ok {
t.Fatal("hosts pointing at an unconfigured vCenter must not become members")
}
}