package resources import ( "testing" "time" ) func TestStoreUpsertAndGet(t *testing.T) { store := NewStore() r := Resource{ ID: "test-1", Type: ResourceTypeNode, Name: "node1", PlatformType: PlatformProxmoxPVE, SourceType: SourceAPI, Status: StatusOnline, LastSeen: time.Now(), } id := store.Upsert(r) if id != "test-1" { t.Errorf("Expected ID test-1, got %s", id) } retrieved, ok := store.Get("test-1") if !ok { t.Fatal("Failed to retrieve resource") } if retrieved.Name != "node1" { t.Errorf("Expected name node1, got %s", retrieved.Name) } } func TestStoreGetAll(t *testing.T) { store := NewStore() store.Upsert(Resource{ID: "1", Type: ResourceTypeNode, LastSeen: time.Now()}) store.Upsert(Resource{ID: "2", Type: ResourceTypeVM, LastSeen: time.Now()}) store.Upsert(Resource{ID: "3", Type: ResourceTypeContainer, LastSeen: time.Now()}) all := store.GetAll() if len(all) != 3 { t.Errorf("Expected 3 resources, got %d", len(all)) } } func TestStoreGetByType(t *testing.T) { store := NewStore() store.Upsert(Resource{ID: "node1", Type: ResourceTypeNode, LastSeen: time.Now()}) store.Upsert(Resource{ID: "node2", Type: ResourceTypeNode, LastSeen: time.Now()}) store.Upsert(Resource{ID: "vm1", Type: ResourceTypeVM, LastSeen: time.Now()}) nodes := store.GetByType(ResourceTypeNode) if len(nodes) != 2 { t.Errorf("Expected 2 nodes, got %d", len(nodes)) } vms := store.GetByType(ResourceTypeVM) if len(vms) != 1 { t.Errorf("Expected 1 VM, got %d", len(vms)) } } func TestStoreGetByPlatform(t *testing.T) { store := NewStore() store.Upsert(Resource{ID: "1", PlatformType: PlatformProxmoxPVE, LastSeen: time.Now()}) store.Upsert(Resource{ID: "2", PlatformType: PlatformProxmoxPVE, LastSeen: time.Now()}) store.Upsert(Resource{ID: "3", PlatformType: PlatformDocker, LastSeen: time.Now()}) pve := store.GetByPlatform(PlatformProxmoxPVE) if len(pve) != 2 { t.Errorf("Expected 2 PVE resources, got %d", len(pve)) } docker := store.GetByPlatform(PlatformDocker) if len(docker) != 1 { t.Errorf("Expected 1 Docker resource, got %d", len(docker)) } } func TestStoreGetInfrastructureAndWorkloads(t *testing.T) { store := NewStore() store.Upsert(Resource{ID: "node1", Type: ResourceTypeNode, LastSeen: time.Now()}) store.Upsert(Resource{ID: "host1", Type: ResourceTypeHost, LastSeen: time.Now()}) store.Upsert(Resource{ID: "vm1", Type: ResourceTypeVM, LastSeen: time.Now()}) store.Upsert(Resource{ID: "ct1", Type: ResourceTypeContainer, LastSeen: time.Now()}) store.Upsert(Resource{ID: "dc1", Type: ResourceTypeDockerContainer, LastSeen: time.Now()}) infra := store.GetInfrastructure() if len(infra) != 2 { t.Errorf("Expected 2 infrastructure resources, got %d", len(infra)) } workloads := store.GetWorkloads() if len(workloads) != 3 { t.Errorf("Expected 3 workload resources, got %d", len(workloads)) } } func TestStoreGetChildren(t *testing.T) { store := NewStore() store.Upsert(Resource{ID: "node1", Type: ResourceTypeNode, LastSeen: time.Now()}) store.Upsert(Resource{ID: "vm1", Type: ResourceTypeVM, ParentID: "node1", LastSeen: time.Now()}) store.Upsert(Resource{ID: "vm2", Type: ResourceTypeVM, ParentID: "node1", LastSeen: time.Now()}) store.Upsert(Resource{ID: "vm3", Type: ResourceTypeVM, ParentID: "node2", LastSeen: time.Now()}) children := store.GetChildren("node1") if len(children) != 2 { t.Errorf("Expected 2 children of node1, got %d", len(children)) } } func TestStoreRemove(t *testing.T) { store := NewStore() store.Upsert(Resource{ID: "test-1", LastSeen: time.Now()}) store.Upsert(Resource{ID: "test-2", LastSeen: time.Now()}) if len(store.GetAll()) != 2 { t.Fatal("Expected 2 resources before remove") } store.Remove("test-1") if len(store.GetAll()) != 1 { t.Error("Expected 1 resource after remove") } _, ok := store.Get("test-1") if ok { t.Error("Removed resource should not be retrievable") } } func TestDeduplicationByHostname(t *testing.T) { store := NewStore() now := time.Now() // Add a Proxmox node (API source) nodeResource := Resource{ ID: "pve1/node/server1", Type: ResourceTypeNode, Name: "server1", PlatformType: PlatformProxmoxPVE, SourceType: SourceAPI, Status: StatusOnline, CPU: &MetricValue{Current: 50.0}, LastSeen: now, Identity: &ResourceIdentity{ Hostname: "server1", }, } store.Upsert(nodeResource) // Add a host agent for the same server (agent source) // Different types should coexist, not merge hostResource := Resource{ ID: "host-agent/server1", Type: ResourceTypeHost, Name: "server1", PlatformType: PlatformHostAgent, SourceType: SourceAgent, Status: StatusOnline, CPU: &MetricValue{Current: 55.0}, LastSeen: now, Identity: &ResourceIdentity{ Hostname: "server1", }, } store.Upsert(hostResource) // Different types should coexist (node + host = 2 resources) all := store.GetAll() if len(all) != 2 { t.Errorf("Expected 2 resources (node + host coexist), got %d", len(all)) } // Both should be retrievable _, ok := store.Get("pve1/node/server1") if !ok { t.Error("Node resource should be retrievable") } _, ok = store.Get("host-agent/server1") if !ok { t.Error("Host resource should be retrievable") } // Now test same-type deduplication: add another node with same hostname nodeResource2 := Resource{ ID: "pve2/node/server1", Type: ResourceTypeNode, Name: "server1", PlatformType: PlatformProxmoxPVE, SourceType: SourceAPI, Status: StatusOnline, CPU: &MetricValue{Current: 60.0}, LastSeen: now.Add(time.Second), // Newer Identity: &ResourceIdentity{ Hostname: "server1", }, } store.Upsert(nodeResource2) // Should still have 2 (newer node replaces old node, host remains) all = store.GetAll() if len(all) != 2 { t.Errorf("Expected 2 resources after same-type dedup, got %d", len(all)) } // The newer node should have replaced the old one r, ok := store.Get("pve2/node/server1") if !ok { t.Error("Newer node should be present") } if r.CPU.Current != 60.0 { t.Errorf("Expected CPU 60.0 from newer node, got %f", r.CPU.Current) } } func TestDeduplicationByMachineID(t *testing.T) { store := NewStore() now := time.Now() machineID := "abc-123-def-456" // Add a Docker host dockerHost := Resource{ ID: "docker-host-1", Type: ResourceTypeDockerHost, Name: "server-different-name", PlatformType: PlatformDocker, SourceType: SourceAgent, Status: StatusOnline, LastSeen: now, Identity: &ResourceIdentity{ Hostname: "server-different-name", MachineID: machineID, }, } store.Upsert(dockerHost) // Add a host agent with the same machine ID but different type // Different types should coexist hostAgent := Resource{ ID: "host-agent-1", Type: ResourceTypeHost, Name: "server-production", PlatformType: PlatformHostAgent, SourceType: SourceAgent, Status: StatusOnline, LastSeen: now.Add(time.Second), Identity: &ResourceIdentity{ Hostname: "server-production", MachineID: machineID, }, } store.Upsert(hostAgent) // Different types should coexist (docker-host + host = 2 resources) all := store.GetAll() if len(all) != 2 { t.Errorf("Expected 2 resources (different types coexist with same machineID), got %d", len(all)) } // Now test same-type deduplication: add another host with same machine ID hostAgent2 := Resource{ ID: "host-agent-2", Type: ResourceTypeHost, // Same type as first host Name: "server-newer", PlatformType: PlatformHostAgent, SourceType: SourceAgent, Status: StatusOnline, LastSeen: now.Add(2 * time.Second), // Newer Identity: &ResourceIdentity{ Hostname: "server-newer", MachineID: machineID, }, } store.Upsert(hostAgent2) // Should still have 2 (newer host replaces old host, docker-host remains) all = store.GetAll() if len(all) != 2 { t.Errorf("Expected 2 resources after same-type dedup, got %d", len(all)) } } func TestDeduplicationByIP(t *testing.T) { store := NewStore() now := time.Now() sharedIP := "192.168.1.100" // Add a Proxmox node node := Resource{ ID: "node-1", Type: ResourceTypeNode, Name: "pve-node", PlatformType: PlatformProxmoxPVE, SourceType: SourceAPI, Status: StatusOnline, LastSeen: now, Identity: &ResourceIdentity{ Hostname: "pve-node", IPs: []string{sharedIP}, }, } store.Upsert(node) // Add a host agent with the same IP but different type // Different types should coexist host := Resource{ ID: "host-1", Type: ResourceTypeHost, Name: "different-hostname", PlatformType: PlatformHostAgent, SourceType: SourceAgent, Status: StatusOnline, LastSeen: now, Identity: &ResourceIdentity{ Hostname: "different-hostname", IPs: []string{sharedIP}, }, } store.Upsert(host) // Different types should coexist (node + host = 2 resources) all := store.GetAll() if len(all) != 2 { t.Errorf("Expected 2 resources (different types coexist), got %d", len(all)) } // Both should be retrievable _, ok := store.Get("node-1") if !ok { t.Error("Node should be retrievable") } _, ok = store.Get("host-1") if !ok { t.Error("Host should be retrievable") } } func TestNoDeduplicationForWorkloads(t *testing.T) { store := NewStore() now := time.Now() // VMs with the same hostname should NOT be deduplicated // (they're workloads, not infrastructure) vm1 := Resource{ ID: "pve1/vm/100", Type: ResourceTypeVM, Name: "webserver", PlatformType: PlatformProxmoxPVE, SourceType: SourceAPI, LastSeen: now, Identity: &ResourceIdentity{ Hostname: "webserver", // Same hostname }, } store.Upsert(vm1) vm2 := Resource{ ID: "pve2/vm/100", Type: ResourceTypeVM, Name: "webserver", PlatformType: PlatformProxmoxPVE, SourceType: SourceAPI, LastSeen: now, Identity: &ResourceIdentity{ Hostname: "webserver", // Same hostname }, } store.Upsert(vm2) // Both VMs should exist (workloads are not deduplicated) all := store.GetAll() if len(all) != 2 { t.Errorf("Expected 2 VMs (no dedup for workloads), got %d", len(all)) } } func TestNoDeduplicationForLocalhost(t *testing.T) { store := NewStore() now := time.Now() host1 := Resource{ ID: "host-1", Type: ResourceTypeHost, Name: "server1", PlatformType: PlatformHostAgent, SourceType: SourceAgent, LastSeen: now, Identity: &ResourceIdentity{ Hostname: "server1", IPs: []string{"127.0.0.1", "192.168.1.1"}, }, } store.Upsert(host1) host2 := Resource{ ID: "host-2", Type: ResourceTypeHost, Name: "server2", PlatformType: PlatformHostAgent, SourceType: SourceAgent, LastSeen: now, Identity: &ResourceIdentity{ Hostname: "server2", IPs: []string{"127.0.0.1", "192.168.1.2"}, // Both have localhost }, } store.Upsert(host2) // Both should exist (127.0.0.1 shouldn't trigger dedup) all := store.GetAll() if len(all) != 2 { t.Errorf("Expected 2 hosts (localhost shouldn't dedup), got %d", len(all)) } } func TestStoreStats(t *testing.T) { store := NewStore() store.Upsert(Resource{ID: "1", Type: ResourceTypeNode, PlatformType: PlatformProxmoxPVE, LastSeen: time.Now()}) store.Upsert(Resource{ID: "2", Type: ResourceTypeNode, PlatformType: PlatformProxmoxPVE, LastSeen: time.Now()}) store.Upsert(Resource{ID: "3", Type: ResourceTypeVM, PlatformType: PlatformProxmoxPVE, LastSeen: time.Now()}) store.Upsert(Resource{ID: "4", Type: ResourceTypeDockerHost, PlatformType: PlatformDocker, LastSeen: time.Now()}) stats := store.GetStats() if stats.TotalResources != 4 { t.Errorf("Expected 4 total resources, got %d", stats.TotalResources) } if stats.ByType[ResourceTypeNode] != 2 { t.Errorf("Expected 2 nodes, got %d", stats.ByType[ResourceTypeNode]) } if stats.ByPlatform[PlatformProxmoxPVE] != 3 { t.Errorf("Expected 3 PVE resources, got %d", stats.ByPlatform[PlatformProxmoxPVE]) } } func TestStoreQuery(t *testing.T) { store := NewStore() store.Upsert(Resource{ ID: "1", Type: ResourceTypeNode, PlatformType: PlatformProxmoxPVE, Status: StatusOnline, LastSeen: time.Now(), }) store.Upsert(Resource{ ID: "2", Type: ResourceTypeVM, PlatformType: PlatformProxmoxPVE, Status: StatusRunning, ParentID: "1", LastSeen: time.Now(), }) store.Upsert(Resource{ ID: "3", Type: ResourceTypeVM, PlatformType: PlatformProxmoxPVE, Status: StatusStopped, ParentID: "1", LastSeen: time.Now(), }) store.Upsert(Resource{ ID: "4", Type: ResourceTypeDockerContainer, PlatformType: PlatformDocker, Status: StatusRunning, LastSeen: time.Now(), }) // Query by type vms := store.Query().OfType(ResourceTypeVM).Execute() if len(vms) != 2 { t.Errorf("Expected 2 VMs, got %d", len(vms)) } // Query by status running := store.Query().WithStatus(StatusRunning).Execute() if len(running) != 2 { t.Errorf("Expected 2 running resources, got %d", len(running)) } // Query by platform pve := store.Query().FromPlatform(PlatformProxmoxPVE).Execute() if len(pve) != 3 { t.Errorf("Expected 3 PVE resources, got %d", len(pve)) } // Query by parent node1Children := store.Query().WithParent("1").Execute() if len(node1Children) != 2 { t.Errorf("Expected 2 children of node 1, got %d", len(node1Children)) } // Combined query runningVMs := store.Query(). OfType(ResourceTypeVM). WithStatus(StatusRunning). Execute() if len(runningVMs) != 1 { t.Errorf("Expected 1 running VM, got %d", len(runningVMs)) } // Count count := store.Query().OfType(ResourceTypeVM).Count() if count != 2 { t.Errorf("Expected count 2, got %d", count) } // Limit limited := store.Query().Limit(2).Execute() if len(limited) > 2 { t.Errorf("Expected at most 2 results, got %d", len(limited)) } } func TestMarkStale(t *testing.T) { store := NewStore() old := time.Now().Add(-2 * time.Hour) recent := time.Now() store.Upsert(Resource{ ID: "old-1", Status: StatusOnline, LastSeen: old, }) store.Upsert(Resource{ ID: "recent-1", Status: StatusOnline, LastSeen: recent, }) stale := store.MarkStale(time.Hour) if len(stale) != 1 { t.Errorf("Expected 1 stale resource, got %d", len(stale)) } r, _ := store.Get("old-1") if r.Status != StatusDegraded { t.Errorf("Expected stale resource to be degraded, got %s", r.Status) } r, _ = store.Get("recent-1") if r.Status != StatusOnline { t.Errorf("Recent resource should still be online, got %s", r.Status) } } func TestPruneStale(t *testing.T) { store := NewStore() veryOld := time.Now().Add(-48 * time.Hour) old := time.Now().Add(-2 * time.Hour) recent := time.Now() store.Upsert(Resource{ID: "very-old", LastSeen: veryOld}) store.Upsert(Resource{ID: "old", LastSeen: old}) store.Upsert(Resource{ID: "recent", LastSeen: recent}) removed := store.PruneStale(time.Hour, 24*time.Hour) if len(removed) != 1 { t.Errorf("Expected 1 removed resource, got %d", len(removed)) } if len(store.GetAll()) != 2 { t.Errorf("Expected 2 remaining resources, got %d", len(store.GetAll())) } } func TestAPIToAgentPreference(t *testing.T) { store := NewStore() now := time.Now() // First, add an API-sourced host apiResource := Resource{ ID: "api-host", Type: ResourceTypeHost, // Same type as agent Name: "server", PlatformType: PlatformProxmoxPVE, SourceType: SourceAPI, CPU: &MetricValue{Current: 50.0}, LastSeen: now, Identity: &ResourceIdentity{ Hostname: "server", }, } store.Upsert(apiResource) // Then, add an agent resource for the same machine (same type, different source) agentResource := Resource{ ID: "agent-host", Type: ResourceTypeHost, // Same type Name: "server", PlatformType: PlatformHostAgent, SourceType: SourceAgent, CPU: &MetricValue{Current: 55.0}, LastSeen: now, Identity: &ResourceIdentity{ Hostname: "server", }, } store.Upsert(agentResource) // Only agent resource should exist (same type = dedup, agent preferred) all := store.GetAll() if len(all) != 1 { t.Fatalf("Expected 1 resource (same type dedup), got %d", len(all)) } if all[0].SourceType != SourceAgent { t.Errorf("Expected agent source type (preferred), got %s", all[0].SourceType) } } func TestGetTopByCPU(t *testing.T) { store := NewStore() now := time.Now() store.Upsert(Resource{ ID: "vm1", Type: ResourceTypeVM, Name: "low-cpu-vm", CPU: &MetricValue{Current: 20.0}, LastSeen: now, }) store.Upsert(Resource{ ID: "vm2", Type: ResourceTypeVM, Name: "high-cpu-vm", CPU: &MetricValue{Current: 85.0}, LastSeen: now, }) store.Upsert(Resource{ ID: "node1", Type: ResourceTypeNode, Name: "busy-node", CPU: &MetricValue{Current: 75.0}, LastSeen: now, }) // Get top 2 by CPU top := store.GetTopByCPU(2, nil) if len(top) != 2 { t.Fatalf("Expected 2 resources, got %d", len(top)) } if top[0].Name != "high-cpu-vm" { t.Errorf("Expected high-cpu-vm first, got %s", top[0].Name) } if top[1].Name != "busy-node" { t.Errorf("Expected busy-node second, got %s", top[1].Name) } // Filter by type topVMs := store.GetTopByCPU(10, []ResourceType{ResourceTypeVM}) if len(topVMs) != 2 { t.Errorf("Expected 2 VMs, got %d", len(topVMs)) } } func TestGetRelated(t *testing.T) { store := NewStore() now := time.Now() store.Upsert(Resource{ ID: "node1", Type: ResourceTypeNode, Name: "parent-node", ClusterID: "cluster1", LastSeen: now, }) store.Upsert(Resource{ ID: "vm1", Type: ResourceTypeVM, Name: "child-vm-1", ParentID: "node1", ClusterID: "cluster1", LastSeen: now, }) store.Upsert(Resource{ ID: "vm2", Type: ResourceTypeVM, Name: "child-vm-2", ParentID: "node1", ClusterID: "cluster1", LastSeen: now, }) store.Upsert(Resource{ ID: "node2", Type: ResourceTypeNode, Name: "cluster-peer", ClusterID: "cluster1", LastSeen: now, }) // Get related resources for vm1 related := store.GetRelated("vm1") // Should have parent if parent, ok := related["parent"]; !ok || len(parent) != 1 { t.Error("Expected 1 parent") } // Should have sibling (vm2) if siblings, ok := related["siblings"]; !ok || len(siblings) != 1 { t.Errorf("Expected 1 sibling, got %d", len(related["siblings"])) } // Should have cluster members if cluster, ok := related["cluster_members"]; !ok || len(cluster) != 3 { t.Errorf("Expected 3 cluster members, got %d", len(related["cluster_members"])) } } func TestGetResourceSummary(t *testing.T) { store := NewStore() now := time.Now() store.Upsert(Resource{ ID: "node1", Type: ResourceTypeNode, PlatformType: PlatformProxmoxPVE, Status: StatusOnline, CPU: &MetricValue{Current: 50}, Memory: &MetricValue{Current: 50}, // 50% usage LastSeen: now, }) store.Upsert(Resource{ ID: "vm1", Type: ResourceTypeVM, PlatformType: PlatformProxmoxPVE, Status: StatusRunning, CPU: &MetricValue{Current: 70}, Memory: &MetricValue{Current: 50}, // 50% usage LastSeen: now, }) store.Upsert(Resource{ ID: "vm2", Type: ResourceTypeVM, PlatformType: PlatformProxmoxPVE, Status: StatusStopped, LastSeen: now, }) summary := store.GetResourceSummary() if summary.TotalResources != 3 { t.Errorf("Expected 3 total resources, got %d", summary.TotalResources) } if summary.Healthy != 2 { t.Errorf("Expected 2 healthy, got %d", summary.Healthy) } if summary.Offline != 1 { t.Errorf("Expected 1 offline, got %d", summary.Offline) } // Check per-type stats vmStats := summary.ByType[ResourceTypeVM] if vmStats.Count != 2 { t.Errorf("Expected 2 VMs, got %d", vmStats.Count) } }