Files
pulse/internal/monitoring/guest_memory_stability_test.go
T
courtmanr@gmail.com 1dc19bfec0 Remove dead cache-aware RRD fields from the guest RRD path
Recorded PVE 8 and PVE 9 guest rrddata responses (fixtures under
pkg/proxmox/testdata/rrd/) prove guest RRD never carries the cache-aware
memused/memavailable columns — they exist only in node RRD — so every
consumer branch reading them was dead code that #1634's listing fallback
(7d7d2b6a3) had already routed around.

Drop the two fields from GuestRRDPoint (now time/maxmem only, matching
the recordings), delete the dead VM RRD memory fallback and its
getVMRRDMetrics/getVMRRDMemory helpers plus the vmRRDMemCache they fed,
remove the pointless per-poll guest RRD fetch from the LXC memory path,
and retire the guest RRD lookups from PVEClientInterface. VMMemoryRaw
loses its never-populated RRD diagnostic fields, and guest reliability
scoring no longer treats the node-only rrd-* sources as trusted guest
evidence. The knownDeadGuestRRDFields allowlist in the fixture
alignment test is gone; a new reflection guard in
code_standards_test.go keeps GuestRRDPoint pinned to recorded columns,
and cleanupRRDCache pruning of the guest-agent meminfo cache gains
direct coverage.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 18:36:50 +01:00

232 lines
6.5 KiB
Go

package monitoring
import (
"context"
"testing"
"time"
"github.com/rcourtman/pulse-go-rewrite/internal/models"
"github.com/rcourtman/pulse-go-rewrite/pkg/proxmox"
)
func TestStabilizeGuestLowTrustMemoryCarriesForwardTrustedSnapshot(t *testing.T) {
const gib = uint64(1024 * 1024 * 1024)
now := time.Now()
prev := &GuestMemorySnapshot{
Status: "running",
RetrievedAt: now.Add(-time.Minute),
MemorySource: "guest-agent-meminfo",
Memory: models.Memory{
Total: 8 * int64(gib),
Used: 3 * int64(gib),
Usage: safePercentage(float64(3*gib), float64(8*gib)),
},
}
used, source, notes := stabilizeGuestLowTrustMemory(prev, "running", "status-freemem", 8*gib, 8*gib, now, false)
if used != 3*gib {
t.Fatalf("used = %d, want %d", used, 3*gib)
}
if source != "previous-snapshot" {
t.Fatalf("source = %q, want previous-snapshot", source)
}
if len(notes) != 1 || notes[0] != "preserved-previous-memory-after-repeated-low-trust-pattern" {
t.Fatalf("notes = %#v, want repeated low-trust carry-forward note", notes)
}
}
func TestStabilizeGuestLowTrustMemoryCarriesForwardAcrossUnavailableReconnect(t *testing.T) {
const gib = uint64(1024 * 1024 * 1024)
now := time.Now()
prev := &GuestMemorySnapshot{
Status: "running",
RetrievedAt: now.Add(-30 * time.Second),
MemorySource: "guest-agent-meminfo",
Memory: models.Memory{
Total: 8 * int64(gib),
Used: 3 * int64(gib),
Free: 5 * int64(gib),
Usage: 37.5,
},
}
used, source, notes := stabilizeGuestLowTrustMemory(prev, "running", "unavailable", 8*gib, 0, now, false)
if used != 3*gib {
t.Fatalf("used = %d, want %d", used, 3*gib)
}
if source != "previous-snapshot" {
t.Fatalf("source = %q, want previous-snapshot", source)
}
if len(notes) != 1 {
t.Fatalf("notes = %#v, want carry-forward diagnostic", notes)
}
}
func TestStabilizeGuestLowTrustMemoryUsesHealthyGuestAgentEvidence(t *testing.T) {
const gib = uint64(1024 * 1024 * 1024)
now := time.Now()
prev := &GuestMemorySnapshot{
Status: "running",
RetrievedAt: now.Add(-2 * time.Minute),
MemorySource: "previous-snapshot",
Memory: models.Memory{
Total: 8 * int64(gib),
Used: 3 * int64(gib),
Usage: safePercentage(float64(3*gib), float64(8*gib)),
},
}
used, source, notes := stabilizeGuestLowTrustMemory(prev, "running", "cluster-resources", 8*gib, 8*gib, now, true)
if used != 3*gib {
t.Fatalf("used = %d, want %d", used, 3*gib)
}
if source != "previous-snapshot" {
t.Fatalf("source = %q, want previous-snapshot", source)
}
if len(notes) != 1 || notes[0] != "preserved-previous-memory-for-healthy-guest-low-trust-full-usage" {
t.Fatalf("notes = %#v, want healthy guest carry-forward note", notes)
}
}
func TestHandleClusterVMResourcePreservesHealthyGuestMemoryFromPreviousSnapshot(t *testing.T) {
t.Setenv("PULSE_DATA_DIR", t.TempDir())
const gib = uint64(1024 * 1024 * 1024)
mon := newTestPVEMonitor("test")
defer mon.alertManager.Stop()
defer mon.notificationMgr.Stop()
previousSample := time.Now().Add(-time.Minute)
mon.recordGuestSnapshot("test", "qemu", "node1", 101, GuestMemorySnapshot{
Name: "vm-101",
Status: "running",
RetrievedAt: previousSample,
MemorySource: "previous-snapshot",
Memory: models.Memory{
Total: 8 * int64(gib),
Used: 3 * int64(gib),
Free: 5 * int64(gib),
Usage: safePercentage(float64(3*gib), float64(8*gib)),
},
})
client := &vmMemoryTrustStubClient{
stubPVEClient: &stubPVEClient{},
vmStatus: &proxmox.VMStatus{
Status: "running",
MaxMem: 8 * gib,
Mem: 8 * gib,
Agent: proxmox.VMAgentField{Value: 1},
},
}
vm, ok := mon.handleClusterVMResource(
context.Background(),
"test",
proxmox.ClusterResource{
ID: "qemu/101",
Type: "qemu",
Node: "node1",
Name: "vm-101",
Status: "running",
VMID: 101,
MaxMem: 8 * gib,
Mem: 8 * gib,
MaxCPU: 2,
},
makeGuestID("test", "node1", 101),
client,
nil,
nil,
)
if !ok {
t.Fatal("handleClusterVMResource() returned ok=false")
}
if got := uint64(vm.Memory.Used); got != 3*gib {
t.Fatalf("vm.Memory.Used = %d, want %d", got, 3*gib)
}
key := makeGuestSnapshotKey("test", "qemu", "node1", 101)
snap, ok := mon.guestSnapshots[key]
if !ok {
t.Fatalf("expected guest snapshot %q to be recorded", key)
}
if snap.MemorySource != "previous-snapshot" {
t.Fatalf("snapshot.MemorySource = %q, want previous-snapshot", snap.MemorySource)
}
if len(snap.Notes) != 1 || snap.Notes[0] != "preserved-previous-memory-for-healthy-guest-low-trust-full-usage" {
t.Fatalf("snapshot.Notes = %#v, want healthy guest carry-forward note", snap.Notes)
}
}
func TestPollVMsWithNodesPreservesHealthyGuestMemoryFromPreviousSnapshot(t *testing.T) {
t.Setenv("PULSE_DATA_DIR", t.TempDir())
const gib = uint64(1024 * 1024 * 1024)
mon := newTestPVEMonitor("test")
defer mon.alertManager.Stop()
defer mon.notificationMgr.Stop()
previousSample := time.Now().Add(-time.Minute)
mon.recordGuestSnapshot("test", "qemu", "node1", 101, GuestMemorySnapshot{
Name: "vm-101",
Status: "running",
RetrievedAt: previousSample,
MemorySource: "previous-snapshot",
Memory: models.Memory{
Total: 8 * int64(gib),
Used: 3 * int64(gib),
Free: 5 * int64(gib),
Usage: safePercentage(float64(3*gib), float64(8*gib)),
},
})
client := &vmMemoryTrustStubClient{
stubPVEClient: &stubPVEClient{},
vms: []proxmox.VM{{
VMID: 101,
Name: "vm-101",
Node: "node1",
Status: "running",
MaxMem: 8 * gib,
Mem: 8 * gib,
CPUs: 2,
}},
vmStatus: &proxmox.VMStatus{
Status: "running",
MaxMem: 8 * gib,
Mem: 8 * gib,
Agent: proxmox.VMAgentField{Value: 1},
},
}
nodes := []proxmox.Node{{Node: "node1", Status: "online"}}
nodeEffectiveStatus := map[string]string{"node1": "online"}
mon.pollVMsWithNodes(context.Background(), "test", "", false, client, nodes, nodeEffectiveStatus)
vms := mon.state.GetSnapshot().VMs
if len(vms) != 1 {
t.Fatalf("expected 1 VM, got %d", len(vms))
}
if got := uint64(vms[0].Memory.Used); got != 3*gib {
t.Fatalf("vms[0].Memory.Used = %d, want %d", got, 3*gib)
}
key := makeGuestSnapshotKey("test", "qemu", "node1", 101)
snap, ok := mon.guestSnapshots[key]
if !ok {
t.Fatalf("expected guest snapshot %q to be recorded", key)
}
if snap.MemorySource != "previous-snapshot" {
t.Fatalf("snapshot.MemorySource = %q, want previous-snapshot", snap.MemorySource)
}
if len(snap.Notes) != 1 || snap.Notes[0] != "preserved-previous-memory-for-healthy-guest-low-trust-full-usage" {
t.Fatalf("snapshot.Notes = %#v, want healthy guest carry-forward note", snap.Notes)
}
}