mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-23 12:15:42 +00:00
afc0c865d4
removeAvailabilityProjectionLocked compacted each resource's relationships in place with a Relationships[:0] filter. That is only safe if registry clones own their backing array, and they did not: cloneResource deep-copies roughly twenty-five slice and map fields but Relationships was left as the shallow copy from the struct assignment, so a clone shared the original's array. The reachable path is ordinary. GET /api/resources rebuilds a registry from Monitor.UnifiedResourceSnapshot, whose records come from GetAll clones, so the filter wrote through into the live monitor registry from an HTTP goroutine without holding that registry's lock. Every request performed unconditional same-value writes, and whenever a legacy checks edge was genuinely dropped the compaction left a duplicated trailing relationship on the live host resource. The corrupting case is the post-upgrade seed the projection exists to repair. cloneResource now clones Relationships like every other slice, and the filter builds a fresh slice so correctness no longer depends on each caller having cloned first. Regression coverage: - TestCloneResourceGivesRelationshipsTheirOwnBackingArray pins the clone isolation, and fails with the clone line removed - TestAvailabilityRepeatedProjectionKeepsHostRelationshipsStable drives the real ingest path across repeated re-projection
763 lines
24 KiB
Go
763 lines
24 KiB
Go
package unifiedresources
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/models"
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/storagehealth"
|
|
)
|
|
|
|
// --- cloneResource: top-level isolation ---
|
|
|
|
func TestCloneResource_Nil(t *testing.T) {
|
|
out := cloneResource(nil)
|
|
if out.ID != "" {
|
|
t.Error("clone of nil should produce zero Resource")
|
|
}
|
|
}
|
|
|
|
func TestCloneResourcePtr_Nil(t *testing.T) {
|
|
if cloneResourcePtr(nil) != nil {
|
|
t.Error("cloneResourcePtr(nil) should return nil")
|
|
}
|
|
}
|
|
|
|
func TestCloneResource_MutateOriginalSlice(t *testing.T) {
|
|
original := &Resource{
|
|
ID: "r-1",
|
|
Name: "vm-100",
|
|
Tags: []string{"prod", "web"},
|
|
Sources: []DataSource{SourceProxmox, SourceAgent},
|
|
}
|
|
cloned := cloneResource(original)
|
|
|
|
// Mutate the cloned tags.
|
|
cloned.Tags[0] = "MUTATED"
|
|
if original.Tags[0] == "MUTATED" {
|
|
t.Error("mutating cloned Tags should not affect original")
|
|
}
|
|
|
|
// Mutate cloned sources.
|
|
cloned.Sources[0] = SourceDocker
|
|
if original.Sources[0] == SourceDocker {
|
|
t.Error("mutating cloned Sources should not affect original")
|
|
}
|
|
}
|
|
|
|
func TestCloneResource_MutateOriginalIdentity(t *testing.T) {
|
|
original := &Resource{
|
|
ID: "r-1",
|
|
Identity: ResourceIdentity{
|
|
Hostnames: []string{"host1", "host2"},
|
|
IPAddresses: []string{"10.0.0.1"},
|
|
MACAddresses: []string{"aa:bb:cc:dd:ee:ff"},
|
|
},
|
|
}
|
|
cloned := cloneResource(original)
|
|
|
|
cloned.Identity.Hostnames[0] = "MUTATED"
|
|
if original.Identity.Hostnames[0] == "MUTATED" {
|
|
t.Error("mutating cloned Identity.Hostnames should not affect original")
|
|
}
|
|
|
|
cloned.Identity.IPAddresses[0] = "MUTATED"
|
|
if original.Identity.IPAddresses[0] == "MUTATED" {
|
|
t.Error("mutating cloned Identity.IPAddresses should not affect original")
|
|
}
|
|
}
|
|
|
|
func TestCloneResource_MutateOriginalMetrics(t *testing.T) {
|
|
used := int64(500)
|
|
total := int64(1000)
|
|
original := &Resource{
|
|
ID: "r-1",
|
|
Metrics: &ResourceMetrics{
|
|
CPU: &MetricValue{Value: 42, Used: &used, Total: &total},
|
|
},
|
|
}
|
|
cloned := cloneResource(original)
|
|
|
|
// Mutate the pointer values on the clone.
|
|
*cloned.Metrics.CPU.Used = 999
|
|
if *original.Metrics.CPU.Used != 500 {
|
|
t.Error("mutating cloned Metrics.CPU.Used should not affect original")
|
|
}
|
|
|
|
cloned.Metrics.CPU.Value = 99
|
|
if original.Metrics.CPU.Value == 99 {
|
|
t.Error("mutating cloned Metrics.CPU.Value should not affect original")
|
|
}
|
|
}
|
|
|
|
func TestCloneResource_MutateIncidents(t *testing.T) {
|
|
original := &Resource{
|
|
ID: "r-1",
|
|
Incidents: []ResourceIncident{
|
|
{Code: "raid_degraded", Severity: storagehealth.RiskCritical, Summary: "Degraded"},
|
|
},
|
|
}
|
|
cloned := cloneResource(original)
|
|
|
|
cloned.Incidents[0].Summary = "MUTATED"
|
|
if original.Incidents[0].Summary == "MUTATED" {
|
|
t.Error("mutating cloned Incidents should not affect original")
|
|
}
|
|
}
|
|
|
|
func TestCloneResource_MutateSourceStatusMap(t *testing.T) {
|
|
original := &Resource{
|
|
ID: "r-1",
|
|
SourceStatus: map[DataSource]SourceStatus{
|
|
SourceProxmox: {Status: "online"},
|
|
},
|
|
}
|
|
cloned := cloneResource(original)
|
|
|
|
cloned.SourceStatus[SourceDocker] = SourceStatus{Status: "online"}
|
|
if _, exists := original.SourceStatus[SourceDocker]; exists {
|
|
t.Error("adding to cloned SourceStatus should not affect original")
|
|
}
|
|
}
|
|
|
|
func TestCloneResource_MutateParentID(t *testing.T) {
|
|
parentID := "parent-1"
|
|
original := &Resource{
|
|
ID: "r-1",
|
|
ParentID: &parentID,
|
|
}
|
|
cloned := cloneResource(original)
|
|
|
|
*cloned.ParentID = "MUTATED"
|
|
if *original.ParentID == "MUTATED" {
|
|
t.Error("mutating cloned ParentID should not affect original")
|
|
}
|
|
}
|
|
|
|
func TestCloneResource_MutateParentBySource(t *testing.T) {
|
|
original := &Resource{
|
|
ID: "r-1",
|
|
parentBySource: map[DataSource]string{
|
|
SourceProxmox: "parent-pve",
|
|
},
|
|
}
|
|
cloned := cloneResource(original)
|
|
|
|
cloned.parentBySource[SourceAgent] = "parent-agent"
|
|
if _, exists := original.parentBySource[SourceAgent]; exists {
|
|
t.Error("mutating cloned parentBySource should not affect original")
|
|
}
|
|
}
|
|
|
|
func TestCloneResource_MutateAvailabilityTimes(t *testing.T) {
|
|
checkedAt := time.Date(2026, time.July, 9, 12, 0, 0, 0, time.UTC)
|
|
succeededAt := checkedAt.Add(-time.Minute)
|
|
original := &Resource{
|
|
ID: "availability-router",
|
|
Availability: &AvailabilityData{
|
|
LastChecked: &checkedAt,
|
|
LastSuccess: &succeededAt,
|
|
},
|
|
AvailabilityChecks: []AvailabilityData{{
|
|
TargetID: "probe-1",
|
|
LastChecked: &checkedAt,
|
|
LastSuccess: &succeededAt,
|
|
}},
|
|
}
|
|
cloned := cloneResource(original)
|
|
|
|
*cloned.Availability.LastChecked = checkedAt.Add(time.Hour)
|
|
*cloned.Availability.LastSuccess = succeededAt.Add(time.Hour)
|
|
*cloned.AvailabilityChecks[0].LastChecked = checkedAt.Add(2 * time.Hour)
|
|
if !original.Availability.LastChecked.Equal(checkedAt) {
|
|
t.Error("mutating cloned availability LastChecked should not affect original")
|
|
}
|
|
if !original.Availability.LastSuccess.Equal(succeededAt) {
|
|
t.Error("mutating cloned availability LastSuccess should not affect original")
|
|
}
|
|
if !original.AvailabilityChecks[0].LastChecked.Equal(checkedAt) {
|
|
t.Error("mutating cloned AvailabilityChecks should not affect original")
|
|
}
|
|
}
|
|
|
|
func TestCloneResource_MutateVMwareDetailSlices(t *testing.T) {
|
|
createdAt := time.Date(2026, time.March, 30, 18, 15, 0, 0, time.UTC)
|
|
pciSlot := int64(160)
|
|
scsiBus := int64(0)
|
|
scsiUnit := int64(1)
|
|
diskCapacity := int64(107374182400)
|
|
autoUpdateSupported := true
|
|
installAttempts := int64(1)
|
|
toolsVersionNumber := int64(12352)
|
|
guestRebootRequested := true
|
|
clusterHAEnabled := true
|
|
clusterDRSEnabled := false
|
|
instantCloneFrozen := false
|
|
bootDelayMilliseconds := int64(5000)
|
|
bootRetry := true
|
|
coresPerSocket := int64(2)
|
|
memoryHotAddLimitMiB := int64(16384)
|
|
original := &Resource{
|
|
ID: "vmware-vm-1",
|
|
VMware: &VMwareData{
|
|
ClusterHAEnabled: &clusterHAEnabled,
|
|
ClusterDRSEnabled: &clusterDRSEnabled,
|
|
NetworkHostNames: []string{"esxi-01.lab.local"},
|
|
NetworkVMNames: []string{"app-01"},
|
|
SnapshotTree: []VMwareSnapshotData{{
|
|
Snapshot: "snapshot-201",
|
|
Name: "pre-upgrade",
|
|
CreatedAt: &createdAt,
|
|
Children: []VMwareSnapshotData{{
|
|
Snapshot: "snapshot-202",
|
|
Name: "post-upgrade",
|
|
}},
|
|
}},
|
|
NetworkAdapters: []VMwareNetworkAdapterData{{
|
|
NIC: "4000",
|
|
Label: "Network adapter 1",
|
|
NetworkName: "VM Network",
|
|
MACAddress: "00:50:56:aa:bb:cc",
|
|
PCISlotNumber: &pciSlot,
|
|
}},
|
|
VirtualDisks: []VMwareVirtualDiskData{{
|
|
Disk: "2000",
|
|
Label: "Hard disk 1",
|
|
Type: "SCSI",
|
|
SCSIBus: &scsiBus,
|
|
SCSIUnit: &scsiUnit,
|
|
BackingType: "VMDK_FILE",
|
|
VMDKFile: "[nvme-primary] app-01/app-01.vmdk",
|
|
DatastoreName: "nvme-primary",
|
|
CapacityBytes: &diskCapacity,
|
|
}},
|
|
Tools: &VMwareToolsData{
|
|
AutoUpdateSupported: &autoUpdateSupported,
|
|
InstallAttemptCount: &installAttempts,
|
|
VersionNumber: &toolsVersionNumber,
|
|
Version: "12.4.0",
|
|
VersionStatus: "CURRENT",
|
|
RunState: "RUNNING",
|
|
GuestRebootRequested: &guestRebootRequested,
|
|
GuestRebootComponents: []string{"tools"},
|
|
},
|
|
Hardware: &VMwareVMHardwareData{
|
|
GuestOS: "UBUNTU_64",
|
|
InstantCloneFrozen: &instantCloneFrozen,
|
|
Version: "VMX_20",
|
|
UpgradeStatus: "PENDING",
|
|
BootDelayMilliseconds: &bootDelayMilliseconds,
|
|
BootRetry: &bootRetry,
|
|
BootDevices: []VMwareBootDeviceData{{Type: "DISK", Disks: []string{"2000"}}},
|
|
CPUCoresPerSocket: &coresPerSocket,
|
|
MemoryHotAddLimitMiB: &memoryHotAddLimitMiB,
|
|
},
|
|
},
|
|
}
|
|
|
|
cloned := cloneResource(original)
|
|
cloned.VMware.SnapshotTree[0].Name = "mutated"
|
|
cloned.VMware.SnapshotTree[0].Children[0].Name = "mutated-child"
|
|
*cloned.VMware.SnapshotTree[0].CreatedAt = createdAt.Add(time.Hour)
|
|
cloned.VMware.NetworkAdapters[0].NetworkName = "mutated-network"
|
|
*cloned.VMware.NetworkAdapters[0].PCISlotNumber = 161
|
|
cloned.VMware.VirtualDisks[0].DatastoreName = "mutated-datastore"
|
|
*cloned.VMware.VirtualDisks[0].SCSIUnit = 2
|
|
*cloned.VMware.VirtualDisks[0].CapacityBytes = 1
|
|
*cloned.VMware.ClusterHAEnabled = false
|
|
*cloned.VMware.ClusterDRSEnabled = true
|
|
cloned.VMware.NetworkHostNames[0] = "mutated-host"
|
|
cloned.VMware.NetworkVMNames[0] = "mutated-vm"
|
|
*cloned.VMware.Tools.AutoUpdateSupported = false
|
|
*cloned.VMware.Tools.VersionNumber = 1
|
|
cloned.VMware.Tools.GuestRebootComponents[0] = "mutated"
|
|
*cloned.VMware.Hardware.InstantCloneFrozen = true
|
|
*cloned.VMware.Hardware.BootDelayMilliseconds = 1
|
|
cloned.VMware.Hardware.BootDevices[0].Disks[0] = "mutated"
|
|
*cloned.VMware.Hardware.MemoryHotAddLimitMiB = 1
|
|
|
|
if original.VMware.SnapshotTree[0].Name != "pre-upgrade" {
|
|
t.Fatalf("mutating cloned VMware snapshot should not affect original: %+v", original.VMware.SnapshotTree)
|
|
}
|
|
if original.VMware.SnapshotTree[0].Children[0].Name != "post-upgrade" {
|
|
t.Fatalf("mutating cloned VMware child snapshot should not affect original: %+v", original.VMware.SnapshotTree)
|
|
}
|
|
if !original.VMware.SnapshotTree[0].CreatedAt.Equal(createdAt) {
|
|
t.Fatalf("mutating cloned VMware snapshot time should not affect original: %+v", original.VMware.SnapshotTree[0].CreatedAt)
|
|
}
|
|
if original.VMware.NetworkAdapters[0].NetworkName != "VM Network" {
|
|
t.Fatalf("mutating cloned VMware adapter should not affect original: %+v", original.VMware.NetworkAdapters)
|
|
}
|
|
if original.VMware.NetworkHostNames[0] != "esxi-01.lab.local" {
|
|
t.Fatalf("mutating cloned VMware network hosts should not affect original: %+v", original.VMware.NetworkHostNames)
|
|
}
|
|
if original.VMware.NetworkVMNames[0] != "app-01" {
|
|
t.Fatalf("mutating cloned VMware network VMs should not affect original: %+v", original.VMware.NetworkVMNames)
|
|
}
|
|
if *original.VMware.NetworkAdapters[0].PCISlotNumber != 160 {
|
|
t.Fatalf("mutating cloned VMware adapter PCI slot should not affect original: %+v", original.VMware.NetworkAdapters[0].PCISlotNumber)
|
|
}
|
|
if original.VMware.VirtualDisks[0].DatastoreName != "nvme-primary" {
|
|
t.Fatalf("mutating cloned VMware disk should not affect original: %+v", original.VMware.VirtualDisks)
|
|
}
|
|
if *original.VMware.VirtualDisks[0].SCSIUnit != 1 {
|
|
t.Fatalf("mutating cloned VMware disk unit should not affect original: %+v", original.VMware.VirtualDisks[0].SCSIUnit)
|
|
}
|
|
if *original.VMware.VirtualDisks[0].CapacityBytes != 107374182400 {
|
|
t.Fatalf("mutating cloned VMware disk capacity should not affect original: %+v", original.VMware.VirtualDisks[0].CapacityBytes)
|
|
}
|
|
if original.VMware.ClusterHAEnabled == nil || !*original.VMware.ClusterHAEnabled {
|
|
t.Fatalf("mutating cloned VMware cluster HA flag should not affect original: %+v", original.VMware.ClusterHAEnabled)
|
|
}
|
|
if original.VMware.ClusterDRSEnabled == nil || *original.VMware.ClusterDRSEnabled {
|
|
t.Fatalf("mutating cloned VMware cluster DRS flag should not affect original: %+v", original.VMware.ClusterDRSEnabled)
|
|
}
|
|
if original.VMware.Tools.AutoUpdateSupported == nil || !*original.VMware.Tools.AutoUpdateSupported {
|
|
t.Fatalf("mutating cloned VMware Tools auto-update flag should not affect original: %+v", original.VMware.Tools.AutoUpdateSupported)
|
|
}
|
|
if original.VMware.Tools.VersionNumber == nil || *original.VMware.Tools.VersionNumber != 12352 {
|
|
t.Fatalf("mutating cloned VMware Tools version number should not affect original: %+v", original.VMware.Tools.VersionNumber)
|
|
}
|
|
if original.VMware.Tools.GuestRebootComponents[0] != "tools" {
|
|
t.Fatalf("mutating cloned VMware Tools reboot components should not affect original: %+v", original.VMware.Tools.GuestRebootComponents)
|
|
}
|
|
if original.VMware.Hardware.InstantCloneFrozen == nil || *original.VMware.Hardware.InstantCloneFrozen {
|
|
t.Fatalf("mutating cloned VMware hardware frozen flag should not affect original: %+v", original.VMware.Hardware.InstantCloneFrozen)
|
|
}
|
|
if original.VMware.Hardware.BootDelayMilliseconds == nil || *original.VMware.Hardware.BootDelayMilliseconds != 5000 {
|
|
t.Fatalf("mutating cloned VMware hardware boot delay should not affect original: %+v", original.VMware.Hardware.BootDelayMilliseconds)
|
|
}
|
|
if original.VMware.Hardware.BootDevices[0].Disks[0] != "2000" {
|
|
t.Fatalf("mutating cloned VMware hardware boot devices should not affect original: %+v", original.VMware.Hardware.BootDevices)
|
|
}
|
|
if original.VMware.Hardware.MemoryHotAddLimitMiB == nil || *original.VMware.Hardware.MemoryHotAddLimitMiB != 16384 {
|
|
t.Fatalf("mutating cloned VMware hardware memory limit should not affect original: %+v", original.VMware.Hardware.MemoryHotAddLimitMiB)
|
|
}
|
|
}
|
|
|
|
// --- cloneProxmoxData ---
|
|
|
|
func TestCloneProxmoxData_Nil(t *testing.T) {
|
|
if cloneProxmoxData(nil) != nil {
|
|
t.Error("nil should clone to nil")
|
|
}
|
|
}
|
|
|
|
func TestCloneProxmoxData_NodeAliasesIsolation(t *testing.T) {
|
|
original := &ProxmoxData{
|
|
NodeIdentity: "production-pve1",
|
|
NodeName: "pve1",
|
|
NodeAliases: []string{"pve-old"},
|
|
}
|
|
|
|
cloned := cloneProxmoxData(original)
|
|
cloned.NodeAliases[0] = "changed"
|
|
|
|
if original.NodeAliases[0] != "pve-old" {
|
|
t.Fatalf("Proxmox resource clone shared node aliases: %+v", original)
|
|
}
|
|
}
|
|
|
|
func TestCloneProxmoxData_TemperatureIsolation(t *testing.T) {
|
|
temp := 45.5
|
|
original := &ProxmoxData{
|
|
NodeName: "node1",
|
|
Temperature: &temp,
|
|
LoadAverage: []float64{1.0, 2.0, 3.0},
|
|
}
|
|
cloned := cloneProxmoxData(original)
|
|
|
|
*cloned.Temperature = 99.9
|
|
if *original.Temperature != 45.5 {
|
|
t.Error("mutating cloned Temperature should not affect original")
|
|
}
|
|
|
|
cloned.LoadAverage[0] = 99.0
|
|
if original.LoadAverage[0] != 1.0 {
|
|
t.Error("mutating cloned LoadAverage should not affect original")
|
|
}
|
|
}
|
|
|
|
func TestCloneProxmoxData_NetworkInterfaceIsolation(t *testing.T) {
|
|
original := &ProxmoxData{
|
|
NetworkInterfaces: []NetworkInterface{
|
|
{Name: "eth0", Addresses: []string{"10.0.0.1", "10.0.0.2"}},
|
|
},
|
|
}
|
|
cloned := cloneProxmoxData(original)
|
|
|
|
cloned.NetworkInterfaces[0].Addresses[0] = "MUTATED"
|
|
if original.NetworkInterfaces[0].Addresses[0] == "MUTATED" {
|
|
t.Error("mutating cloned network interface addresses should not affect original")
|
|
}
|
|
}
|
|
|
|
func TestCloneProxmoxDataMemoryIsolation(t *testing.T) {
|
|
original := &ProxmoxData{
|
|
Memory: &models.Memory{Total: 8 << 30, Used: 4 << 30, Usage: 50},
|
|
}
|
|
cloned := cloneProxmoxData(original)
|
|
|
|
cloned.Memory.Used = 7 << 30
|
|
if original.Memory.Used != 4<<30 {
|
|
t.Error("mutating cloned memory should not affect original")
|
|
}
|
|
}
|
|
|
|
func TestCloneAgentAndDockerMemoryIsolation(t *testing.T) {
|
|
agent := &AgentData{Memory: &AgentMemoryMeta{Total: 8 << 30}}
|
|
clonedAgent := cloneAgentData(agent)
|
|
clonedAgent.Memory.Total = 4 << 30
|
|
if agent.Memory.Total != 8<<30 {
|
|
t.Error("mutating cloned agent memory should not affect original")
|
|
}
|
|
|
|
docker := &DockerData{Memory: &AgentMemoryMeta{Total: 8 << 30}}
|
|
clonedDocker := cloneDockerData(docker)
|
|
clonedDocker.Memory.Total = 4 << 30
|
|
if docker.Memory.Total != 8<<30 {
|
|
t.Error("mutating cloned Docker memory should not affect original")
|
|
}
|
|
}
|
|
|
|
// --- cloneStorageMeta ---
|
|
|
|
func TestCloneStorageMeta_Nil(t *testing.T) {
|
|
if cloneStorageMeta(nil) != nil {
|
|
t.Error("nil should clone to nil")
|
|
}
|
|
}
|
|
|
|
func TestCloneStorageMeta_SliceIsolation(t *testing.T) {
|
|
original := &StorageMeta{
|
|
ContentTypes: []string{"images", "backup"},
|
|
Nodes: []string{"node1", "node2"},
|
|
ConsumerTypes: []string{"vm"},
|
|
TopConsumers: []StorageConsumerMeta{
|
|
{Name: "vm-100", DiskCount: 3},
|
|
},
|
|
}
|
|
cloned := cloneStorageMeta(original)
|
|
|
|
cloned.ContentTypes[0] = "MUTATED"
|
|
if original.ContentTypes[0] == "MUTATED" {
|
|
t.Error("mutating cloned ContentTypes should not affect original")
|
|
}
|
|
|
|
cloned.Nodes[0] = "MUTATED"
|
|
if original.Nodes[0] == "MUTATED" {
|
|
t.Error("mutating cloned Nodes should not affect original")
|
|
}
|
|
}
|
|
|
|
func TestCloneStorageMeta_RiskIsolation(t *testing.T) {
|
|
original := &StorageMeta{
|
|
Risk: &StorageRisk{
|
|
Level: storagehealth.RiskWarning,
|
|
Reasons: []StorageRiskReason{
|
|
{Code: "test", Summary: "test reason"},
|
|
},
|
|
},
|
|
}
|
|
cloned := cloneStorageMeta(original)
|
|
|
|
cloned.Risk.Reasons[0].Summary = "MUTATED"
|
|
if original.Risk.Reasons[0].Summary == "MUTATED" {
|
|
t.Error("mutating cloned Risk.Reasons should not affect original")
|
|
}
|
|
|
|
cloned.Risk.Level = storagehealth.RiskCritical
|
|
if original.Risk.Level == storagehealth.RiskCritical {
|
|
t.Error("mutating cloned Risk.Level should not affect original")
|
|
}
|
|
}
|
|
|
|
// --- cloneAgentData ---
|
|
|
|
func TestCloneAgentData_Nil(t *testing.T) {
|
|
if cloneAgentData(nil) != nil {
|
|
t.Error("nil should clone to nil")
|
|
}
|
|
}
|
|
|
|
func TestCloneAgentData_DeepIsolation(t *testing.T) {
|
|
temp := 55.0
|
|
original := &AgentData{
|
|
AgentID: "agent-1",
|
|
Temperature: &temp,
|
|
LoadAverage: []float64{0.5, 1.0, 1.5},
|
|
DiskExclude: []string{"/dev/sda"},
|
|
RAID: []HostRAIDMeta{
|
|
{Device: "/dev/md0", Level: "raid1", Risk: &StorageRisk{Level: storagehealth.RiskHealthy}},
|
|
},
|
|
PackageUpdates: &AgentPackageUpdateMeta{Packages: []AgentPackageUpdate{{Name: "openssl"}}},
|
|
StorageCleanup: &AgentStorageCleanupMeta{Provider: "apt-package-cache", ReclaimableBytes: 512},
|
|
}
|
|
cloned := cloneAgentData(original)
|
|
|
|
*cloned.Temperature = 99.9
|
|
if *original.Temperature != 55.0 {
|
|
t.Error("mutating cloned temp should not affect original")
|
|
}
|
|
|
|
cloned.DiskExclude[0] = "MUTATED"
|
|
if original.DiskExclude[0] == "MUTATED" {
|
|
t.Error("mutating cloned DiskExclude should not affect original")
|
|
}
|
|
|
|
cloned.RAID[0].Risk.Level = storagehealth.RiskCritical
|
|
if original.RAID[0].Risk.Level == storagehealth.RiskCritical {
|
|
t.Error("mutating cloned RAID risk should not affect original")
|
|
}
|
|
cloned.PackageUpdates.Packages[0].Name = "mutated"
|
|
cloned.StorageCleanup.Provider = "mutated"
|
|
if original.PackageUpdates.Packages[0].Name != "openssl" || original.StorageCleanup.Provider != "apt-package-cache" {
|
|
t.Fatal("mutating cloned agent maintenance posture should not affect original")
|
|
}
|
|
}
|
|
|
|
// --- cloneDockerData ---
|
|
|
|
func TestCloneDockerData_Nil(t *testing.T) {
|
|
if cloneDockerData(nil) != nil {
|
|
t.Error("nil should clone to nil")
|
|
}
|
|
}
|
|
|
|
func TestCloneDockerData_LabelsAndPortsIsolation(t *testing.T) {
|
|
original := &DockerData{
|
|
ContainerID: "abc123",
|
|
Labels: map[string]string{"env": "prod"},
|
|
Ports: []DockerPortMeta{
|
|
{PrivatePort: 8080, Protocol: "tcp"},
|
|
},
|
|
}
|
|
cloned := cloneDockerData(original)
|
|
|
|
cloned.Labels["new"] = "label"
|
|
if _, exists := original.Labels["new"]; exists {
|
|
t.Error("adding to cloned Labels should not affect original")
|
|
}
|
|
|
|
cloned.Ports[0].PublicPort = 9999
|
|
if original.Ports[0].PublicPort == 9999 {
|
|
t.Error("mutating cloned Ports should not affect original")
|
|
}
|
|
}
|
|
|
|
// --- primitive clone helpers ---
|
|
|
|
func TestCloneStringSlice_Nil(t *testing.T) {
|
|
if cloneStringSlice(nil) != nil {
|
|
t.Error("nil should clone to nil")
|
|
}
|
|
}
|
|
|
|
func TestCloneStringSlice_Empty(t *testing.T) {
|
|
empty := []string{}
|
|
cloned := cloneStringSlice(empty)
|
|
if cloned == nil {
|
|
t.Error("empty slice should clone to non-nil empty slice")
|
|
}
|
|
if len(cloned) != 0 {
|
|
t.Error("empty slice clone should have length 0")
|
|
}
|
|
}
|
|
|
|
func TestCloneStringSlice_Isolation(t *testing.T) {
|
|
original := []string{"a", "b", "c"}
|
|
cloned := cloneStringSlice(original)
|
|
cloned[0] = "MUTATED"
|
|
if original[0] == "MUTATED" {
|
|
t.Error("mutation should not propagate")
|
|
}
|
|
}
|
|
|
|
func TestCloneStringMap_Nil(t *testing.T) {
|
|
if cloneStringMap(nil) != nil {
|
|
t.Error("nil should clone to nil")
|
|
}
|
|
}
|
|
|
|
func TestCloneStringMap_Isolation(t *testing.T) {
|
|
original := map[string]string{"key": "val"}
|
|
cloned := cloneStringMap(original)
|
|
cloned["key"] = "MUTATED"
|
|
if original["key"] == "MUTATED" {
|
|
t.Error("mutation should not propagate")
|
|
}
|
|
}
|
|
|
|
func TestCloneStringPtr_Nil(t *testing.T) {
|
|
if cloneStringPtr(nil) != nil {
|
|
t.Error("nil should clone to nil")
|
|
}
|
|
}
|
|
|
|
func TestCloneStringPtr_Isolation(t *testing.T) {
|
|
s := "hello"
|
|
cloned := cloneStringPtr(&s)
|
|
*cloned = "MUTATED"
|
|
if s == "MUTATED" {
|
|
t.Error("mutation should not propagate")
|
|
}
|
|
}
|
|
|
|
func TestCloneTimePtr_Nil(t *testing.T) {
|
|
if cloneTimePtr(nil) != nil {
|
|
t.Error("nil should clone to nil")
|
|
}
|
|
}
|
|
|
|
func TestCloneTimePtr_Isolation(t *testing.T) {
|
|
t1 := time.Now()
|
|
cloned := cloneTimePtr(&t1)
|
|
*cloned = time.Time{}
|
|
if t1.IsZero() {
|
|
t.Error("mutation should not propagate")
|
|
}
|
|
}
|
|
|
|
func TestCloneBoolPtr_Nil(t *testing.T) {
|
|
if cloneBoolPtr(nil) != nil {
|
|
t.Error("nil should clone to nil")
|
|
}
|
|
}
|
|
|
|
func TestCloneBoolPtr_Isolation(t *testing.T) {
|
|
b := true
|
|
cloned := cloneBoolPtr(&b)
|
|
*cloned = false
|
|
if !b {
|
|
t.Error("mutation should not propagate")
|
|
}
|
|
}
|
|
|
|
func TestCloneFloat64Ptr_Nil(t *testing.T) {
|
|
if cloneFloat64Ptr(nil) != nil {
|
|
t.Error("nil should clone to nil")
|
|
}
|
|
}
|
|
|
|
func TestCloneFloat64Ptr_Isolation(t *testing.T) {
|
|
f := 3.14
|
|
cloned := cloneFloat64Ptr(&f)
|
|
*cloned = 999.0
|
|
if f != 3.14 {
|
|
t.Error("mutation should not propagate")
|
|
}
|
|
}
|
|
|
|
func TestCloneInt64Ptr_Nil(t *testing.T) {
|
|
if cloneInt64Ptr(nil) != nil {
|
|
t.Error("nil should clone to nil")
|
|
}
|
|
}
|
|
|
|
func TestCloneInt64Ptr_Isolation(t *testing.T) {
|
|
i := int64(42)
|
|
cloned := cloneInt64Ptr(&i)
|
|
*cloned = 999
|
|
if i != 42 {
|
|
t.Error("mutation should not propagate")
|
|
}
|
|
}
|
|
|
|
// --- cloneParentBySourceMap ---
|
|
|
|
func TestCloneParentBySourceMap_NilVsEmpty(t *testing.T) {
|
|
if cloneParentBySourceMap(nil) != nil {
|
|
t.Error("nil should clone to nil")
|
|
}
|
|
|
|
empty := map[DataSource]string{}
|
|
cloned := cloneParentBySourceMap(empty)
|
|
if cloned == nil {
|
|
t.Error("empty map should clone to non-nil empty map")
|
|
}
|
|
}
|
|
|
|
// --- cloneTemperature ---
|
|
|
|
func TestCloneTemperature_Nil(t *testing.T) {
|
|
if cloneTemperature(nil) != nil {
|
|
t.Error("nil should clone to nil")
|
|
}
|
|
}
|
|
|
|
func TestCloneTemperature_SliceIsolation(t *testing.T) {
|
|
original := &models.Temperature{
|
|
Cores: []models.CoreTemp{{Core: 0, Temp: 45.0}},
|
|
GPU: []models.GPUTemp{{Device: "gpu0", Edge: 55.0}},
|
|
NVMe: []models.NVMeTemp{{Device: "nvme0", Temp: 35.0}},
|
|
SMART: []models.DiskTemp{{Device: "/dev/sda", Temperature: 30}},
|
|
}
|
|
cloned := cloneTemperature(original)
|
|
|
|
cloned.Cores[0].Temp = 99.0
|
|
if original.Cores[0].Temp != 45.0 {
|
|
t.Error("mutating cloned Cores should not affect original")
|
|
}
|
|
}
|
|
|
|
// --- zeroTimeToPtr ---
|
|
|
|
func TestZeroTimeToPtr_ZeroReturnsNil(t *testing.T) {
|
|
if zeroTimeToPtr(time.Time{}) != nil {
|
|
t.Error("zero time should return nil")
|
|
}
|
|
}
|
|
|
|
func TestZeroTimeToPtr_NonZeroReturnsPtr(t *testing.T) {
|
|
now := time.Now()
|
|
result := zeroTimeToPtr(now)
|
|
if result == nil {
|
|
t.Fatal("non-zero time should return non-nil pointer")
|
|
}
|
|
if !result.Equal(now) {
|
|
t.Error("returned time should equal input")
|
|
}
|
|
}
|
|
|
|
// TestCloneResourceGivesRelationshipsTheirOwnBackingArray pins the aliasing fix
|
|
// for the availability projection scrub. cloneResource deep-copies every other
|
|
// slice on Resource; Relationships was left shallow, so a clone shared the
|
|
// original's backing array and removeAvailabilityProjectionLocked's in-place
|
|
// compaction wrote through into the live registry from an HTTP goroutine.
|
|
func TestCloneResourceGivesRelationshipsTheirOwnBackingArray(t *testing.T) {
|
|
original := Resource{
|
|
ID: "host-1",
|
|
Type: ResourceTypeAgent,
|
|
Name: "host-1",
|
|
Relationships: []ResourceRelationship{
|
|
{SourceID: "check-1", TargetID: "host-1", Type: RelChecks, Active: true},
|
|
{SourceID: "check-2", TargetID: "host-1", Type: RelChecks, Active: true},
|
|
{SourceID: "host-1", TargetID: "vm-1", Type: RelRunsOn, Active: true},
|
|
},
|
|
}
|
|
|
|
cloned := cloneResource(&original)
|
|
if len(cloned.Relationships) != 3 {
|
|
t.Fatalf("cloned relationships = %d, want 3", len(cloned.Relationships))
|
|
}
|
|
|
|
// Simulate the projection scrub compacting the clone in place.
|
|
filtered := cloned.Relationships[:0]
|
|
for _, relationship := range cloned.Relationships {
|
|
if relationship.Type == RelChecks {
|
|
continue
|
|
}
|
|
filtered = append(filtered, relationship)
|
|
}
|
|
cloned.Relationships = filtered
|
|
|
|
if len(original.Relationships) != 3 {
|
|
t.Fatalf("original relationships = %d, want 3", len(original.Relationships))
|
|
}
|
|
for i, want := range []string{"check-1", "check-2", "host-1"} {
|
|
if got := original.Relationships[i].SourceID; got != want {
|
|
t.Fatalf("original relationship %d sourceID = %q, want %q (clone wrote through)", i, got, want)
|
|
}
|
|
}
|
|
}
|