Pulse/internal/monitoring/issue1913_host_continuity_test.go
rcourtman bd37ae186a Preserve live agent state over saved enrollment history
Saved enrollment records can correlate with a live Proxmox host after a
reinstall. The continuity overlay then replaced its current agent payload
with an older offline identity, hiding metrics and marking the node offline.

Use canonical matching to add only absent continuity resources. Preserve
current identity, telemetry and provider links without deleting history or
changing token admission. Cover repeated reads and identity collisions.

Refs #1913
2026-09-10 16:00:36 +01:00

91 lines
3.6 KiB
Go

package monitoring
import (
"testing"
"time"
"github.com/rcourtman/pulse-go-rewrite/internal/config"
"github.com/rcourtman/pulse-go-rewrite/internal/models"
"github.com/rcourtman/pulse-go-rewrite/internal/unifiedresources"
)
// A reinstall can leave an earlier enrollment in continuity while only its
// successor reports. Hydrating that history must not replace the live agent
// in Infrastructure or turn its linked Proxmox node offline (#1913).
func TestHostContinuityPreservesLiveAgentAfterReenrollment(t *testing.T) {
for _, linked := range []bool{false, true} {
name := "standalone"
if linked {
name = "proxmox"
}
t.Run(name, func(t *testing.T) {
now := time.Now().UTC()
live := models.Host{
ID: "current-enrollment", MachineID: "same-machine", Hostname: "home-pm",
Platform: "linux", Status: "online", LastSeen: now, IntervalSeconds: 30,
CPUUsage: 17, CPUCount: 8, AgentVersion: "6.4.1", TokenID: "current-token",
}
state := models.NewState()
if linked {
live.LinkedNodeID = "pve:home-pm"
state.Nodes = []models.Node{{
ID: live.LinkedNodeID, Name: live.Hostname, Instance: "home-pm",
Host: "https://192.0.2.149:8006", Status: "online", LastSeen: now,
LinkedAgentID: live.ID,
}}
}
state.Hosts = []models.Host{live}
continuity := config.NewHostContinuityStore(t.TempDir(), nil)
for _, entry := range []config.HostContinuityEntry{
{HostID: "retired-enrollment", MachineID: live.MachineID, Hostname: live.Hostname, Platform: "linux", LastSeen: now.Add(-time.Minute), TokenID: "retired-token"},
{HostID: "absent-machine", MachineID: "other-machine", Hostname: "other-host", Platform: "linux", LastSeen: now.Add(-time.Hour)},
} {
if err := continuity.Upsert(entry); err != nil {
t.Fatal(err)
}
}
registry := unifiedresources.NewRegistry(nil)
registry.IngestSnapshot(state.GetSnapshot())
adapter := unifiedresources.NewMonitorAdapter(registry)
m := &Monitor{state: state, resourceStore: adapter, hostContinuityStore: continuity}
for round := 0; round < 3; round++ {
hosts := m.HostsSnapshot()
if len(hosts) != 2 {
t.Fatalf("host count = %d, want live machine plus absent historical machine", len(hosts))
}
var foundLive, foundAbsent bool
for _, host := range hosts {
switch host.ID {
case live.ID:
foundLive = true
if host.Status != "online" || host.CPUUsage != live.CPUUsage || host.TokenID != live.TokenID || host.LinkedNodeID != live.LinkedNodeID || !host.LastSeen.Equal(now) {
t.Fatalf("round %d: history changed live observation: %+v", round, host)
}
case "absent-machine":
foundAbsent = true
if host.Status == "online" || !host.LastSeen.Equal(now.Add(-time.Hour)) {
t.Fatalf("historical machine was presented as a live sighting: %+v", host)
}
default:
t.Fatalf("round %d: retired identity replaced current enrollment: %s", round, host.ID)
}
}
if !foundLive || !foundAbsent {
t.Fatal("lost live observation or absent-machine continuity")
}
if linked {
nodes := m.NodesSnapshot()
if len(nodes) != 1 || nodes[0].Status != "online" || nodes[0].LinkedAgentID != live.ID {
t.Fatalf("round %d: history changed Proxmox status or agent link: %+v", round, nodes)
}
}
}
if len(adapter.Hosts()) != 1 || adapter.Hosts()[0].AgentID() != live.ID {
t.Fatal("read-time hydration mutated the live registry")
}
if len(continuity.RecentEntries(now.Add(-24*time.Hour))) != 2 {
t.Fatal("read-time hydration deleted retained enrollment history")
}
})
}
}