Pulse/internal/resources/store_test.go
rcourtman 3fdf753a5b Enhance devcontainer and CI workflows
- Add persistent volume mounts for Go/npm caches (faster rebuilds)
- Add shell config with helpful aliases and custom prompt
- Add comprehensive devcontainer documentation
- Add pre-commit hooks for Go formatting and linting
- Use go-version-file in CI workflows instead of hardcoded versions
- Simplify docker compose commands with --wait flag
- Add gitignore entries for devcontainer auth files

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-01 22:29:15 +00:00

779 lines
20 KiB
Go

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)
}
}