Files
pulse/internal/monitoring/monitor_metadata_test.go
T
pulse-triage[bot] 244d322dce Coalesce guest identity writes and retain incomplete tenant shutdown
Admit detached metadata in order, merge identity fields without restoring stale
operator edits, and persist complete snapshots with one owned writer. Seal late
writes and return shutdown failures before organisation data or offboarding is
removed. Preserve existing drain budgets, file format, tenant boundaries and
atomic persistence. Add controlled burst, ordering, write-error, blocked-close
and real deletion controls.

Contract-Neutral: Behavioural repair of existing JSON persistence and organisation deletion; no route, field, permission, identity format, schema or provider authority change. Shutdown failure uses the existing HTTP error envelope. Internal ownership methods and explanatory contract text change, not the public contract shape.
Change-source: pulse-maintainer
2026-10-05 09:21:40 +01:00

208 lines
6.8 KiB
Go

package monitoring
import (
"os"
"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/unifiedresources"
"github.com/rcourtman/pulse-go-rewrite/pkg/proxmox"
)
func TestPersistGuestIdentity_Concurrent(t *testing.T) {
// Setup temporary metadata store
tmpDir, err := os.MkdirTemp("", "persist_test")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tmpDir)
store := config.NewGuestMetadataStore(tmpDir, nil)
guestKey := "pve1:node1:100"
// Test basic persistence
persistGuestIdentity(store, guestKey, "VM 100", "qemu")
if !store.WaitForPendingWrites(time.Second) {
t.Fatal("identity writer did not drain")
}
meta := store.Get(guestKey)
if meta == nil || meta.LastKnownName != "VM 100" || meta.LastKnownType != "qemu" {
t.Errorf("Failed to persist guest identity: %+v", meta)
}
// Test persistence with "downgrade" prevention
// Set type to "oci" manually first
ociMeta := &config.GuestMetadata{
ID: guestKey,
LastKnownName: "VM 100",
LastKnownType: "oci",
}
_ = store.Set(guestKey, ociMeta)
// Try to update to "lxc"
persistGuestIdentity(store, guestKey, "VM 100", "lxc")
if !store.WaitForPendingWrites(time.Second) {
t.Fatal("identity writer did not drain")
}
meta = store.Get(guestKey)
if meta.LastKnownType != "oci" {
t.Errorf("Should ensure type 'oci' is preserved, got '%s'", meta.LastKnownType)
}
// Test persistence that shouldn't happen (no change) -> coverage of the if check
// Should not trigger Set()
persistGuestIdentity(store, guestKey, "VM 100", "oci")
if err := store.Close(time.Second); err != nil {
t.Fatal(err)
}
}
func TestEnrichWithPersistedMetadata_Detail(t *testing.T) {
tmpDir, err := os.MkdirTemp("", "enrich_test")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tmpDir)
store := config.NewGuestMetadataStore(tmpDir, nil)
// 1. Add some metadata
_ = store.Set("pve1:node1:100", &config.GuestMetadata{ID: "pve1:node1:100", LastKnownName: "PersistedVM", LastKnownType: "qemu"})
_ = store.Set("pve1:node1:101", &config.GuestMetadata{ID: "pve1:node1:101", LastKnownName: "LiveVM", LastKnownType: "qemu"})
_ = store.Set("invalid:key", &config.GuestMetadata{ID: "invalid:key", LastKnownName: "BadKey", LastKnownType: "qemu"}) // coverage for bad key
_ = store.Set("pve1:node1:badid", &config.GuestMetadata{ID: "pve1:node1:badid", LastKnownName: "BadID", LastKnownType: "qemu"}) // coverage for atoi error
// 2. Setup existing lookup
lookup := make(map[string][]alerts.GuestLookup)
// VM 101 is live
lookup["101"] = []alerts.GuestLookup{
{Name: "LiveVM", Instance: "pve1", Node: "node1", VMID: 101},
}
// 3. Run enrich
enrichWithPersistedMetadata(store, lookup)
// 4. Verify
// 100 should be added
if entries, ok := lookup["100"]; !ok || len(entries) != 1 {
t.Error("Expected VM 100 to be enriched")
} else {
if entries[0].Name != "PersistedVM" {
t.Errorf("Expected name PersistedVM, got %s", entries[0].Name)
}
}
// 101 should not be duplicated (it was live)
if len(lookup["101"]) != 1 {
t.Error("VM 101 should not be duplicated")
}
}
func TestBuildGuestLookupsFromReadState_PrefersCanonicalWorkloads(t *testing.T) {
tmpDir, err := os.MkdirTemp("", "guest_lookup_read_state")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tmpDir)
store := config.NewGuestMetadataStore(tmpDir, nil)
registry := unifiedresources.NewRegistry(nil)
registry.IngestSnapshot(models.StateSnapshot{
VMs: []models.VM{{
ID: "vm-1",
Name: "canonical-vm",
Instance: "pve1",
Node: "node1",
VMID: 100,
Type: "qemu",
}},
Containers: []models.Container{{
ID: "ct-1",
Name: "canonical-ct",
Instance: "pve1",
Node: "node2",
VMID: 101,
Type: "oci",
IsOCI: true,
}},
})
byKey, byVMID := buildGuestLookupsFromReadState(registry, store)
vmKey := alerts.BuildGuestKey("pve1", "node1", 100)
if got := byKey[vmKey]; got.Name != "canonical-vm" || got.Type != "qemu" {
t.Fatalf("expected canonical vm lookup, got %+v", got)
}
ctKey := alerts.BuildGuestKey("pve1", "node2", 101)
if got := byKey[ctKey]; got.Name != "canonical-ct" || got.Type != "oci" {
t.Fatalf("expected canonical container lookup, got %+v", got)
}
if len(byVMID["100"]) != 1 || byVMID["100"][0].Name != "canonical-vm" {
t.Fatalf("expected vmid lookup for VM 100, got %+v", byVMID["100"])
}
if len(byVMID["101"]) != 1 || byVMID["101"][0].Name != "canonical-ct" {
t.Fatalf("expected vmid lookup for container 101, got %+v", byVMID["101"])
}
}
func TestBuildGuestLookupsFromReadState_UsesPersistedMetadataWhenCanonicalStateEmpty(t *testing.T) {
tmpDir, err := os.MkdirTemp("", "guest_lookup_persisted")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tmpDir)
store := config.NewGuestMetadataStore(tmpDir, nil)
_ = store.Set("pve1:node1:100", &config.GuestMetadata{ID: "pve1:node1:100", LastKnownName: "persisted-vm", LastKnownType: "qemu"})
byKey, byVMID := buildGuestLookupsFromReadState(unifiedresources.NewRegistry(nil), store)
if len(byKey) != 0 {
t.Fatalf("expected no direct byKey entries without live canonical workloads, got %+v", byKey)
}
if len(byVMID["100"]) != 1 || byVMID["100"][0].Name != "persisted-vm" || byVMID["100"][0].Type != "qemu" {
t.Fatalf("expected persisted metadata fallback, got %+v", byVMID["100"])
}
}
func TestPVEBackupTemplateInventoryScopeFromClusterResources(t *testing.T) {
m := &Monitor{}
m.updatePVEBackupTemplateSubjectsFromClusterResources("pve-a", []proxmox.ClusterResource{
{Type: "qemu", Node: "node-a", VMID: 700, Template: 1},
{Type: "lxc", Node: "node-b", VMID: 701, Template: 1},
{Type: "qemu", Node: "node-c", VMID: 702, Template: 0},
})
scope := m.backupInventoryScopeForAlerts()
if scope == nil {
t.Fatalf("expected backup inventory scope")
}
if !scope.PVEOrphanInventoryReady["pve-a"]["qemu"] {
t.Fatalf("expected qemu backup inventory to be marked ready")
}
if !scope.PVEOrphanInventoryReady["pve-a"]["lxc"] {
t.Fatalf("expected lxc backup inventory to be marked ready")
}
qemuTemplate := alerts.BuildBackupPVETemplateSubjectKey("pve-a", "qemu", "node-a", 700)
if _, exists := scope.PVETemplateSubjects[qemuTemplate]; !exists {
t.Fatalf("expected qemu template subject to be captured")
}
lxcTemplate := alerts.BuildBackupPVETemplateSubjectKey("pve-a", "lxc", "node-b", 701)
if _, exists := scope.PVETemplateSubjects[lxcTemplate]; !exists {
t.Fatalf("expected lxc template subject to be captured")
}
nonTemplate := alerts.BuildBackupPVETemplateSubjectKey("pve-a", "qemu", "node-c", 702)
if _, exists := scope.PVETemplateSubjects[nonTemplate]; exists {
t.Fatalf("did not expect non-template subject to be captured")
}
}