Files
pulse/internal/mock/generator_test.go
T
rcourtman 47392b5f68 fix(mock): derive all fixture identities from stable hashes so IDs survive restarts
Mock fixture IDs churned across backend restarts, breaking resource
identity continuity in mock mode: k8s pod and deployment names picked
their namespace and prefix with rand.Intn per boot, the ceph FSID was
pure rand.Int63n, and generic agent hosts randomized both platform
profile (which feeds the host ID) and hostname. Every restart re-keyed
those unified resources and re-seeded ~31k orphan metric rows that
lingered for the full 90d mock retention.

Derive all of them from mockStableChoice/mockStableDecimalString over
stable inputs (cluster ID, item ordinal, instance name) instead,
matching the generator's existing stable-ID idiom and its documented
contract. Add a regression test that builds the fixture graph twice
and requires every identity set to match.

Verified live: two mock-mode boots now share all 185 distinct metric
resource IDs and the second boot's backfill seeds 0 rows (was ~31k).
2026-06-10 21:49:15 +01:00

764 lines
22 KiB
Go

package mock
import (
"fmt"
"math"
"sort"
"strings"
"testing"
"time"
"github.com/rcourtman/pulse-go-rewrite/internal/models"
)
func TestBuildFixtureStateIncludesDockerHosts(t *testing.T) {
cfg := DefaultConfig
cfg.DockerHostCount = 2
cfg.DockerContainersPerHost = 5
data := buildFixtureState(cfg)
if len(data.DockerHosts) != cfg.DockerHostCount {
t.Fatalf("expected %d docker hosts, got %d", cfg.DockerHostCount, len(data.DockerHosts))
}
for _, host := range data.DockerHosts {
if host.ID == "" {
t.Fatalf("docker host missing id: %+v", host)
}
if len(host.Containers) == 0 {
t.Fatalf("docker host %s has no containers", host.Hostname)
}
if len(host.Images) == 0 {
t.Fatalf("docker host %s has no image inventory", host.Hostname)
}
if len(host.Volumes) == 0 {
t.Fatalf("docker host %s has no volume inventory", host.Hostname)
}
if len(host.Networks) == 0 {
t.Fatalf("docker host %s has no network inventory", host.Hostname)
}
if host.StorageUsage == nil || host.StorageUsage.Images.TotalCount == 0 || host.StorageUsage.Volumes.TotalCount == 0 {
t.Fatalf("docker host %s has no engine storage usage inventory: %+v", host.Hostname, host.StorageUsage)
}
}
}
func TestComputeGuestCountsHandlesZeroBaselines(t *testing.T) {
cfg := MockConfig{
VMsPerNode: 0,
LXCsPerNode: 0,
}
roles := []string{"vm-heavy", "container-heavy", "light", "mixed"}
for _, role := range roles {
vmCount, lxcCount := computeGuestCounts(cfg, role)
if vmCount < 0 || lxcCount < 0 {
t.Fatalf("expected non-negative counts for role %q, got vm=%d lxc=%d", role, vmCount, lxcCount)
}
}
}
func TestBuildFixtureStateWithZeroGuestBaselinesDoesNotPanic(t *testing.T) {
cfg := DefaultConfig
cfg.NodeCount = 4
cfg.VMsPerNode = 0
cfg.LXCsPerNode = 0
cfg.DockerHostCount = 0
cfg.GenericHostCount = 0
cfg.K8sClusterCount = 0
for i := 0; i < 20; i++ {
func() {
defer func() {
if r := recover(); r != nil {
t.Fatalf("buildFixtureState panicked on iteration %d: %v", i, r)
}
}()
_ = buildFixtureState(cfg)
}()
}
}
func TestBuildFixtureStateIncludesSwarmServices(t *testing.T) {
cfg := DefaultConfig
cfg.DockerHostCount = 4
cfg.DockerContainersPerHost = 6
cfg.RandomMetrics = false
data := buildFixtureState(cfg)
found := false
for _, host := range data.DockerHosts {
if len(host.Services) == 0 {
continue
}
if host.Swarm == nil {
t.Fatalf("expected swarm metadata for host %s", host.ID)
}
if len(host.Tasks) == 0 {
t.Fatalf("expected tasks for service host %s", host.ID)
}
if len(host.Secrets) == 0 {
t.Fatalf("expected secrets for service host %s", host.ID)
}
if len(host.Configs) == 0 {
t.Fatalf("expected configs for service host %s", host.ID)
}
if len(host.Nodes) == 0 {
t.Fatalf("expected swarm nodes for service host %s", host.ID)
}
found = true
break
}
if !found {
t.Fatalf("expected at least one docker host with swarm services")
}
}
func TestBuildFixtureStateIncludesHostAgents(t *testing.T) {
cfg := DefaultConfig
cfg.GenericHostCount = 5
cfg.K8sClusterCount = 0
cfg.RandomMetrics = false
data := buildFixtureState(cfg)
expectedMin := cfg.GenericHostCount + cfg.NodeCount
if len(data.Hosts) < expectedMin {
t.Fatalf("expected at least %d host agents, got %d", expectedMin, len(data.Hosts))
}
for _, host := range data.Hosts {
if host.ID == "" {
t.Fatalf("host agent missing id: %+v", host)
}
if host.Hostname == "" {
t.Fatalf("host agent missing hostname: %+v", host)
}
if host.Status == "" {
t.Fatalf("host agent missing status: %+v", host)
}
}
}
func TestBuildFixtureStatePreservesStandaloneHostAgentsWhenLinkingNodes(t *testing.T) {
cfg := DefaultConfig
cfg.NodeCount = 3
cfg.GenericHostCount = 4
cfg.K8sClusterCount = 0
cfg.RandomMetrics = false
data := buildFixtureState(cfg)
standalone := 0
proxmoxLinked := 0
for _, host := range data.Hosts {
if host.LinkedNodeID != "" {
proxmoxLinked++
} else {
standalone++
}
}
if standalone != cfg.GenericHostCount {
t.Fatalf("expected %d standalone agent hosts, got %d", cfg.GenericHostCount, standalone)
}
if proxmoxLinked != cfg.NodeCount {
t.Fatalf("expected %d Proxmox-linked agent hosts, got %d", cfg.NodeCount, proxmoxLinked)
}
}
func TestGenerateVMStoppedPreservesConfiguredCapacity(t *testing.T) {
cfg := DefaultConfig
cfg.StoppedPercent = 1
vm := generateVM("node-01", "mock-cluster", 1001, cfg)
if vm.Status != "stopped" {
t.Fatalf("expected stopped VM, got %q", vm.Status)
}
if vm.Memory.Total <= 0 {
t.Fatalf("expected stopped VM to keep configured memory total, got %d", vm.Memory.Total)
}
if vm.Memory.Used != 0 || vm.Memory.Usage != 0 {
t.Fatalf("expected stopped VM memory usage to be zero, got used=%d usage=%f", vm.Memory.Used, vm.Memory.Usage)
}
if vm.Memory.Free != vm.Memory.Total {
t.Fatalf("expected stopped VM free memory %d to equal total %d", vm.Memory.Free, vm.Memory.Total)
}
}
func TestGenerateContainerStoppedPreservesConfiguredCapacity(t *testing.T) {
cfg := DefaultConfig
cfg.StoppedPercent = 1
ct := generateContainer("node-01", "mock-cluster", 2001, cfg)
if ct.Status != "stopped" {
t.Fatalf("expected stopped container, got %q", ct.Status)
}
if ct.Memory.Total <= 0 {
t.Fatalf("expected stopped container to keep configured memory total, got %d", ct.Memory.Total)
}
if ct.Memory.Used != 0 || ct.Memory.Usage != 0 {
t.Fatalf("expected stopped container memory usage to be zero, got used=%d usage=%f", ct.Memory.Used, ct.Memory.Usage)
}
if ct.Memory.Free != ct.Memory.Total {
t.Fatalf("expected stopped container free memory %d to equal total %d", ct.Memory.Free, ct.Memory.Total)
}
}
func TestNormalizeMockBlendWeight_ComposesAcrossUpdateInterval(t *testing.T) {
perMinuteWeight := 0.22
perTickWeight := normalizeMockBlendWeight(perMinuteWeight, defaultMockUpdateInterval, time.Minute)
compounded := 1 - math.Pow(1-perTickWeight, float64(time.Minute/defaultMockUpdateInterval))
if perTickWeight >= perMinuteWeight {
t.Fatalf("expected per-tick weight %.6f to be less than per-minute weight %.6f", perTickWeight, perMinuteWeight)
}
if math.Abs(compounded-perMinuteWeight) > 0.01 {
t.Fatalf("expected compounded weight %.6f to stay close to %.6f", compounded, perMinuteWeight)
}
}
func TestBuildFixtureStateLinksAllNodesToHostAgents(t *testing.T) {
cfg := DefaultConfig
cfg.NodeCount = 7
cfg.GenericHostCount = 2
cfg.RandomMetrics = false
data := buildFixtureState(cfg)
if len(data.Hosts) < cfg.NodeCount {
t.Fatalf("expected enough hosts to link all nodes, got hosts=%d nodes=%d", len(data.Hosts), cfg.NodeCount)
}
hostsByID := make(map[string]models.Host, len(data.Hosts))
for _, host := range data.Hosts {
hostsByID[host.ID] = host
}
for _, node := range data.Nodes {
if node.LinkedAgentID == "" {
t.Fatalf("node %s is missing linked host agent id", node.ID)
}
host, ok := hostsByID[node.LinkedAgentID]
if !ok {
t.Fatalf("node %s linked host %s not found", node.ID, node.LinkedAgentID)
}
if host.LinkedNodeID != node.ID {
t.Fatalf("host %s linkedNodeID=%q, want %q", host.ID, host.LinkedNodeID, node.ID)
}
}
}
func TestBuildFixtureStatePopulatesHostIORates(t *testing.T) {
cfg := DefaultConfig
cfg.NodeCount = 6
cfg.GenericHostCount = 1
cfg.RandomMetrics = true
data := buildFixtureState(cfg)
for _, host := range data.Hosts {
if host.Status == "offline" {
if host.NetInRate != 0 || host.NetOutRate != 0 || host.DiskReadRate != 0 || host.DiskWriteRate != 0 {
t.Fatalf("offline host %s should have zero I/O rates", host.ID)
}
continue
}
if host.NetInRate <= 0 || host.NetOutRate <= 0 {
t.Fatalf("host %s missing network rates: in=%f out=%f", host.ID, host.NetInRate, host.NetOutRate)
}
if host.DiskReadRate <= 0 || host.DiskWriteRate <= 0 {
t.Fatalf("host %s missing disk rates: read=%f write=%f", host.ID, host.DiskReadRate, host.DiskWriteRate)
}
}
}
func TestBuildFixtureStatePopulatesDockerHostIORates(t *testing.T) {
cfg := DefaultConfig
cfg.DockerHostCount = 3
cfg.RandomMetrics = true
data := buildFixtureState(cfg)
if len(data.DockerHosts) == 0 {
t.Fatal("expected docker hosts in mock data")
}
for _, host := range data.DockerHosts {
if host.Status == "offline" {
if host.NetInRate != 0 || host.NetOutRate != 0 || host.DiskReadRate != 0 || host.DiskWriteRate != 0 {
t.Fatalf("offline docker host %s should have zero I/O rates", host.ID)
}
if host.Temperature != nil {
t.Fatalf("offline docker host %s should not report temperature", host.ID)
}
continue
}
if host.NetInRate <= 0 || host.NetOutRate <= 0 {
t.Fatalf("docker host %s missing network rates: in=%f out=%f", host.ID, host.NetInRate, host.NetOutRate)
}
if host.DiskReadRate <= 0 || host.DiskWriteRate <= 0 {
t.Fatalf("docker host %s missing disk rates: read=%f write=%f", host.ID, host.DiskReadRate, host.DiskWriteRate)
}
if host.Temperature == nil {
t.Fatalf("docker host %s missing temperature", host.ID)
}
}
}
func TestBuildFixtureStatePopulatesKubernetesUsageMetrics(t *testing.T) {
cfg := DefaultConfig
cfg.K8sClusterCount = 1
cfg.K8sNodesPerCluster = 4
cfg.K8sPodsPerCluster = 24
cfg.RandomMetrics = false
data := buildFixtureState(cfg)
if len(data.KubernetesClusters) != 1 {
t.Fatalf("expected exactly one kubernetes cluster, got %d", len(data.KubernetesClusters))
}
cluster := data.KubernetesClusters[0]
readyNodeNames := make(map[string]struct{}, len(cluster.Nodes))
for _, node := range cluster.Nodes {
if node.Ready {
readyNodeNames[strings.TrimSpace(node.Name)] = struct{}{}
}
}
runningPodsWithUsage := 0
for _, pod := range cluster.Pods {
if !strings.EqualFold(pod.Phase, "running") {
continue
}
if _, ok := readyNodeNames[strings.TrimSpace(pod.NodeName)]; !ok {
continue
}
if pod.UsageCPUMilliCores <= 0 || pod.UsageMemoryBytes <= 0 {
t.Fatalf("running pod %s missing cpu/memory usage: %+v", pod.Name, pod)
}
if pod.NetInRate <= 0 || pod.NetOutRate <= 0 {
t.Fatalf("running pod %s missing network rates: in=%f out=%f", pod.Name, pod.NetInRate, pod.NetOutRate)
}
if pod.EphemeralStorageCapacityBytes <= 0 || pod.EphemeralStorageUsedBytes <= 0 {
t.Fatalf("running pod %s missing ephemeral storage usage: %+v", pod.Name, pod)
}
if pod.DiskUsagePercent <= 0 {
t.Fatalf("running pod %s missing disk usage percent: %+v", pod.Name, pod)
}
runningPodsWithUsage++
}
if runningPodsWithUsage == 0 {
t.Fatal("expected at least one running kubernetes pod with usage metrics")
}
readyNodesWithUsage := 0
for _, node := range cluster.Nodes {
if !node.Ready {
continue
}
if node.UsageCPUMilliCores <= 0 || node.UsageMemoryBytes <= 0 {
t.Fatalf("ready node %s missing usage metrics: %+v", node.Name, node)
}
readyNodesWithUsage++
}
if readyNodesWithUsage == 0 {
t.Fatal("expected at least one ready kubernetes node with usage metrics")
}
}
func TestBuildFixtureStateIncludesKubernetesDeploymentAPIMetadata(t *testing.T) {
cfg := DefaultConfig
cfg.K8sClusterCount = 1
cfg.K8sNodesPerCluster = 3
cfg.K8sPodsPerCluster = 8
cfg.K8sDeploymentsPerCluster = 6
cfg.RandomMetrics = false
data := buildFixtureState(cfg)
if len(data.KubernetesClusters) != 1 {
t.Fatalf("expected exactly one kubernetes cluster, got %d", len(data.KubernetesClusters))
}
cluster := data.KubernetesClusters[0]
if len(cluster.Deployments) == 0 {
t.Fatal("expected kubernetes deployment inventory")
}
for _, deployment := range cluster.Deployments {
if deployment.UID == "" {
t.Fatalf("deployment missing uid: %+v", deployment)
}
if deployment.CreatedAt.IsZero() {
t.Fatalf("deployment missing creation metadata: %+v", deployment)
}
if deployment.ObservedGeneration == 0 {
t.Fatalf("deployment missing observed generation: %+v", deployment)
}
}
}
func TestBuildFixtureStateCreatesHostEntriesForKubernetesNodes(t *testing.T) {
cfg := DefaultConfig
cfg.K8sClusterCount = 1
cfg.K8sNodesPerCluster = 3
cfg.K8sPodsPerCluster = 8
cfg.NodeCount = 0
cfg.RandomMetrics = false
data := buildFixtureState(cfg)
if len(data.KubernetesClusters) == 0 {
t.Fatal("expected kubernetes clusters in mock data")
}
hostsByName := make(map[string]models.Host, len(data.Hosts))
for _, host := range data.Hosts {
hostsByName[strings.ToLower(strings.TrimSpace(host.Hostname))] = host
}
for _, node := range data.KubernetesClusters[0].Nodes {
host, ok := hostsByName[strings.ToLower(strings.TrimSpace(node.Name))]
if !ok {
t.Fatalf("expected host entry for kubernetes node %s", node.Name)
}
if host.NetInRate <= 0 || host.NetOutRate <= 0 {
t.Fatalf("kubernetes node host %s missing network rates: in=%f out=%f", host.Hostname, host.NetInRate, host.NetOutRate)
}
if host.DiskReadRate <= 0 || host.DiskWriteRate <= 0 {
t.Fatalf("kubernetes node host %s missing disk rates: read=%f write=%f", host.Hostname, host.DiskReadRate, host.DiskWriteRate)
}
if len(host.Sensors.TemperatureCelsius) == 0 {
t.Fatalf("kubernetes node host %s missing temperature sensors", host.Hostname)
}
}
}
func TestBuildFixtureStateIncludesKubernetesStorageInventory(t *testing.T) {
cfg := DefaultConfig
cfg.K8sClusterCount = 1
cfg.K8sNodesPerCluster = 3
cfg.K8sPodsPerCluster = 8
cfg.RandomMetrics = false
data := buildFixtureState(cfg)
if len(data.KubernetesClusters) != 1 {
t.Fatalf("expected exactly one kubernetes cluster, got %d", len(data.KubernetesClusters))
}
cluster := data.KubernetesClusters[0]
if len(cluster.StorageClasses) == 0 {
t.Fatal("expected kubernetes storage class inventory")
}
if len(cluster.PersistentVolumes) == 0 {
t.Fatal("expected kubernetes persistent volume inventory")
}
if len(cluster.PersistentVolumeClaims) == 0 {
t.Fatal("expected kubernetes persistent volume claim inventory")
}
}
func TestBuildFixtureStateIncludesKubernetesNetworkingInventory(t *testing.T) {
cfg := DefaultConfig
cfg.K8sClusterCount = 1
cfg.K8sNodesPerCluster = 3
cfg.K8sPodsPerCluster = 8
cfg.RandomMetrics = false
data := buildFixtureState(cfg)
if len(data.KubernetesClusters) != 1 {
t.Fatalf("expected exactly one kubernetes cluster, got %d", len(data.KubernetesClusters))
}
cluster := data.KubernetesClusters[0]
if len(cluster.Services) == 0 {
t.Fatal("expected kubernetes service inventory")
}
foundPublishedService := false
for _, service := range cluster.Services {
if service.ServiceType != "NodePort" || len(service.ExternalIPs) == 0 {
continue
}
for _, port := range service.Ports {
if port.NodePort > 0 {
foundPublishedService = true
}
}
}
if !foundPublishedService {
t.Fatal("expected kubernetes service inventory to include node-port/external service metadata")
}
if len(cluster.Ingresses) == 0 {
t.Fatal("expected kubernetes ingress inventory")
}
if len(cluster.EndpointSlices) == 0 {
t.Fatal("expected kubernetes endpoint slice inventory")
}
}
func TestMockStateIncludesHostAgents(t *testing.T) {
mustSetEnabled(t, true)
t.Cleanup(func() {
mustSetEnabled(t, false)
})
state := CurrentFixtureGraph().State
if len(state.Hosts) == 0 {
t.Fatalf("expected hosts in mock state, got %d", len(state.Hosts))
}
frontend := state.ToFrontend()
if frontend.Resources == nil {
t.Fatal("expected canonical frontend resources slice to be initialized")
}
if frontend.ConnectedInfrastructure == nil {
t.Fatal("expected canonical connectedInfrastructure slice to be initialized")
}
}
func TestUpdateMetricsMaintainsServiceHealth(t *testing.T) {
cfg := DefaultConfig
cfg.DockerHostCount = 3
cfg.DockerContainersPerHost = 6
data := buildFixtureState(cfg)
updateFixtureStateMetrics(&data, cfg)
for _, host := range data.DockerHosts {
if len(host.Services) == 0 {
continue
}
if host.Swarm == nil {
t.Fatalf("expected swarm metadata for host %s after update", host.ID)
}
for _, svc := range host.Services {
if svc.DesiredTasks < 0 {
t.Fatalf("service %s has negative desired tasks", svc.Name)
}
if svc.RunningTasks < 0 {
t.Fatalf("service %s has negative running tasks", svc.Name)
}
if svc.RunningTasks > svc.DesiredTasks && svc.DesiredTasks > 0 {
t.Fatalf("service %s has running (%d) > desired (%d)", svc.Name, svc.RunningTasks, svc.DesiredTasks)
}
}
}
}
func TestBuildFixtureStateIncludesPMGInstances(t *testing.T) {
cfg := DefaultConfig
data := buildFixtureState(cfg)
if len(data.PMGInstances) == 0 {
t.Fatalf("expected PMG instances in mock data")
}
for _, inst := range data.PMGInstances {
if inst.Name == "" {
t.Fatalf("PMG instance missing name: %+v", inst)
}
if inst.Status == "" {
t.Fatalf("PMG instance missing status: %+v", inst)
}
}
}
func TestCloneStateCopiesPMGInstances(t *testing.T) {
state := models.StateSnapshot{
PMGInstances: []models.PMGInstance{
{ID: "pmg-test", Name: "pmg-test", Status: "online"},
},
}
cloned := cloneState(state)
if len(cloned.PMGInstances) != 1 {
t.Fatalf("expected cloned state to include PMG instances, got %d", len(cloned.PMGInstances))
}
cloned.PMGInstances[0].Name = "modified"
if state.PMGInstances[0].Name == "modified" {
t.Fatal("expected PMG instances to be deep-copied")
}
}
func TestCloneStatePreservesIgnoredInfrastructureEntries(t *testing.T) {
state := models.StateSnapshot{
RemovedDockerHosts: []models.RemovedDockerHost{
{ID: "docker-1", Hostname: "docker.local"},
},
RemovedHostAgents: []models.RemovedHostAgent{
{ID: "agent-1", Hostname: "host.local"},
},
RemovedKubernetesClusters: []models.RemovedKubernetesCluster{
{ID: "cluster-1", Name: "cluster.local"},
},
}
cloned := cloneState(state)
if len(cloned.RemovedDockerHosts) != 1 {
t.Fatalf("expected cloned state to include removed docker hosts, got %d", len(cloned.RemovedDockerHosts))
}
if len(cloned.RemovedHostAgents) != 1 {
t.Fatalf("expected cloned state to include removed host agents, got %d", len(cloned.RemovedHostAgents))
}
if len(cloned.RemovedKubernetesClusters) != 1 {
t.Fatalf("expected cloned state to include removed kubernetes clusters, got %d", len(cloned.RemovedKubernetesClusters))
}
cloned.RemovedDockerHosts[0].Hostname = "mutated-docker.local"
cloned.RemovedHostAgents[0].Hostname = "mutated-host.local"
cloned.RemovedKubernetesClusters[0].Name = "mutated-cluster.local"
if state.RemovedDockerHosts[0].Hostname == "mutated-docker.local" {
t.Fatal("expected removed docker hosts to be copied")
}
if state.RemovedHostAgents[0].Hostname == "mutated-host.local" {
t.Fatal("expected removed host agents to be copied")
}
if state.RemovedKubernetesClusters[0].Name == "mutated-cluster.local" {
t.Fatal("expected removed kubernetes clusters to be copied")
}
}
func TestBuildFixtureStateInitializesIgnoredInfrastructureSlices(t *testing.T) {
data := buildFixtureState(DefaultConfig)
if data.RemovedDockerHosts == nil {
t.Fatal("expected buildFixtureState to initialize RemovedDockerHosts")
}
if data.RemovedHostAgents == nil {
t.Fatal("expected buildFixtureState to initialize RemovedHostAgents")
}
if data.RemovedKubernetesClusters == nil {
t.Fatal("expected buildFixtureState to initialize RemovedKubernetesClusters")
}
}
// collectFixtureIdentities gathers every identity-bearing ID the fixture
// graph produces, keyed by category. Metric series resource IDs in the mock
// metrics store derive from these, so any per-boot churn here orphans stored
// history and breaks resource continuity across backend restarts.
func collectFixtureIdentities(graph FixtureGraph) map[string][]string {
ids := map[string][]string{}
add := func(category, id string) {
ids[category] = append(ids[category], id)
}
state := graph.State
for _, node := range state.Nodes {
add("node", node.ID)
}
for _, vm := range state.VMs {
add("vm", vm.ID)
}
for _, ct := range state.Containers {
add("container", ct.ID)
}
for _, host := range state.Hosts {
add("host", host.ID)
add("host-hostname", host.Hostname)
}
for _, dockerHost := range state.DockerHosts {
add("docker-host", dockerHost.ID)
for _, c := range dockerHost.Containers {
add("docker-container", c.ID)
}
}
for _, cluster := range state.KubernetesClusters {
add("k8s-cluster", cluster.ID)
for _, node := range cluster.Nodes {
add("k8s-node", node.UID)
}
for _, pod := range cluster.Pods {
add("k8s-pod", pod.UID)
}
for _, deployment := range cluster.Deployments {
add("k8s-deployment", deployment.UID)
}
for _, svc := range cluster.Services {
add("k8s-service", svc.UID)
}
}
for _, storage := range state.Storage {
add("storage", storage.ID)
}
for _, ceph := range state.CephClusters {
add("ceph-cluster", ceph.ID)
add("ceph-fsid", ceph.FSID)
}
for _, disk := range state.PhysicalDisks {
add("physical-disk", disk.ID)
}
for category := range ids {
sort.Strings(ids[category])
}
return ids
}
// TestFixtureIdentityStableAcrossBoots simulates two backend boots (the
// package-global rand source is in a different state for each build, exactly
// as two freshly seeded processes would be) and requires every identity set
// to match. Guards the generator's "stable IDs" contract end to end.
func TestFixtureIdentityStableAcrossBoots(t *testing.T) {
now := time.Date(2026, time.June, 10, 12, 0, 0, 0, time.UTC)
first := collectFixtureIdentities(buildFixtureGraph(DefaultConfig, now))
second := collectFixtureIdentities(buildFixtureGraph(DefaultConfig, now))
categories := map[string]struct{}{}
for category := range first {
categories[category] = struct{}{}
}
for category := range second {
categories[category] = struct{}{}
}
for category := range categories {
diff := diffIdentitySets(first[category], second[category])
if diff != "" {
t.Errorf("%s identities churned across boots:\n%s", category, diff)
}
}
}
func diffIdentitySets(first, second []string) string {
firstSet := make(map[string]struct{}, len(first))
for _, id := range first {
firstSet[id] = struct{}{}
}
secondSet := make(map[string]struct{}, len(second))
for _, id := range second {
secondSet[id] = struct{}{}
}
var lines []string
for _, id := range first {
if _, ok := secondSet[id]; !ok {
lines = append(lines, fmt.Sprintf(" only in first boot: %s", id))
}
}
for _, id := range second {
if _, ok := firstSet[id]; !ok {
lines = append(lines, fmt.Sprintf(" only in second boot: %s", id))
}
}
if len(lines) == 0 {
return ""
}
sort.Strings(lines)
out := ""
for _, line := range lines {
out += line + "\n"
}
return out
}