Files
pulse/internal/api/connections_aggregator_test.go
T
rcourtman 884694a046 Carry the PBS-reported node name into the connections ledger
The PBS poll records the hostname the node reports about itself on
PBSInstance.NodeName so /api/connections can alias the PBS connection by
it and group the host agent running on that machine into the PBS row.

The name reaches the unified PBS resource (PBSData.NodeName), but
Monitor.PBSInstancesSnapshot() rebuilds each instance from the read-state
view, and the view had no accessor for it, so the snapshot always carried
an empty NodeName. The ledger's reported-name map was therefore always
empty and a PBS machine configured by IP or DNS alias still showed as an
API row plus a separate agent row. The unit tests missed it because they
hand the aggregator its inputs instead of going through the monitor.

Add PBSInstanceView.NodeName() and copy it in pbsInstanceFromReadStateView.
The new ledger test drives a real monitor through the input builder, the
connection aliases and the system grouping: with the conversion line
removed it fails on the missing alias, with it the agent attaches to the
PBS system.

Only direct root@pam sessions can read the node name (API-token
connections get none), so this restores grouping for password-auth PBS
connections; token-auth ones still match only on what they were
configured with.
2026-10-10 12:06:38 +01:00

1547 lines
54 KiB
Go

package api
import (
"context"
"reflect"
"testing"
"time"
"github.com/rcourtman/pulse-go-rewrite/internal/alerts"
"github.com/rcourtman/pulse-go-rewrite/internal/config"
"github.com/rcourtman/pulse-go-rewrite/internal/models"
"github.com/rcourtman/pulse-go-rewrite/internal/monitoring"
"github.com/rcourtman/pulse-go-rewrite/internal/platformsupport"
"github.com/rcourtman/pulse-go-rewrite/internal/remoteconfig"
unified "github.com/rcourtman/pulse-go-rewrite/internal/unifiedresources"
)
func ptrTime(t time.Time) *time.Time { return &t }
func ptrBool(v bool) *bool { return &v }
func healthEntry(lastSuccess *time.Time, errMessage, errCategory string, breakerState string) monitoring.InstanceHealth {
ps := monitoring.InstancePollStatus{LastSuccess: lastSuccess}
if errMessage != "" {
ps.LastError = &monitoring.ErrorDetail{
At: time.Now(),
Message: errMessage,
Category: errCategory,
}
}
return monitoring.InstanceHealth{
PollStatus: ps,
Breaker: monitoring.InstanceBreaker{State: breakerState},
}
}
func desiredAgentConfig(t *testing.T, commandsEnabled *bool, settings map[string]interface{}) connectionAgentDesiredConfig {
t.Helper()
metadata, err := remoteconfig.BuildDesiredConfigMetadata(commandsEnabled, settings)
if err != nil {
t.Fatalf("BuildDesiredConfigMetadata: %v", err)
}
fingerprint := ConnectionFleetConfigFingerprint{
Version: metadata.Version,
Hash: metadata.Hash,
}
return connectionAgentDesiredConfig{
Fingerprint: &fingerprint,
CommandsEnabled: cloneBoolPtr(commandsEnabled),
}
}
func TestDeriveConnectionState_Paused(t *testing.T) {
state, reason, _, _ := deriveConnectionState(false, monitoring.InstanceHealth{}, time.Now(), 0)
if state != ConnectionStatePaused {
t.Fatalf("got %q, want %q", state, ConnectionStatePaused)
}
if reason == "" {
t.Fatal("expected non-empty reason for paused state")
}
}
func TestDeriveConnectionState_Pending(t *testing.T) {
state, _, lastSeen, lastError := deriveConnectionState(true, monitoring.InstanceHealth{}, time.Now(), 0)
if state != ConnectionStatePending {
t.Fatalf("got %q, want %q", state, ConnectionStatePending)
}
if lastSeen != nil || lastError != nil {
t.Fatal("expected nil lastSeen and lastError in pending state")
}
}
func TestDeriveConnectionState_Unauthorized(t *testing.T) {
now := time.Now()
h := healthEntry(ptrTime(now.Add(-30*time.Second)), "401 Unauthorized: token invalid", "auth", "closed")
state, reason, _, err := deriveConnectionState(true, h, now, 0)
if state != ConnectionStateUnauthorized {
t.Fatalf("got %q, want %q", state, ConnectionStateUnauthorized)
}
if reason == "" {
t.Fatal("expected reason to include error message")
}
if err == nil || err.Message != "401 Unauthorized: token invalid" {
t.Fatalf("unexpected lastError: %+v", err)
}
}
func TestDeriveConnectionState_Unreachable(t *testing.T) {
now := time.Now()
h := healthEntry(ptrTime(now.Add(-30*time.Second)), "connection refused", "network", "open")
state, _, _, _ := deriveConnectionState(true, h, now, 0)
if state != ConnectionStateUnreachable {
t.Fatalf("got %q, want %q", state, ConnectionStateUnreachable)
}
}
func TestDeriveConnectionState_Stale(t *testing.T) {
now := time.Now()
stale := now.Add(-5 * time.Minute)
h := healthEntry(ptrTime(stale), "", "", "closed")
state, reason, _, _ := deriveConnectionState(true, h, now, 0)
if state != ConnectionStateStale {
t.Fatalf("got %q, want %q", state, ConnectionStateStale)
}
if reason == "" {
t.Fatal("expected reason to describe staleness")
}
}
func TestDeriveConnectionState_Active(t *testing.T) {
now := time.Now()
h := healthEntry(ptrTime(now.Add(-10*time.Second)), "", "", "closed")
state, _, _, _ := deriveConnectionState(true, h, now, 0)
if state != ConnectionStateActive {
t.Fatalf("got %q, want %q", state, ConnectionStateActive)
}
}
func TestBuildConnections_SortsByTypeThenName(t *testing.T) {
now := time.Now()
in := aggregatorInputs{
pveInstances: []config.PVEInstance{
{Name: "beta", Host: "https://b.lan:8006", MonitorVMs: true},
{Name: "alpha", Host: "https://a.lan:8006", MonitorVMs: true},
},
pbsInstances: []config.PBSInstance{
{Name: "backups", Host: "https://bkp.lan:8007"},
},
now: now,
}
got := buildConnections(in)
if len(got) != 3 {
t.Fatalf("expected 3 connections, got %d", len(got))
}
if got[0].Type != ConnectionTypePBS {
t.Fatalf("expected PBS first, got %q", got[0].Type)
}
if got[1].Name != "alpha" || got[2].Name != "beta" {
t.Fatalf("PVE order wrong: %q, %q", got[1].Name, got[2].Name)
}
}
func TestBuildConnectionsCarriesMonitorPolicyIdentityForPBSAndPMG(t *testing.T) {
got := buildConnections(aggregatorInputs{
pbsInstances: []config.PBSInstance{{Name: "backup-main"}},
pmgInstances: []config.PMGInstance{{Name: "mail-main"}},
now: time.Now(),
})
policyIDs := make(map[ConnectionType]string, len(got))
for _, connection := range got {
policyIDs[connection.Type] = connection.alertPolicyResourceID
}
if policyIDs[ConnectionTypePBS] != monitoring.PBSMonitorResourceID("backup-main") {
t.Fatalf("PBS alert policy ID = %q, want monitor resource identity", policyIDs[ConnectionTypePBS])
}
if policyIDs[ConnectionTypePMG] != monitoring.PMGMonitorResourceID("mail-main") {
t.Fatalf("PMG alert policy ID = %q, want monitor resource identity", policyIDs[ConnectionTypePMG])
}
}
func TestBuildConnections_PVEPausedRespectsDisabled(t *testing.T) {
in := aggregatorInputs{
pveInstances: []config.PVEInstance{{Name: "pve1", Host: "https://pve1.lan:8006", Disabled: true}},
now: time.Now(),
}
got := buildConnections(in)
if len(got) != 1 {
t.Fatalf("expected 1 connection, got %d", len(got))
}
if got[0].State != ConnectionStatePaused {
t.Fatalf("expected paused, got %q", got[0].State)
}
if got[0].Enabled {
t.Fatal("expected Enabled=false for Disabled PVE instance")
}
}
func TestBuildConnections_AgentStateFromLastSeen(t *testing.T) {
now := time.Now()
in := aggregatorInputs{
hosts: []models.Host{
{ID: "fresh", Hostname: "h1", LastSeen: now.Add(-10 * time.Second), IntervalSeconds: 30},
{ID: "stale", Hostname: "h2", LastSeen: now.Add(-5*time.Minute - time.Second), IntervalSeconds: 30},
{ID: "never", Hostname: "h3"},
},
now: now,
}
got := buildConnections(in)
byID := map[string]Connection{}
for _, c := range got {
byID[c.ID] = c
}
if byID["agent:fresh"].State != ConnectionStateActive {
t.Fatalf("fresh agent: got %q, want active", byID["agent:fresh"].State)
}
if byID["agent:stale"].State != ConnectionStateStale {
t.Fatalf("stale agent: got %q, want stale", byID["agent:stale"].State)
}
if byID["agent:never"].State != ConnectionStatePending {
t.Fatalf("never-reported agent: got %q, want pending", byID["agent:never"].State)
}
for _, c := range got {
if c.Capabilities.SupportsPause || c.Capabilities.SupportsScope {
t.Fatalf("agents must not advertise pause/scope capabilities: %+v", c)
}
}
}
func TestBuildConnections_AvailabilityTargetStateFromProbeStatus(t *testing.T) {
checkedAt := time.Date(2026, 5, 6, 10, 0, 0, 0, time.UTC)
in := aggregatorInputs{
availabilityTargets: []config.AvailabilityTarget{
{
ID: "sensor-1",
Name: "Energy monitor",
Address: "192.0.2.10",
Protocol: config.AvailabilityProbeICMP,
Enabled: true,
FailureThreshold: 2,
},
},
availabilityStatuses: map[string]monitoring.AvailabilityProbeStatus{
"sensor-1": {
TargetID: "sensor-1",
Available: false,
LastChecked: checkedAt,
ConsecutiveFailures: 1,
LastError: "timeout",
},
},
now: checkedAt.Add(5 * time.Second),
}
got := buildConnections(in)
if len(got) != 1 {
t.Fatalf("expected 1 connection, got %d", len(got))
}
connection := got[0]
if connection.ID != "availability:sensor-1" {
t.Fatalf("ID = %q, want availability:sensor-1", connection.ID)
}
if connection.Type != ConnectionTypeAvailability {
t.Fatalf("Type = %q, want availability", connection.Type)
}
if connection.State != ConnectionStateUnreachable {
t.Fatalf("State = %q, want unreachable", connection.State)
}
if connection.LastError == nil || connection.LastError.Category != "availability" {
t.Fatalf("LastError = %+v, want availability category", connection.LastError)
}
if !reflect.DeepEqual(connection.Surfaces, []string{"availability"}) {
t.Fatalf("Surfaces = %+v, want [availability]", connection.Surfaces)
}
if !connection.Capabilities.SupportsPause || connection.Capabilities.SupportsScope || !connection.Capabilities.SupportsTest {
t.Fatalf("Capabilities = %+v, want pause/test without scope", connection.Capabilities)
}
if !reflect.DeepEqual(connection.HostAliases, []string{"192.0.2.10"}) {
t.Fatalf("HostAliases = %+v, want endpoint address", connection.HostAliases)
}
}
func TestDeriveAvailabilityConnectionStateUsesCanonicalRemoteProbeFreshness(t *testing.T) {
receivedAt := time.Date(2026, 7, 29, 10, 0, 0, 0, time.UTC)
agentCheckedAt := receivedAt.Add(-24 * time.Hour)
target := config.NormalizeAvailabilityTarget(config.AvailabilityTarget{
ID: "remote-sensor",
Name: "Remote sensor",
Address: "sensor.example",
Protocol: config.AvailabilityProbeICMP,
Enabled: true,
PollIntervalSecs: 60,
ProbeAgentID: "probe-agent",
})
status := monitoring.AvailabilityProbeStatus{
TargetID: target.ID,
Available: true,
LastChecked: agentCheckedAt,
LastSuccess: agentCheckedAt,
ProbeAgentID: "probe-agent",
ProbeReportReceivedAt: receivedAt,
}
state, reason, lastSeen, lastError := deriveAvailabilityConnectionState(
target,
status,
receivedAt.Add(4*time.Minute),
)
if state != ConnectionStateActive || reason != "" || lastError != nil {
t.Fatalf("connection = state %q reason %q error %+v, want active", state, reason, lastError)
}
if lastSeen == nil || !lastSeen.Equal(receivedAt) {
t.Fatalf("last seen = %v, want server receipt %v", lastSeen, receivedAt)
}
}
func TestBuildConnections_AgentHostAliasesIncludeReportedIdentityHints(t *testing.T) {
now := time.Now()
in := aggregatorInputs{
hosts: []models.Host{
{
ID: "pi",
Hostname: "pi",
Platform: "slackware",
OSName: "Unraid",
OSVersion: "7.1.0",
KernelVersion: "6.12.0",
Architecture: "x86_64",
ReportIP: "192.168.0.2",
LastSeen: now,
AgentVersion: "6.0.0",
CommandsEnabled: true,
NetworkInterfaces: []models.HostNetworkInterface{
{Name: "eth0", Addresses: []string{"192.168.0.2/24", "fe80::1/64"}},
},
},
},
expectedAgentVersion: "6.0.1",
now: now,
}
got := buildConnections(in)
if len(got) != 1 {
t.Fatalf("expected 1 connection, got %d", len(got))
}
if !reflect.DeepEqual(got[0].HostAliases, []string{"pi", "192.168.0.2", "fe80::1"}) {
t.Fatalf("agent host aliases = %+v, want %+v", got[0].HostAliases, []string{"pi", "192.168.0.2", "fe80::1"})
}
if got[0].AgentIdentity == nil {
t.Fatal("expected agent identity metadata")
}
if !reflect.DeepEqual(
got[0].AgentIdentity,
&ConnectionAgentIdentity{
Hostname: "pi",
Platform: "linux",
HostProfile: "unraid",
OSName: "Unraid",
OSVersion: "7.1.0",
KernelVersion: "6.12.0",
Architecture: "x86_64",
ReportIP: "192.168.0.2",
CommandsEnabled: true,
},
) {
t.Fatalf("agent identity = %+v", got[0].AgentIdentity)
}
}
// A PBS connection's identity must include the hostname the PBS node reports
// about itself, not just the configured address: reported identity is what
// lets the host agent running on the PBS machine attach to the PBS
// connection when the connection was configured by IP or DNS alias, so the
// ledger shows one system row instead of an API row plus an agent row.
func TestBuildConnections_PBSHostAliasesIncludeReportedNodeName(t *testing.T) {
now := time.Now()
in := aggregatorInputs{
pbsInstances: []config.PBSInstance{
{Name: "backup", Host: "https://192.0.2.40:8007"},
},
pbsReportedNodeNames: map[string]string{"backup": "pbs01"},
now: now,
}
got := buildConnections(in)
if len(got) != 1 {
t.Fatalf("expected 1 connection, got %d", len(got))
}
if !reflect.DeepEqual(got[0].HostAliases, []string{"backup", "192.0.2.40", "pbs01"}) {
t.Fatalf("pbs host aliases = %+v, want reported node name included", got[0].HostAliases)
}
agent := Connection{
ID: "agent:pbs01",
Type: ConnectionTypeAgent,
Name: "pbs01",
Address: "pbs01",
HostAliases: []string{"pbs01", "10.10.0.4"},
}
connections := map[string]Connection{got[0].ID: got[0], agent.ID: agent}
if attached := directPlatformHostAttachment(agent, connections); attached != "pbs:backup" {
t.Fatalf("directPlatformHostAttachment() = %q, want %q", attached, "pbs:backup")
}
}
func TestPBSReportedNodeNamesByInstanceSkipsBlankIdentity(t *testing.T) {
got := pbsReportedNodeNamesByInstance([]models.PBSInstance{
{Name: "backup", NodeName: " pbs01 "},
{Name: "unnamed", NodeName: " "},
{Name: "", NodeName: "orphan"},
})
want := map[string]string{"backup": "pbs01"}
if !reflect.DeepEqual(got, want) {
t.Fatalf("pbsReportedNodeNamesByInstance() = %+v, want %+v", got, want)
}
}
// The reported node name has to survive the whole route into the ledger:
// unified PBS resource, Monitor.PBSInstancesSnapshot, aggregator inputs,
// connection HostAliases, system grouping. A PBS connection configured by IP
// depends on it, because nothing the host agent running on that machine
// reports matches the configured address. The unit tests above feed their
// inputs by hand and cannot see the snapshot dropping the name.
func TestConnectionsLedger_GroupsHostAgentWithPBSByReportedNodeName(t *testing.T) {
setMockModeForTest(t, false)
monitor, err := monitoring.New(&config.Config{DataPath: t.TempDir()})
if err != nil {
t.Fatalf("monitoring.New: %v", err)
}
t.Cleanup(func() { monitor.Stop() })
now := time.Now()
adapter := unified.NewMonitorAdapter(nil)
adapter.PopulateFromSnapshot(models.StateSnapshot{
PBSInstances: []models.PBSInstance{{
ID: "pbs-backup",
Name: "backup",
Host: "https://192.0.2.40:8007",
NodeName: "pbs01",
Status: "online",
LastSeen: now,
}},
Hosts: []models.Host{{
ID: "host-pbs01",
Hostname: "pbs01",
Status: "online",
LastSeen: now,
}},
})
setTestUnexportedField(t, monitor, "resourceStore", monitoring.ResourceStoreInterface(adapter))
cfg := &config.Config{
PBSInstances: []config.PBSInstance{{Name: "backup", Host: "https://192.0.2.40:8007"}},
}
inputs := buildAggregatorInputsWithRuntimeSources(context.Background(), cfg, nil, monitor, aggregatorRuntimeSources{})
connections := buildConnections(inputs)
var pbsConnection, agentConnection *Connection
for i := range connections {
switch connections[i].Type {
case ConnectionTypePBS:
pbsConnection = &connections[i]
case ConnectionTypeAgent:
agentConnection = &connections[i]
}
}
if pbsConnection == nil || agentConnection == nil {
t.Fatalf("expected a PBS and an agent connection, got %+v", connections)
}
if !reflect.DeepEqual(pbsConnection.HostAliases, []string{"backup", "192.0.2.40", "pbs01"}) {
t.Fatalf("pbs host aliases = %+v, want the reported node name %q included", pbsConnection.HostAliases, "pbs01")
}
systems := buildConnectionSystems(connections, monitor)
if len(systems) != 1 || systems[0].ID != pbsConnection.ID {
t.Fatalf("expected one system led by %q, got %+v", pbsConnection.ID, systems)
}
roles := make(map[string]ConnectionSystemComponentRole, len(systems[0].Components))
for _, component := range systems[0].Components {
roles[component.ConnectionID] = component.Role
}
if roles[pbsConnection.ID] != ConnectionSystemComponentRolePrimary {
t.Fatalf("%s role = %q, want %q", pbsConnection.ID, roles[pbsConnection.ID], ConnectionSystemComponentRolePrimary)
}
if roles[agentConnection.ID] != ConnectionSystemComponentRoleAttachment {
t.Fatalf("%s role = %q, want %q", agentConnection.ID, roles[agentConnection.ID], ConnectionSystemComponentRoleAttachment)
}
}
func TestBuildConnections_ProjectsLegacyLinuxDistroToRuntimePlatform(t *testing.T) {
now := time.Now()
got := buildConnections(aggregatorInputs{
hosts: []models.Host{{
ID: "mageia-host",
Hostname: "mageia-host",
Platform: "mageia",
OSName: "Mageia",
Architecture: "x86_64",
LastSeen: now,
AgentVersion: "6.1.0-rc.4",
CommandsEnabled: true,
}},
expectedAgentVersion: "6.1.1",
now: now,
})
if len(got) != 1 || got[0].AgentIdentity == nil {
t.Fatalf("expected one connection with agent identity, got %+v", got)
}
if got[0].AgentIdentity.Platform != platformsupport.RuntimePlatformLinux {
t.Fatalf("agent platform = %q, want linux", got[0].AgentIdentity.Platform)
}
if got[0].AgentIdentity.OSName != "Mageia" {
t.Fatalf("agent OS name = %q, want Mageia", got[0].AgentIdentity.OSName)
}
}
func TestBuildConnections_AgentHostProfileFromUnraidStorageFacts(t *testing.T) {
now := time.Now()
in := aggregatorInputs{
hosts: []models.Host{
{
ID: "tower",
Hostname: "tower",
Platform: "linux",
Unraid: &models.HostUnraidStorage{ArrayStarted: true},
LastSeen: now,
AgentVersion: "6.12.10",
CommandsEnabled: false,
},
},
now: now,
}
got := buildConnections(in)
if len(got) != 1 {
t.Fatalf("expected 1 connection, got %d", len(got))
}
identity := got[0].AgentIdentity
if identity == nil {
t.Fatal("expected agent identity metadata")
}
if identity.HostProfile != "unraid" {
t.Fatalf("host profile = %q, want %q", identity.HostProfile, "unraid")
}
if identity.Platform != "linux" {
t.Fatalf("platform = %q, want %q", identity.Platform, "linux")
}
if identity.OSName != "" {
t.Fatalf("os name = %q, want empty when source identity is absent", identity.OSName)
}
}
func TestBuildConnections_AgentVersionUpdateAvailability(t *testing.T) {
now := time.Now()
in := aggregatorInputs{
hosts: []models.Host{
{ID: "current", Hostname: "current", LastSeen: now, AgentVersion: "6.0.3", AgentUpdate: &models.AgentUpdateStatus{State: "disabled"}},
{ID: "outdated", Hostname: "outdated", LastSeen: now, AgentVersion: "6.0.0", AgentUpdate: &models.AgentUpdateStatus{State: "disabled"}},
{
ID: "failed",
Hostname: "failed",
LastSeen: now,
AgentVersion: "6.0.3",
AgentUpdate: &models.AgentUpdateStatus{State: "error", LastError: "signature verification failed"},
AgentModules: []models.AgentModuleStatus{{
Name: "docker", Enabled: true, State: "retrying", LastError: "socket unavailable", UpdatedAt: now,
}},
},
{ID: "unknown", Hostname: "unknown", LastSeen: now},
},
expectedAgentVersion: "6.0.3",
now: now,
}
got := buildConnections(in)
byID := map[string]Connection{}
for _, connection := range got {
byID[connection.ID] = connection
}
if byID["agent:current"].AgentVersion != "6.0.3" {
t.Fatalf("current agent version = %q, want %q", byID["agent:current"].AgentVersion, "6.0.3")
}
if byID["agent:current"].ExpectedAgentVersion != "6.0.3" {
t.Fatalf(
"current expected agent version = %q, want %q",
byID["agent:current"].ExpectedAgentVersion,
"6.0.3",
)
}
if byID["agent:current"].AgentUpdateAvailable {
t.Fatal("current agent should not report an update available")
}
if byID["agent:current"].Fleet.UpdateStatus != fleetStateCurrent {
t.Fatalf("current agent update status = %q, want current", byID["agent:current"].Fleet.UpdateStatus)
}
if byID["agent:outdated"].AgentVersion != "6.0.0" {
t.Fatalf(
"outdated agent version = %q, want %q",
byID["agent:outdated"].AgentVersion,
"6.0.0",
)
}
if !byID["agent:outdated"].AgentUpdateAvailable {
t.Fatal("outdated agent should report an update available")
}
if byID["agent:outdated"].Fleet.UpdateStatus != fleetStateDisabled {
t.Fatalf("outdated disabled agent update status = %q, want disabled", byID["agent:outdated"].Fleet.UpdateStatus)
}
if byID["agent:failed"].Fleet.UpdateStatus != fleetStateFailed || byID["agent:failed"].AgentUpdate == nil || byID["agent:failed"].AgentUpdate.LastError != "signature verification failed" {
t.Fatalf("failed agent update status = %+v", byID["agent:failed"])
}
if byID["agent:failed"].Fleet.AdapterHealth != fleetStateDegraded || len(byID["agent:failed"].AgentModules) != 1 {
t.Fatalf("failed agent module status = %+v", byID["agent:failed"])
}
if byID["agent:unknown"].AgentVersion != "" {
t.Fatalf("unknown agent version = %q, want empty", byID["agent:unknown"].AgentVersion)
}
if byID["agent:unknown"].AgentUpdateAvailable {
t.Fatal("agent without a reported version should not raise an update flag")
}
}
func TestBuildConnections_AgentFleetGovernance(t *testing.T) {
now := time.Now()
currentDesiredConfig := desiredAgentConfig(t, ptrBool(true), nil)
currentDesired := *currentDesiredConfig.Fingerprint
in := aggregatorInputs{
hosts: []models.Host{
{
ID: "current",
Hostname: "current",
LastSeen: now,
AgentVersion: "6.0.3",
CommandsEnabled: true,
AppliedConfig: &models.AgentConfigFingerprint{
Version: currentDesired.Version,
Hash: currentDesired.Hash,
},
},
{
ID: "outdated",
Hostname: "outdated",
LastSeen: now,
AgentVersion: "6.0.0",
},
{
ID: "pending",
Hostname: "pending",
},
},
agentDesiredConfigs: map[string]connectionAgentDesiredConfig{
"current": currentDesiredConfig,
},
expectedAgentVersion: "6.0.3",
now: now,
}
got := buildConnections(in)
byID := map[string]Connection{}
for _, connection := range got {
byID[connection.ID] = connection
}
current := byID["agent:current"].Fleet
if current.EnrollmentState != fleetStateEnrolled ||
current.LivenessState != fleetStateActive ||
current.VersionDrift != fleetStateCurrent ||
current.AdapterHealth != fleetStateHealthy ||
current.ConfigRollout != fleetStateReported ||
current.CredentialStatus != fleetStateVerified ||
current.UpdateStatus != fleetStateCurrent ||
current.RemoteControl != fleetStateEnabled {
t.Fatalf("current agent fleet governance = %+v", current)
}
if current.ConfigDrift == nil ||
current.ConfigDrift.Status != fleetConfigDriftCurrent ||
current.ConfigDrift.Desired == nil ||
current.ConfigDrift.Applied == nil ||
current.ConfigDrift.Desired.Version != connectionAgentConfigFingerprintVersion ||
current.ConfigDrift.Desired.Hash != currentDesired.Hash ||
current.ConfigDrift.Applied.Hash != currentDesired.Hash {
t.Fatalf("current agent config drift = %+v", current.ConfigDrift)
}
if current.Rollout == nil || current.Rollout.Status != fleetStateCurrent || current.Rollout.Stage != fleetRolloutStageApplied {
t.Fatalf("current agent rollout = %+v", current.Rollout)
}
if current.CommandPolicy == nil ||
current.CommandPolicy.Status != fleetStateEnabled ||
current.CommandPolicy.Desired != fleetStateEnabled ||
current.CommandPolicy.Applied != fleetStateEnabled ||
current.CommandPolicy.Enforcement != fleetCommandPolicyInSync {
t.Fatalf("current agent command policy = %+v", current.CommandPolicy)
}
outdated := byID["agent:outdated"].Fleet
if outdated.VersionDrift != fleetStateBehind ||
outdated.UpdateStatus != fleetStateUpdateAvailable ||
outdated.RemoteControl != fleetStateDisabled {
t.Fatalf("outdated agent fleet governance = %+v", outdated)
}
if outdated.CommandPolicy == nil ||
outdated.CommandPolicy.Status != fleetStateDisabled ||
outdated.CommandPolicy.Desired != fleetStateUnknown ||
outdated.CommandPolicy.Applied != fleetStateDisabled ||
outdated.CommandPolicy.Enforcement != fleetCommandPolicyNotApplicable {
t.Fatalf("outdated agent command policy = %+v", outdated.CommandPolicy)
}
pending := byID["agent:pending"].Fleet
if pending.EnrollmentState != fleetStatePending ||
pending.LivenessState != string(ConnectionStatePending) ||
pending.AdapterHealth != fleetStateDegraded ||
pending.ConfigRollout != fleetStateUnknown ||
pending.CredentialStatus != fleetStateUnknown {
t.Fatalf("pending agent fleet governance = %+v", pending)
}
if pending.ConfigDrift == nil || pending.ConfigDrift.Status != fleetStateUnknown {
t.Fatalf("pending agent config drift = %+v", pending.ConfigDrift)
}
if pending.Rollout == nil || pending.Rollout.Status != fleetStatePending {
t.Fatalf("pending agent rollout = %+v", pending.Rollout)
}
if pending.CommandPolicy == nil ||
pending.CommandPolicy.Desired != fleetStateUnknown ||
pending.CommandPolicy.Applied != fleetStateUnknown ||
pending.CommandPolicy.Enforcement != fleetStatePending {
t.Fatalf("pending agent command policy = %+v", pending.CommandPolicy)
}
}
func TestBuildConnections_AgentCredentialInventoryDrivesFleetHealth(t *testing.T) {
now := time.Date(2026, 8, 9, 12, 0, 0, 0, time.UTC)
lastUsedAt := now.Add(-time.Minute)
expiredAt := now.Add(-time.Hour)
tests := []struct {
name string
tokenID string
tokens []config.APITokenRecord
wantStatus string
wantHealthStatus string
wantHealthRotation string
}{
{
name: "active",
tokenID: "active-token",
tokens: []config.APITokenRecord{{ID: "active-token", LastUsedAt: &lastUsedAt}},
wantStatus: fleetStateVerified,
wantHealthStatus: fleetStateVerified,
wantHealthRotation: fleetCredentialRotationHealthy,
},
{
name: "missing or revoked",
tokenID: "missing-token",
wantStatus: fleetStateInvalid,
wantHealthStatus: fleetStateInvalid,
wantHealthRotation: fleetCredentialRotationInvalid,
},
{
name: "expired",
tokenID: "expired-token",
tokens: []config.APITokenRecord{{
ID: "expired-token",
ExpiresAt: &expiredAt,
}},
wantStatus: fleetStateInvalid,
wantHealthStatus: fleetCredentialStatusExpired,
wantHealthRotation: fleetCredentialRotationExpired,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
connections := buildConnections(aggregatorInputs{
apiTokens: test.tokens,
agentTokenInventoryKnown: true,
hosts: []models.Host{{
ID: "agent-auth",
Hostname: "agent-auth",
Status: "online",
LastSeen: now,
AgentVersion: "6.2.0",
TokenID: test.tokenID,
}},
expectedAgentVersion: "6.2.0",
now: now,
})
if len(connections) != 1 {
t.Fatalf("connections = %d, want 1", len(connections))
}
fleet := connections[0].Fleet
if fleet.CredentialStatus != test.wantStatus {
t.Fatalf("credential status = %q, want %q", fleet.CredentialStatus, test.wantStatus)
}
if fleet.CredentialHealth == nil ||
fleet.CredentialHealth.Status != test.wantHealthStatus ||
fleet.CredentialHealth.Rotation != test.wantHealthRotation {
t.Fatalf("credential health = %+v, want status %q rotation %q", fleet.CredentialHealth, test.wantHealthStatus, test.wantHealthRotation)
}
})
}
}
func TestBuildConnections_UnknownCredentialInventoryDoesNotInventInvalidState(t *testing.T) {
now := time.Date(2026, 8, 9, 12, 0, 0, 0, time.UTC)
connections := buildConnections(aggregatorInputs{
hosts: []models.Host{{
ID: "agent-mock",
Hostname: "agent-mock",
Status: "online",
LastSeen: now,
AgentVersion: "6.2.0",
TokenID: "synthetic-token",
}},
expectedAgentVersion: "6.2.0",
now: now,
})
if len(connections) != 1 {
t.Fatalf("connections = %d, want 1", len(connections))
}
if got := connections[0].Fleet.CredentialStatus; got != fleetStateVerified {
t.Fatalf("credential status = %q, want %q when inventory is unavailable", got, fleetStateVerified)
}
}
func TestBuildConnections_AgentWithoutManagedDesiredConfigDoesNotReportRolloutPending(t *testing.T) {
now := time.Now()
in := aggregatorInputs{
hosts: []models.Host{
{
ID: "default-agent",
Hostname: "default-agent",
LastSeen: now,
CommandsEnabled: false,
},
},
agentDesiredConfigs: map[string]connectionAgentDesiredConfig{
"default-agent": {},
},
now: now,
}
got := buildConnections(in)
if len(got) != 1 {
t.Fatalf("expected 1 connection, got %d", len(got))
}
fleet := got[0].Fleet
if fleet.ConfigDrift == nil ||
fleet.ConfigDrift.Status != fleetConfigDriftNotApplicable ||
fleet.ConfigDrift.Desired != nil ||
fleet.ConfigDrift.Applied != nil {
t.Fatalf("default agent config drift = %+v", fleet.ConfigDrift)
}
if fleet.Rollout == nil ||
fleet.Rollout.Status != fleetStateCurrent ||
fleet.Rollout.Stage != fleetRolloutStageApplied {
t.Fatalf("default agent rollout = %+v", fleet.Rollout)
}
}
func TestBuildConnections_AgentCommandPolicyDesiredAppliedConvergence(t *testing.T) {
now := time.Now()
desiredDisabled := desiredAgentConfig(t, ptrBool(false), nil)
desiredEnabled := desiredAgentConfig(t, ptrBool(true), nil)
in := aggregatorInputs{
hosts: []models.Host{
{
ID: "desired-disabled-applied-enabled",
Hostname: "desired-disabled-applied-enabled",
LastSeen: now,
CommandsEnabled: true,
},
{
ID: "desired-enabled-applied-disabled",
Hostname: "desired-enabled-applied-disabled",
LastSeen: now,
CommandsEnabled: false,
},
{
ID: "desired-disabled-unreported",
Hostname: "desired-disabled-unreported",
},
},
agentDesiredConfigs: map[string]connectionAgentDesiredConfig{
"desired-disabled-applied-enabled": desiredDisabled,
"desired-enabled-applied-disabled": desiredEnabled,
"desired-disabled-unreported": desiredDisabled,
},
now: now,
}
got := buildConnections(in)
byID := map[string]Connection{}
for _, connection := range got {
byID[connection.ID] = connection
}
disabledDesired := byID["agent:desired-disabled-applied-enabled"].Fleet
if disabledDesired.RemoteControl != fleetStateEnabled {
t.Fatalf("remote control for desired-disabled/applied-enabled = %q, want applied enabled", disabledDesired.RemoteControl)
}
if disabledDesired.CommandPolicy == nil ||
disabledDesired.CommandPolicy.Status != fleetStateEnabled ||
disabledDesired.CommandPolicy.Desired != fleetStateDisabled ||
disabledDesired.CommandPolicy.Applied != fleetStateEnabled ||
disabledDesired.CommandPolicy.Enforcement != fleetCommandPolicyDrifted {
t.Fatalf("desired-disabled/applied-enabled command policy = %+v", disabledDesired.CommandPolicy)
}
enabledDesired := byID["agent:desired-enabled-applied-disabled"].Fleet
if enabledDesired.RemoteControl != fleetStateDisabled {
t.Fatalf("remote control for desired-enabled/applied-disabled = %q, want applied disabled", enabledDesired.RemoteControl)
}
if enabledDesired.CommandPolicy == nil ||
enabledDesired.CommandPolicy.Status != fleetStateDisabled ||
enabledDesired.CommandPolicy.Desired != fleetStateEnabled ||
enabledDesired.CommandPolicy.Applied != fleetStateDisabled ||
enabledDesired.CommandPolicy.Enforcement != fleetCommandPolicyDrifted {
t.Fatalf("desired-enabled/applied-disabled command policy = %+v", enabledDesired.CommandPolicy)
}
unreported := byID["agent:desired-disabled-unreported"].Fleet
if unreported.RemoteControl != fleetStateUnknown {
t.Fatalf("remote control for unreported agent = %q, want unknown", unreported.RemoteControl)
}
if unreported.CommandPolicy == nil ||
unreported.CommandPolicy.Status != fleetStateUnknown ||
unreported.CommandPolicy.Desired != fleetStateDisabled ||
unreported.CommandPolicy.Applied != fleetStateUnknown ||
unreported.CommandPolicy.Enforcement != fleetStatePending {
t.Fatalf("unreported command policy = %+v", unreported.CommandPolicy)
}
}
func TestBuildConnections_AgentConfigDriftUsesCanonicalDesiredMetadataWithoutSelfComparing(t *testing.T) {
now := time.Now()
desiredConfig := desiredAgentConfig(t, ptrBool(true), map[string]interface{}{
"interval": "10s",
})
desired := *desiredConfig.Fingerprint
in := aggregatorInputs{
hosts: []models.Host{
{
ID: "agent-1",
Hostname: "agent-1",
LastSeen: now,
CommandsEnabled: false,
DiskExclude: []string{"/dev/loop*"},
},
},
agentDesiredConfigs: map[string]connectionAgentDesiredConfig{
"agent-1": desiredConfig,
},
now: now,
}
got := buildConnections(in)
if len(got) != 1 {
t.Fatalf("expected 1 connection, got %d", len(got))
}
drift := got[0].Fleet.ConfigDrift
if drift == nil {
t.Fatal("expected agent config drift metadata")
}
if drift.Status != fleetStatePending {
t.Fatalf("config drift status = %q, want pending", drift.Status)
}
if drift.Desired == nil || *drift.Desired != desired {
t.Fatalf("desired config drift fingerprint = %+v, want %+v", drift.Desired, desired)
}
if drift.Applied != nil {
t.Fatalf("applied config fingerprint should be absent until agent reports a comparable fingerprint, got %+v", drift.Applied)
}
selfCompared := connectionConfigFingerprint(connectionAgentConfigFingerprintVersion, map[string]any{
"commandsEnabled": false,
"diskExclude": []string{"/dev/loop*"},
})
if selfCompared == nil {
t.Fatal("expected local self-comparison fingerprint to be derivable")
}
if drift.Desired.Hash == selfCompared.Hash {
t.Fatalf("desired config hash reused report-field fingerprint %q", drift.Desired.Hash)
}
if got[0].Fleet.Rollout == nil || got[0].Fleet.Rollout.Status == fleetStateCurrent {
t.Fatalf("rollout should not claim current without an applied config comparison, got %+v", got[0].Fleet.Rollout)
}
}
func TestConnectionAgentDesiredConfigFingerprintsSkipsEmptyDefaultConfig(t *testing.T) {
monitor, err := monitoring.New(&config.Config{DataPath: t.TempDir()})
if err != nil {
t.Fatalf("monitoring.New: %v", err)
}
t.Cleanup(func() { monitor.Stop() })
hostID := "default-agent"
got := connectionAgentDesiredConfigFingerprints(monitor, []models.Host{{ID: hostID}}, nil)
desired, ok := got[hostID]
if !ok {
t.Fatalf("expected resolved desired config entry for %q, got %+v", hostID, got)
}
if desired.Fingerprint != nil {
t.Fatalf("empty default config should not create rollout fingerprint, got %+v", desired.Fingerprint)
}
if desired.CommandsEnabled != nil {
t.Fatalf("empty default config should not set command override, got %+v", desired.CommandsEnabled)
}
commandsEnabled := true
if err := monitor.UpdateHostAgentConfig(hostID, &commandsEnabled); err != nil {
t.Fatalf("UpdateHostAgentConfig: %v", err)
}
got = connectionAgentDesiredConfigFingerprints(monitor, []models.Host{{ID: hostID}}, nil)
desired = got[hostID]
if desired.Fingerprint == nil {
t.Fatalf("managed command override should create desired config fingerprint")
}
if desired.CommandsEnabled == nil || !*desired.CommandsEnabled {
t.Fatalf("managed command override = %+v, want true", desired.CommandsEnabled)
}
}
func TestConnectionAgentDesiredConfigFingerprintsUsesTokenEffectiveCommandPolicy(t *testing.T) {
monitor, err := monitoring.New(&config.Config{DataPath: t.TempDir()})
if err != nil {
t.Fatalf("monitoring.New: %v", err)
}
t.Cleanup(func() { monitor.Stop() })
hostID := "agent-token-gated"
rawDesiredCommands := true
if err := monitor.UpdateHostAgentConfig(hostID, &rawDesiredCommands); err != nil {
t.Fatalf("UpdateHostAgentConfig: %v", err)
}
now := time.Now()
host := models.Host{
ID: hostID,
Hostname: "agent-token-gated.local",
LastSeen: now,
CommandsEnabled: false,
TokenID: "runtime-without-exec",
}
tokens := []config.APITokenRecord{
{
ID: "runtime-without-exec",
Scopes: []string{config.ScopeAgentReport, config.ScopeAgentConfigRead},
},
}
got := connectionAgentDesiredConfigFingerprints(monitor, []models.Host{host}, tokens)
desired, ok := got[hostID]
if !ok {
t.Fatalf("expected resolved desired config entry for %q, got %+v", hostID, got)
}
if desired.CommandsEnabled == nil || *desired.CommandsEnabled {
t.Fatalf("effective desired CommandsEnabled = %+v, want false", desired.CommandsEnabled)
}
if desired.Fingerprint == nil {
t.Fatal("effective command override should still create a desired config fingerprint")
}
effectiveDisabled := false
effectiveMetadata, err := remoteconfig.BuildDesiredConfigMetadata(&effectiveDisabled, nil)
if err != nil {
t.Fatalf("BuildDesiredConfigMetadata disabled: %v", err)
}
rawMetadata, err := remoteconfig.BuildDesiredConfigMetadata(&rawDesiredCommands, nil)
if err != nil {
t.Fatalf("BuildDesiredConfigMetadata raw: %v", err)
}
if desired.Fingerprint.Hash != effectiveMetadata.Hash {
t.Fatalf("desired fingerprint hash = %q, want effective disabled hash %q", desired.Fingerprint.Hash, effectiveMetadata.Hash)
}
if desired.Fingerprint.Hash == rawMetadata.Hash {
t.Fatalf("desired fingerprint reused raw unsanitized command policy hash %q", desired.Fingerprint.Hash)
}
connections := buildConnections(aggregatorInputs{
hosts: []models.Host{host},
agentDesiredConfigs: got,
now: now,
})
if len(connections) != 1 {
t.Fatalf("expected one connection, got %d", len(connections))
}
policy := connections[0].Fleet.CommandPolicy
if policy == nil ||
policy.Status != fleetStateDisabled ||
policy.Desired != fleetStateDisabled ||
policy.Applied != fleetStateDisabled ||
policy.Enforcement != fleetCommandPolicyInSync {
t.Fatalf("command policy = %+v, want effective disabled in sync", policy)
}
}
func TestConnectionFleetAgentConfigDriftComparesAppliedFingerprintsWhenAvailable(t *testing.T) {
now := time.Now()
conn := Connection{
Type: ConnectionTypeAgent,
State: ConnectionStateActive,
Enabled: true,
LastSeen: &now,
}
desired := &ConnectionFleetConfigFingerprint{Version: connectionAgentConfigFingerprintVersion, Hash: "sha256:desired"}
applied := &ConnectionFleetConfigFingerprint{Version: connectionAgentConfigFingerprintVersion, Hash: "sha256:applied"}
drifted := connectionFleetAgentConfigDriftForFingerprints(conn, desired, applied)
if drifted.Status != fleetConfigDriftDrifted ||
drifted.Desired != desired ||
drifted.Applied != applied ||
drifted.LastObservedAt == nil {
t.Fatalf("drifted config comparison = %+v", drifted)
}
matchingApplied := &ConnectionFleetConfigFingerprint{Version: desired.Version, Hash: desired.Hash}
current := connectionFleetAgentConfigDriftForFingerprints(conn, desired, matchingApplied)
if current.Status != fleetConfigDriftCurrent ||
current.Desired != desired ||
current.Applied != matchingApplied ||
current.LastObservedAt == nil {
t.Fatalf("current config comparison = %+v", current)
}
}
func TestBuildConnections_PlatformFleetGovernance(t *testing.T) {
now := time.Now()
lastSuccess := now.Add(-30 * time.Second)
in := aggregatorInputs{
pveInstances: []config.PVEInstance{
{Name: "healthy", Host: "https://healthy.lan:8006"},
{Name: "bad-token", Host: "https://bad-token.lan:8006"},
{Name: "paused", Host: "https://paused.lan:8006", Disabled: true},
},
instanceHealth: map[string]monitoring.InstanceHealth{
"pve::healthy": healthEntry(&lastSuccess, "", "", "closed"),
"pve::bad-token": healthEntry(&lastSuccess, "403 forbidden", "auth", "closed"),
},
now: now,
}
got := buildConnections(in)
byID := map[string]Connection{}
for _, connection := range got {
byID[connection.ID] = connection
}
healthy := byID["pve:healthy"].Fleet
if healthy.EnrollmentState != fleetStateConfigured ||
healthy.LivenessState != fleetStateActive ||
healthy.VersionDrift != fleetStateNotApplicable ||
healthy.AdapterHealth != fleetStateHealthy ||
healthy.ConfigRollout != fleetStateConfigured ||
healthy.CredentialStatus != fleetStateVerified ||
healthy.UpdateStatus != fleetStateNotApplicable ||
healthy.RemoteControl != fleetStateNotApplicable {
t.Fatalf("healthy platform fleet governance = %+v", healthy)
}
if healthy.ConfigDrift == nil ||
healthy.ConfigDrift.Status != fleetConfigDriftCurrent ||
healthy.ConfigDrift.Desired == nil ||
healthy.ConfigDrift.Applied == nil ||
healthy.ConfigDrift.Desired.Hash != healthy.ConfigDrift.Applied.Hash {
t.Fatalf("healthy platform config drift = %+v", healthy.ConfigDrift)
}
if healthy.Rollout == nil || healthy.Rollout.Status != fleetStateCurrent {
t.Fatalf("healthy platform rollout = %+v", healthy.Rollout)
}
if healthy.CredentialHealth == nil || healthy.CredentialHealth.Status != fleetStateVerified {
t.Fatalf("healthy platform credential health = %+v", healthy.CredentialHealth)
}
badToken := byID["pve:bad-token"].Fleet
if badToken.AdapterHealth != fleetStateBlocked ||
badToken.CredentialStatus != fleetStateInvalid ||
badToken.LivenessState != string(ConnectionStateUnauthorized) {
t.Fatalf("bad token fleet governance = %+v", badToken)
}
if badToken.CredentialHealth == nil ||
badToken.CredentialHealth.Status != fleetStateInvalid ||
badToken.CredentialHealth.LastFailedAt == nil {
t.Fatalf("bad token credential health = %+v", badToken.CredentialHealth)
}
if badToken.Rollout == nil || badToken.Rollout.Status != fleetRolloutBlocked {
t.Fatalf("bad token rollout = %+v", badToken.Rollout)
}
paused := byID["pve:paused"].Fleet
if paused.EnrollmentState != fleetStatePaused ||
paused.AdapterHealth != fleetStatePaused ||
paused.ConfigRollout != fleetStatePaused ||
paused.CredentialStatus != fleetStatePaused {
t.Fatalf("paused platform fleet governance = %+v", paused)
}
if paused.ConfigDrift == nil || paused.ConfigDrift.Status != fleetStatePaused {
t.Fatalf("paused config drift = %+v", paused.ConfigDrift)
}
if paused.Rollout == nil || paused.Rollout.Status != fleetStatePaused {
t.Fatalf("paused rollout = %+v", paused.Rollout)
}
}
func TestBuildConnections_VMwareAndTrueNASEnabledFlag(t *testing.T) {
in := aggregatorInputs{
vmwareInstances: []config.VMwareVCenterInstance{{
ID: "vc1", Name: "vc", Host: "vc.lan", Enabled: false,
MonitorVMs: true, MonitorHosts: true, MonitorDatastores: false,
}},
truenasInstances: []config.TrueNASInstance{{
ID: "tn1", Name: "tn", Host: "tn.lan", Enabled: true, UseHTTPS: true,
MonitorDatasets: true, MonitorPools: false, MonitorReplication: true,
}},
now: time.Now(),
}
got := buildConnections(in)
var vmw, tn Connection
for _, c := range got {
switch c.Type {
case ConnectionTypeVMware:
vmw = c
case ConnectionTypeTrueNAS:
tn = c
}
}
if vmw.State != ConnectionStatePaused || vmw.Enabled {
t.Fatalf("vmware with Enabled=false should be paused, got state=%q enabled=%v", vmw.State, vmw.Enabled)
}
if !vmw.Capabilities.SupportsScope {
t.Fatal("vmware connections must advertise scope capability")
}
if vmw.Scope["datastores"] || !vmw.Scope["vms"] || !vmw.Scope["hosts"] {
t.Fatalf("vmware scope map should reflect Monitor* fields, got %+v", vmw.Scope)
}
if tn.State != ConnectionStatePending {
t.Fatalf("truenas with no health yet should be pending, got %q", tn.State)
}
if !tn.Enabled {
t.Fatal("truenas with Enabled=true should surface enabled=true")
}
if !tn.Capabilities.SupportsScope {
t.Fatal("truenas connections must advertise scope capability")
}
if tn.Scope["pools"] || !tn.Scope["datasets"] || !tn.Scope["replication"] {
t.Fatalf("truenas scope map should reflect Monitor* fields, got %+v", tn.Scope)
}
}
func TestBuildConnections_VMwareProjectsSanitizedInventoryCompleteness(t *testing.T) {
in := aggregatorInputs{
vmwareInstances: []config.VMwareVCenterInstance{{
ID: "vc1", Name: "vc", Host: "vc.lan", Enabled: true, MonitorVMs: true,
}},
vmwareSummaries: map[string]monitoring.VMwareConnectionSummary{
"vc1": {
Observed: &monitoring.VMwareConnectionObservedSummary{
Degraded: true,
Issues: []monitoring.VMwareConnectionObservedIssue{{
Stage: "guest-details",
Category: "permission",
Message: "vm-42 at https://vcenter.secret.local/sdk denied",
}},
},
},
},
now: time.Now(),
}
var vmwareConnection Connection
for _, connection := range buildConnections(in) {
if connection.Type == ConnectionTypeVMware {
vmwareConnection = connection
break
}
}
completeness := vmwareConnection.inventoryCompleteness
if completeness == nil || completeness.State != "degraded" || completeness.IssueCount != 1 {
t.Fatalf("inventory completeness = %#v", completeness)
}
if len(completeness.Issues) != 1 || completeness.Issues[0].Stage != "guest-details" || completeness.Issues[0].Category != "permission" {
t.Fatalf("inventory issues = %#v", completeness.Issues)
}
}
func TestBuildConnections_PlatformPollerSummariesDriveRuntimeState(t *testing.T) {
now := time.Now().UTC()
vmwareSuccess := now.Add(-40 * time.Second)
trueNASSuccess := now.Add(-30 * time.Second)
in := aggregatorInputs{
vmwareInstances: []config.VMwareVCenterInstance{{
ID: "vc1", Name: "vc", Host: "vc.lan", Enabled: true,
MonitorVMs: true, MonitorHosts: true, MonitorDatastores: true,
}},
vmwareSummaries: map[string]monitoring.VMwareConnectionSummary{
"vc1": {
Poll: &monitoring.VMwareConnectionPollStatus{
LastSuccessAt: &vmwareSuccess,
},
},
},
truenasInstances: []config.TrueNASInstance{{
ID: "tn1", Name: "tn", Host: "tn.lan", Enabled: true, UseHTTPS: true,
APIKey: "secret", MonitorDatasets: true, MonitorPools: true, MonitorReplication: true,
}},
truenasSummaries: map[string]monitoring.TrueNASConnectionSummary{
"tn1": {
Poll: &monitoring.TrueNASConnectionPollStatus{
LastSuccessAt: &trueNASSuccess,
},
},
},
now: now,
}
got := buildConnections(in)
byID := map[string]Connection{}
for _, connection := range got {
byID[connection.ID] = connection
}
vmw := byID["vmware:vc1"]
if vmw.State != ConnectionStateActive {
t.Fatalf("vmware state = %q, want active", vmw.State)
}
if vmw.LastSeen == nil || !vmw.LastSeen.Equal(vmwareSuccess) {
t.Fatalf("vmware lastSeen = %+v, want %s", vmw.LastSeen, vmwareSuccess)
}
if vmw.Fleet.CredentialStatus != fleetStateVerified || vmw.Fleet.CredentialHealth == nil || vmw.Fleet.CredentialHealth.Status != fleetStateVerified {
t.Fatalf("vmware credential governance = %+v", vmw.Fleet)
}
tn := byID["truenas:tn1"]
if tn.State != ConnectionStateActive {
t.Fatalf("truenas state = %q, want active", tn.State)
}
if tn.LastSeen == nil || !tn.LastSeen.Equal(trueNASSuccess) {
t.Fatalf("truenas lastSeen = %+v, want %s", tn.LastSeen, trueNASSuccess)
}
if tn.Fleet.CredentialStatus != fleetStateVerified || tn.Fleet.CredentialHealth == nil || tn.Fleet.CredentialHealth.Status != fleetStateVerified {
t.Fatalf("truenas credential governance = %+v", tn.Fleet)
}
}
func TestDeriveConnectionState_FirstPollFailureIsUnreachable(t *testing.T) {
now := time.Now()
h := healthEntry(nil, "connection refused", "network", "closed")
state, reason, lastSeen, lastError := deriveConnectionState(true, h, now, 0)
if state != ConnectionStateUnreachable {
t.Fatalf("state = %q, want unreachable", state)
}
if reason != "connection refused" {
t.Fatalf("reason = %q, want connection refused", reason)
}
if lastSeen != nil {
t.Fatalf("lastSeen = %+v, want nil", lastSeen)
}
if lastError == nil || lastError.Message != "connection refused" {
t.Fatalf("lastError = %+v, want connection refused", lastError)
}
}
func TestDeriveConnectionState_CurrentFailureOverridesRecentSuccess(t *testing.T) {
now := time.Now()
lastSuccess := now.Add(-5 * time.Second)
for _, test := range []struct {
name string
message string
wantState ConnectionState
}{
{
name: "authentication rejected",
message: "401 Unauthorized: token invalid",
wantState: ConnectionStateUnauthorized,
},
{
name: "network timeout",
message: "context deadline exceeded while awaiting response headers",
wantState: ConnectionStateUnreachable,
},
} {
t.Run(test.name, func(t *testing.T) {
h := healthEntry(&lastSuccess, test.message, "transient", "closed")
state, reason, gotLastSeen, lastError := deriveConnectionState(true, h, now, 0)
if state != test.wantState {
t.Fatalf("state = %q, want %q", state, test.wantState)
}
if reason != test.message || lastError == nil || lastError.Message != test.message {
t.Fatalf("failure detail = reason %q error %+v, want %q", reason, lastError, test.message)
}
if gotLastSeen == nil || !gotLastSeen.Equal(lastSuccess) {
t.Fatalf("lastSeen = %+v, want preserved success %s", gotLastSeen, lastSuccess)
}
})
}
}
func TestBuildConnectionsKeepsDistinctPBSSystemsWithSameHostname(t *testing.T) {
now := time.Now()
lastSuccess := now.Add(-10 * time.Second)
got := buildConnections(aggregatorInputs{
pbsInstances: []config.PBSInstance{
{Name: "pbs-east", Host: "https://backup.local:8007"},
{Name: "pbs-west", Host: "https://backup.local:8007"},
},
instanceHealth: map[string]monitoring.InstanceHealth{
"pbs::pbs-east": healthEntry(&lastSuccess, "", "", "closed"),
"pbs::pbs-west": healthEntry(&lastSuccess, "dial tcp: i/o timeout", "transient", "closed"),
},
now: now,
})
if len(got) != 2 {
t.Fatalf("connections = %d, want two distinct PBS rows", len(got))
}
byID := make(map[string]Connection, len(got))
for _, connection := range got {
byID[connection.ID] = connection
}
if byID["pbs:pbs-east"].State != ConnectionStateActive {
t.Fatalf("pbs-east = %+v, want active", byID["pbs:pbs-east"])
}
if byID["pbs:pbs-west"].State != ConnectionStateUnreachable {
t.Fatalf("pbs-west = %+v, want unreachable", byID["pbs:pbs-west"])
}
alertSnapshots := snapshotConnectionsForAlerts(got)
if len(alertSnapshots) != 2 {
t.Fatalf("alert snapshots = %+v, want both PBS systems", alertSnapshots)
}
alertsByID := make(map[string]alerts.ConnectionSnapshot, len(alertSnapshots))
for _, snapshot := range alertSnapshots {
alertsByID[snapshot.ID] = snapshot
}
if alertsByID["pbs:pbs-east"].State != alerts.ConnectionStateActive {
t.Fatalf("pbs-east alert state = %+v, want active", alertsByID["pbs:pbs-east"])
}
if west := alertsByID["pbs:pbs-west"]; west.State != alerts.ConnectionStateUnreachable || west.LastError == nil {
t.Fatalf("pbs-west alert state = %+v, want unreachable with current error", west)
}
}
func TestBuildConnections_UsesHealthLookup(t *testing.T) {
now := time.Now()
ls := now.Add(-15 * time.Second)
in := aggregatorInputs{
pveInstances: []config.PVEInstance{{Name: "pve1", Host: "https://pve1.lan:8006"}},
instanceHealth: map[string]monitoring.InstanceHealth{
"pve::pve1": healthEntry(&ls, "", "", "closed"),
},
now: now,
}
got := buildConnections(in)
if got[0].State != ConnectionStateActive {
t.Fatalf("expected active, got %q", got[0].State)
}
if got[0].LastSeen == nil || !got[0].LastSeen.Equal(ls) {
t.Fatalf("lastSeen not propagated: %+v", got[0].LastSeen)
}
}
func TestConnectionStaleThresholdFor(t *testing.T) {
for _, test := range []struct {
name string
interval time.Duration
want time.Duration
}{
{name: "unknown cadence keeps floor", interval: 0, want: connectionStaleThreshold},
{name: "negative cadence keeps floor", interval: -time.Minute, want: connectionStaleThreshold},
{name: "fast cadence keeps floor", interval: 10 * time.Second, want: connectionStaleThreshold},
{name: "floor boundary cadence keeps floor", interval: 40 * time.Second, want: connectionStaleThreshold},
{name: "slow cadence scales to 3x", interval: 5 * time.Minute, want: 15 * time.Minute},
} {
t.Run(test.name, func(t *testing.T) {
if got := connectionStaleThresholdFor(test.interval); got != test.want {
t.Fatalf("connectionStaleThresholdFor(%s) = %s, want %s", test.interval, got, test.want)
}
})
}
}
// Regression for https://github.com/rcourtman/Pulse/issues/1620: with a
// 5-minute polling cadence the previous fixed 2-minute cutoff marked every
// connection permanently stale, latching the connection-degraded alert.
func TestDeriveConnectionState_SlowCadenceIsNotPermanentlyStale(t *testing.T) {
now := time.Now()
interval := 5 * time.Minute
// A last success exactly one interval old is the steady state when the
// connection check runs concurrently with the poll on the same tick; it
// must derive active, not stale.
h := healthEntry(ptrTime(now.Add(-interval)), "", "", "closed")
state, _, _, _ := deriveConnectionState(true, h, now, interval)
if state != ConnectionStateActive {
t.Fatalf("state at 1x interval = %q, want %q", state, ConnectionStateActive)
}
// Beyond 3x the interval the connection is genuinely stale.
h = healthEntry(ptrTime(now.Add(-3*interval-time.Second)), "", "", "closed")
state, _, _, _ = deriveConnectionState(true, h, now, interval)
if state != ConnectionStateStale {
t.Fatalf("state beyond 3x interval = %q, want %q", state, ConnectionStateStale)
}
}
func TestBuildConnections_ScalesStaleThresholdWithPollingIntervals(t *testing.T) {
now := time.Now()
lastSuccess := now.Add(-5 * time.Minute)
in := aggregatorInputs{
pveInstances: []config.PVEInstance{{Name: "pve1", Host: "https://pve1.lan:8006"}},
pbsInstances: []config.PBSInstance{{Name: "pbs1", Host: "https://pbs1.lan:8007"}},
truenasInstances: []config.TrueNASInstance{
{ID: "tn1", Name: "tn1", Host: "tn1.lan", Enabled: true, PollIntervalSecs: 300},
},
instanceHealth: map[string]monitoring.InstanceHealth{
"pve::pve1": healthEntry(&lastSuccess, "", "", "closed"),
"pbs::pbs1": healthEntry(&lastSuccess, "", "", "closed"),
"truenas::tn1": healthEntry(&lastSuccess, "", "", "closed"),
},
pvePollingInterval: 5 * time.Minute,
now: now,
}
got := buildConnections(in)
byID := make(map[string]Connection, len(got))
for _, conn := range got {
byID[conn.ID] = conn
}
if state := byID["pve:pve1"].State; state != ConnectionStateActive {
t.Fatalf("pve state = %q, want active (threshold scaled by pvePollingInterval)", state)
}
if state := byID["truenas:tn1"].State; state != ConnectionStateActive {
t.Fatalf("truenas state = %q, want active (threshold scaled by PollIntervalSecs)", state)
}
// PBS was left at the zero-value interval, so the 2-minute floor still
// applies and the same 5-minute-old success reads as stale.
if state := byID["pbs:pbs1"].State; state != ConnectionStateStale {
t.Fatalf("pbs state = %q, want stale under the floor threshold", state)
}
}
func TestPMGCollectionConnectionScopeMatchesLegacyAndExplicitDefaults(t *testing.T) {
for _, tc := range []struct {
name string
cfg config.PMGInstance
wantMail bool
}{
{"legacy", config.PMGInstance{Name: "gateway"}, true},
{"explicit-off", config.PMGInstance{Name: "gateway", MonitoringConfigured: true}, false},
{"queues-only", config.PMGInstance{Name: "gateway", MonitorQueues: true}, false},
} {
t.Run(tc.name, func(t *testing.T) {
got := buildPMGConnection(tc.cfg, nil, time.Now(), time.Minute)
if got.Scope["mailStats"] != tc.wantMail || got.Scope["queues"] != tc.cfg.MonitorQueues {
t.Fatalf("connection scope=%v", got.Scope)
}
})
}
}