mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-27 05:57:27 +00:00
70ed7488a4
A source-filtered report-merge chose the links to undo by the sources each fold's folded side took in, which spans that side's whole subtree. Naming the source of a leaf in a chain (a Docker host linked into an agent linked into a VM) also split the leaf's holder from the VM, and where recorded links form a cycle a reported member stayed joined through the link whose fold did not name it. Folds now record what each side is on its own (HolderOwn, FoldedOwn), read before the other side's sources are merged in, and ReportedManualLinkFolds undoes the links that join a member carrying a reported source to the part that stays or to another reported member. A member hanging below a reported one goes with it and its link stays. A report naming every source or none still undoes every link, so the drawer's Split is unchanged; only API callers with a real sources filter see a difference.
1074 lines
37 KiB
Go
1074 lines
37 KiB
Go
package unifiedresources
|
|
|
|
import (
|
|
"reflect"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/models"
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/storagehealth"
|
|
"github.com/rcourtman/pulse-go-rewrite/pkg/tlsutil"
|
|
)
|
|
|
|
func TestCloneResourceRefreshesScopeAndPolicyMetadata(t *testing.T) {
|
|
// Registry mutation paths can leave derived metadata stale until the next
|
|
// read. Keep refresh on every clone, including after source/tag edits.
|
|
observation := models.MemoryObservation{
|
|
State: "last-known", Source: "guest-agent-meminfo",
|
|
ObservedAt: time.Date(2026, 10, 4, 12, 0, 0, 0, time.UTC),
|
|
}
|
|
original := &Resource{
|
|
ID: "app-container:lab", Type: ResourceTypeAppContainer,
|
|
Sources: []DataSource{SourceDocker, SourceAgent, SourceDocker},
|
|
PlatformScopes: []string{"stale"},
|
|
Agent: &AgentData{Hostname: "lab"},
|
|
Docker: &DockerData{HostSourceID: "proxmox-lxc-docker:lab:node:100"},
|
|
Policy: &ResourcePolicy{Sensitivity: ResourceSensitivityRestricted},
|
|
AISafeSummary: "stale",
|
|
Metrics: &ResourceMetrics{Memory: &MetricValue{
|
|
Percent: 50, Source: SourceProxmox, Observation: observation,
|
|
}},
|
|
}
|
|
for _, test := range []struct {
|
|
name string
|
|
tags []string
|
|
sensitivity ResourceSensitivity
|
|
routing ResourceRoutingScope
|
|
}{
|
|
{"public-last", []string{"backup", "PII", " PUBLIC "}, ResourceSensitivityPublic, ResourceRoutingScopeCloudSummary},
|
|
{"public-first", []string{"public", "restricted", "database"}, ResourceSensitivityPublic, ResourceRoutingScopeCloudSummary},
|
|
{"restricted-last", []string{"backup", " Customer_Data "}, ResourceSensitivityRestricted, ResourceRoutingScopeLocalOnly},
|
|
{"restricted-first", []string{" SECRETS ", "storage"}, ResourceSensitivityRestricted, ResourceRoutingScopeLocalOnly},
|
|
{"sensitive", []string{"", "unrecognised", " DaTaBaSe ", "database"}, ResourceSensitivitySensitive, ResourceRoutingScopeLocalFirst},
|
|
{"internal", []string{"prod", ""}, ResourceSensitivityInternal, ResourceRoutingScopeCloudSummary},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
original.Tags = test.tags
|
|
cloned := cloneResource(original)
|
|
assertStringSliceEqual(t, cloned.PlatformScopes, []string{"agent", "proxmox-pve", "docker"})
|
|
if cloned.Policy.Sensitivity != test.sensitivity || cloned.Policy.Routing.Scope != test.routing || cloned.AISafeSummary == "stale" {
|
|
t.Fatalf("clone retained stale policy: %#v, summary=%q", cloned.Policy, cloned.AISafeSummary)
|
|
}
|
|
cloned.PlatformScopes[0] = "mutated"
|
|
cloned.Policy.Sensitivity = ResourceSensitivityPublic
|
|
if cloned.Metrics == original.Metrics || cloned.Metrics.Memory == original.Metrics.Memory || cloned.Metrics.Memory.Observation != observation || cloned.Metrics.Memory.Source != SourceProxmox {
|
|
t.Fatal("metadata read aliased or renewed the selected memory observation")
|
|
}
|
|
cloned.Metrics.Memory.Observation.State = "current"
|
|
cloned.Metrics.Memory.Observation.ObservedAt = observation.ObservedAt.Add(time.Hour)
|
|
if original.Metrics.Memory.Observation != observation {
|
|
t.Fatal("borrowed metadata readers lost detached metric ownership")
|
|
}
|
|
if !reflect.DeepEqual(original.PlatformScopes, []string{"stale"}) || original.Policy.Sensitivity != ResourceSensitivityRestricted || original.AISafeSummary != "stale" {
|
|
t.Fatal("clone modified the original derived metadata")
|
|
}
|
|
})
|
|
}
|
|
original.Sources = []DataSource{SourceTrueNAS, SourceAgent}
|
|
original.TrueNAS = &TrueNASData{Hostname: "lab"}
|
|
original.Docker = nil
|
|
cloned := cloneResource(original)
|
|
assertStringSliceEqual(t, cloned.PlatformScopes, []string{"agent", "truenas"})
|
|
if cloned.Policy.Sensitivity != ResourceSensitivitySensitive {
|
|
t.Fatalf("edited provider facet lost its default sensitivity: %#v", cloned.Policy)
|
|
}
|
|
}
|
|
|
|
// --- 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 TestCloneZFSPoolIsolatesDatasets(t *testing.T) {
|
|
input := &models.ZFSPool{
|
|
Name: "tank",
|
|
Datasets: []models.ZFSDataset{{Name: "tank/apps", UsedBytes: 100}},
|
|
}
|
|
cloned := cloneZFSPool(input)
|
|
cloned.Datasets[0].Name = "mutated"
|
|
if input.Datasets[0].Name != "tank/apps" {
|
|
t.Fatal("unified resource zfs pool clone aliased datasets")
|
|
}
|
|
}
|
|
|
|
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 TestCloneResourceIsolatesCustomSensorValues(t *testing.T) {
|
|
value := 12.5
|
|
eventAt := time.Date(2026, 7, 30, 18, 0, 0, 0, time.UTC)
|
|
original := &Resource{
|
|
ID: "agent-custom",
|
|
Agent: &AgentData{
|
|
Sensors: &HostSensorMeta{
|
|
Custom: []HostCustomSensorMetric{{
|
|
ID: "queue_depth",
|
|
Name: "Queue depth",
|
|
Value: &value,
|
|
EventAt: &eventAt,
|
|
Status: "warning",
|
|
}},
|
|
},
|
|
},
|
|
}
|
|
|
|
cloned := cloneResource(original)
|
|
*cloned.Agent.Sensors.Custom[0].Value = 99
|
|
if *original.Agent.Sensors.Custom[0].Value != 12.5 {
|
|
t.Fatal("custom sensor value pointer was shared by clone")
|
|
}
|
|
cloned.Agent.Sensors.Custom[0].Name = "mutated"
|
|
if original.Agent.Sensors.Custom[0].Name != "Queue depth" {
|
|
t.Fatal("custom sensor slice was shared by clone")
|
|
}
|
|
*cloned.Agent.Sensors.Custom[0].EventAt = eventAt.Add(time.Hour)
|
|
if !original.Agent.Sensors.Custom[0].EventAt.Equal(eventAt) {
|
|
t.Fatal("custom sensor event time pointer was shared by clone")
|
|
}
|
|
}
|
|
|
|
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", ExpectedUpdateIntervalSeconds: 300},
|
|
SourceAgent: {Status: "stale", reported: StatusOffline},
|
|
},
|
|
}
|
|
cloned := cloneResource(original)
|
|
if cloned.SourceStatus[SourceProxmox].ExpectedUpdateIntervalSeconds != 300 {
|
|
t.Fatal("clone lost collector cadence")
|
|
}
|
|
// A clone that dropped the source verdict would aggregate a silent agent
|
|
// back to warning.
|
|
if cloned.SourceStatus[SourceAgent].reported != StatusOffline {
|
|
t.Fatal("clone lost the agent's offline verdict")
|
|
}
|
|
|
|
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")
|
|
}
|
|
}
|
|
|
|
// Operator link folds ride clones, so a registry seeded from another
|
|
// registry's listing can name each link's pair, and stay detached from them.
|
|
func TestCloneResource_CarriesLinkFolds(t *testing.T) {
|
|
original := &Resource{
|
|
ID: "vm-1",
|
|
linkFolds: []ManualLinkFold{{
|
|
HolderID: "vm-1", FoldedID: "agent-1", Sources: []DataSource{SourceAgent},
|
|
HolderOwn: []DataSource{SourceProxmox}, FoldedOwn: []DataSource{SourceAgent},
|
|
}},
|
|
}
|
|
cloned := cloneResource(original)
|
|
if len(cloned.linkFolds) != 1 || cloned.linkFolds[0].FoldedID != "agent-1" {
|
|
t.Fatalf("clone lost link folds: %+v", cloned.linkFolds)
|
|
}
|
|
|
|
cloned.linkFolds[0].Sources[0] = SourceDocker
|
|
cloned.linkFolds[0].HolderOwn[0] = SourceDocker
|
|
cloned.linkFolds[0].FoldedOwn[0] = SourceDocker
|
|
cloned.linkFolds[0].FoldedID = "MUTATED"
|
|
if original.linkFolds[0].Sources[0] != SourceAgent || original.linkFolds[0].FoldedID != "agent-1" ||
|
|
original.linkFolds[0].HolderOwn[0] != SourceProxmox || original.linkFolds[0].FoldedOwn[0] != SourceAgent {
|
|
t.Errorf("mutating cloned link folds affected the original: %+v", original.linkFolds)
|
|
}
|
|
}
|
|
|
|
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,
|
|
Certificate: &tlsutil.CertificateObservation{DNSNames: []string{"pulse.example.test"}},
|
|
},
|
|
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.Availability.Certificate.DNSNames[0] = "changed.example.test"
|
|
*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.Availability.Certificate.DNSNames[0] != "pulse.example.test" {
|
|
t.Error("mutating cloned availability certificate 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,
|
|
},
|
|
Tags: []VMwareTagData{{
|
|
TagID: "urn:vmomi:InventoryServiceTag:env-prod:GLOBAL",
|
|
Name: "Production",
|
|
CategoryID: "urn:vmomi:InventoryServiceCategory:environment:GLOBAL",
|
|
Category: "Environment",
|
|
Label: "Environment:Production",
|
|
}},
|
|
},
|
|
}
|
|
|
|
cloned := cloneResource(original)
|
|
cloned.VMware.Tags[0].Name = "mutated"
|
|
cloned.VMware.Tags[0].Label = "mutated"
|
|
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
|
|
|
|
// vCenter tags are operator-authored labels the presentation layer renders
|
|
// per row, so a cloned snapshot must never write back onto the registry's
|
|
// copy of them.
|
|
if original.VMware.Tags[0].Name != "Production" || original.VMware.Tags[0].Label != "Environment:Production" {
|
|
t.Fatalf("mutating cloned VMware tags should not affect original: %+v", original.VMware.Tags)
|
|
}
|
|
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")
|
|
}
|
|
}
|
|
|
|
// List and presentation readers get detached clones of a node, so the
|
|
// registry-derived sensor setup verdict travels with the clone and stays put
|
|
// when the live node's verdict changes afterwards.
|
|
func TestCloneProxmoxDataKeepsDerivedSensorSetupVerdict(t *testing.T) {
|
|
outdated := true
|
|
original := &ProxmoxData{NodeName: "pve", SensorSetupOutdated: &outdated}
|
|
cloned := cloneProxmoxData(original)
|
|
if cloned.SensorSetupOutdated == nil || !*cloned.SensorSetupOutdated {
|
|
t.Fatal("clone dropped the derived sensor setup verdict")
|
|
}
|
|
if cloned.SensorSetupOutdated == original.SensorSetupOutdated {
|
|
t.Fatal("clone shares the live node's verdict pointer")
|
|
}
|
|
*original.SensorSetupOutdated = false
|
|
if !*cloned.SensorSetupOutdated {
|
|
t.Fatal("changing the live node's verdict changed a clone already handed out")
|
|
}
|
|
}
|
|
|
|
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) {
|
|
observation := models.MemoryObservation{State: "last-known", Source: "guest-agent-meminfo", ObservedAt: time.Now().Add(-time.Minute)}
|
|
original := &ProxmoxData{
|
|
Memory: &models.Memory{Total: 8 << 30, Used: 4 << 30, Usage: 50, Observation: observation},
|
|
}
|
|
cloned := cloneProxmoxData(original)
|
|
if cloned.Memory.Observation != observation {
|
|
t.Fatal("clone lost original memory observation")
|
|
}
|
|
|
|
cloned.Memory.Used = 7 << 30
|
|
cloned.Memory.Observation.State = "current"
|
|
cloned.Memory.Observation.ObservedAt = time.Now()
|
|
if original.Memory.Used != 4<<30 || original.Memory.Observation != observation {
|
|
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"},
|
|
AliasIDs: []string{"legacy-storage-id"},
|
|
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.AliasIDs[0] = "MUTATED"
|
|
if original.AliasIDs[0] == "MUTATED" {
|
|
t.Error("mutating cloned AliasIDs 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", RequiredDevices: 2, TotalDevices: 3, SpareDevices: 1, 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")
|
|
}
|
|
if cloned.RAID[0].RequiredDevices != 2 || cloned.RAID[0].TotalDevices != 3 || cloned.RAID[0].SpareDevices != 1 {
|
|
t.Fatalf("clone lost RAID count provenance: %+v", cloned.RAID[0])
|
|
}
|
|
cloned.RAID[0].RequiredDevices = 1
|
|
if original.RAID[0].RequiredDevices != 2 {
|
|
t.Fatal("mutating cloned RAID required count changed the source")
|
|
}
|
|
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",
|
|
HealthcheckTargets: []string{"database.internal"},
|
|
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")
|
|
}
|
|
|
|
cloned.HealthcheckTargets[0] = "changed.internal"
|
|
if original.HealthcheckTargets[0] != "database.internal" {
|
|
t.Error("mutating cloned HealthcheckTargets 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)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCloneResourceIsolatesVirtualMachineFacet(t *testing.T) {
|
|
original := Resource{
|
|
ID: "vm-agent-domain-a",
|
|
Type: ResourceTypeVM,
|
|
VirtualMachine: &VirtualMachineData{
|
|
RuntimeState: "running",
|
|
Hypervisor: "libvirt",
|
|
VCPUs: 4,
|
|
},
|
|
}
|
|
cloned := cloneResource(&original)
|
|
if cloned.VirtualMachine == nil {
|
|
t.Fatal("cloned resource lost virtual machine facet")
|
|
}
|
|
cloned.VirtualMachine.RuntimeState = "paused"
|
|
if original.VirtualMachine.RuntimeState != "running" {
|
|
t.Fatal("cloned virtual machine facet aliases original")
|
|
}
|
|
}
|
|
|
|
func TestClonedResourcesPreservePlatformAdmission(t *testing.T) {
|
|
// Readers are served clones, so admission must not change as resources pass
|
|
// through the clone path: a provider-owned host that loses its scopes on
|
|
// clone would start admitting the standalone platform page.
|
|
truenasHost := Resource{
|
|
ID: "truenas-host",
|
|
Type: ResourceTypeAgent,
|
|
Sources: []DataSource{SourceAgent, SourceTrueNAS},
|
|
Agent: &AgentData{},
|
|
TrueNAS: &TrueNASData{},
|
|
}
|
|
RefreshPlatformScopes(&truenasHost)
|
|
|
|
original := BuildPlatformAdmission([]Resource{truenasHost})
|
|
cloned := BuildPlatformAdmission([]Resource{cloneResource(&truenasHost)})
|
|
if original != cloned {
|
|
t.Fatalf("admission changed across clone: %+v -> %+v", original, cloned)
|
|
}
|
|
if cloned.Standalone {
|
|
t.Fatalf("provider-owned host must not admit standalone after clone, got %+v", cloned)
|
|
}
|
|
}
|
|
|
|
func TestCloneResource_PreservesActionReadinessDetail(t *testing.T) {
|
|
original := &Resource{
|
|
ID: "app-container:api",
|
|
ActionReadiness: []ResourceActionReadiness{
|
|
{
|
|
Name: "restart",
|
|
Available: false,
|
|
ReasonCode: "command_agent_disconnected",
|
|
Reason: "Docker / Podman command agent is not connected.",
|
|
Detail: "no live command session for agent agent-1 or hostname docker-host",
|
|
},
|
|
},
|
|
}
|
|
cloned := cloneResource(original)
|
|
if len(cloned.ActionReadiness) != 1 || cloned.ActionReadiness[0] != original.ActionReadiness[0] {
|
|
t.Fatalf("clone dropped action readiness diagnostics: %#v", cloned.ActionReadiness)
|
|
}
|
|
}
|
|
|
|
func TestCloneMaterializedResourceDetachesCanonicalPolicy(t *testing.T) {
|
|
original := &Resource{ID: "agent:synthetic", Type: ResourceTypeAgent, Name: "synthetic", Sources: []DataSource{SourceAgent}, Tags: []string{"customer-data"}, Agent: &AgentData{AgentID: "synthetic", Hostname: "synthetic.example.test"}, Identity: ResourceIdentity{IPAddresses: []string{"192.0.2.1"}}, SupersededCanonicalIDs: []string{"agent:old-synthetic"}}
|
|
RefreshCanonicalMetadata(original)
|
|
copied := cloneMaterializedResource(original)
|
|
if !reflect.DeepEqual(copied, cloneResource(original)) {
|
|
t.Fatal("materialized copy differs from arbitrary-input refreshed clone")
|
|
}
|
|
copied.Canonical.Aliases[0] = "changed"
|
|
copied.Canonical.SupersededIDs[0] = "changed"
|
|
copied.Policy.Routing.Redact[0] = "changed"
|
|
copied.Policy.Sensitivity = ResourceSensitivityPublic
|
|
copied.PlatformScopes[0] = "changed"
|
|
if original.Canonical.Aliases[0] == "changed" || original.Canonical.SupersededIDs[0] == "changed" || original.Policy.Routing.Redact[0] == "changed" || original.Policy.Sensitivity != ResourceSensitivityRestricted || original.PlatformScopes[0] == "changed" {
|
|
t.Fatal("bulk resource copy aliases canonical identity or policy")
|
|
}
|
|
original.Tags = []string{"public"}
|
|
refreshed := cloneResource(original)
|
|
if refreshed.Policy.Sensitivity != ResourceSensitivityPublic {
|
|
t.Fatal("arbitrary clone trusted stale materialized policy")
|
|
}
|
|
if cloneMaterializedResource(nil).ID != "" || cloneMaterializedResourcePtr(nil) != nil {
|
|
t.Fatal("nil materialized clone changed absence")
|
|
}
|
|
}
|
|
|
|
// The resources API seeds its registry from the monitor's read state, so the
|
|
// marker on a row saved-host continuity introduced must ride the clone, or the
|
|
// API's link pass would fold the saved agent into its linked guest.
|
|
func TestCloneResourceKeepsContinuityMarker(t *testing.T) {
|
|
saved := Resource{ID: "agent-saved", Type: ResourceTypeAgent, continuityOnly: true}
|
|
if clone := cloneResource(&saved); !clone.continuityOnly {
|
|
t.Fatal("clone dropped the continuity marker")
|
|
}
|
|
rr := NewRegistry(nil)
|
|
rr.IngestResources([]Resource{saved})
|
|
if seeded, ok := rr.Get(saved.ID); !ok || !seeded.continuityOnly {
|
|
t.Fatalf("seeded row = %+v (listed=%v), want the continuity marker kept", seeded, ok)
|
|
}
|
|
}
|