spaxel/mothership/internal/simulator/virtual_state_test.go
jedarden cb01246657 feat: implement ambient dashboard mode with Canvas 2D renderer
Implement ambient display mode for wall-mounted tablets with:

- Canvas 2D renderer (ambient_renderer.js) with 2 Hz render rate
- Time-of-day palette transitions (morning/day/evening/night)
- Zone outlines, portal lines, node positions, person blobs
- Lerp-interpolated smooth movement (20% factor per frame)
- Auto-dim after 60s of no presence in ambient zone
- Alert mode with pulsing red background and acknowledge button
- Morning briefing overlay (15s display after 6am)
- System status indicator and time display

Files:
- dashboard/js/ambient_renderer.js: Canvas 2D rendering engine
- dashboard/js/ambient_briefing.js: Morning briefing overlay
- dashboard/js/ambient.test.js: Test suite
- dashboard/css/notifications.css: Notification styles
- dashboard/css/simulator.css: Simulator styles
- dashboard/js/notifications.js: Notification handling
- dashboard/js/simplemode.js: Simple mode logic
- dashboard/simple.html: Simple mode page

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-10 22:09:12 -04:00

905 lines
23 KiB
Go

package simulator
import (
"os"
"testing"
)
// Test helper to create a temporary store
func tempStore(t *testing.T) (*VirtualNodeStore, string) {
t.Helper()
tmpDir := t.TempDir()
space := DefaultSpace()
store, err := NewVirtualNodeStore(StoreConfig{
DataDir: tmpDir,
Space: space,
})
if err != nil {
t.Fatalf("Failed to create store: %v", err)
}
return store, tmpDir
}
// TestNewVirtualNodeStore tests store creation
func TestNewVirtualNodeStore(t *testing.T) {
store, tmpDir := tempStore(t)
defer store.Close()
// Check that data directory was created
if _, err := os.Stat(tmpDir); err != nil {
t.Errorf("Data directory not created: %v", err)
}
// Store should start empty
if store.Count() != 0 {
t.Errorf("New store should be empty, got %d nodes", store.Count())
}
// Check space
space := store.GetSpace()
if space == nil {
t.Error("Space should not be nil")
}
if space.ID != "default" {
t.Errorf("Expected space ID 'default', got '%s'", space.ID)
}
}
// TestVirtualNodeStore_CreateNode tests basic node creation
func TestVirtualNodeStore_CreateNode(t *testing.T) {
store, _ := tempStore(t)
defer store.Close()
// Create a virtual node
position := NewPoint(1.0, 2.0, 1.5)
state, err := store.CreateVirtualNode("node-1", "Test Node", position)
if err != nil {
t.Fatalf("Failed to create node: %v", err)
}
// Verify state
if state.ID != "node-1" {
t.Errorf("Expected ID 'node-1', got '%s'", state.ID)
}
if state.Name != "Test Node" {
t.Errorf("Expected name 'Test Node', got '%s'", state.Name)
}
if state.Type != NodeTypeVirtual {
t.Errorf("Expected type '%s', got '%s'", NodeTypeVirtual, state.Type)
}
if state.Role != RoleTXRX {
t.Errorf("Expected default role '%s', got '%s'", RoleTXRX, state.Role)
}
if !state.Enabled {
t.Error("New node should be enabled")
}
// Verify position
if state.Position.X != 1.0 || state.Position.Y != 2.0 || state.Position.Z != 1.5 {
t.Errorf("Position mismatch: got (%f, %f, %f)",
state.Position.X, state.Position.Y, state.Position.Z)
}
// Verify timestamps
if state.CreatedAt.IsZero() {
t.Error("CreatedAt should not be zero")
}
if state.UpdatedAt.IsZero() {
t.Error("UpdatedAt should not be zero")
}
// Verify node count
if store.Count() != 1 {
t.Errorf("Expected 1 node, got %d", store.Count())
}
}
// TestVirtualNodeStore_CreateAPNode tests AP node creation
func TestVirtualNodeStore_CreateAPNode(t *testing.T) {
store, _ := tempStore(t)
defer store.Close()
position := NewPoint(0, 0, 2.5)
state, err := store.CreateAPNode("ap-1", "Router", "AA:BB:CC:DD:EE:FF", 6, position)
if err != nil {
t.Fatalf("Failed to create AP node: %v", err)
}
// Verify AP-specific fields
if state.Type != NodeTypeAP {
t.Errorf("Expected type '%s', got '%s'", NodeTypeAP, state.Type)
}
if state.Role != RoleTX {
t.Errorf("AP should have TX role, got '%s'", state.Role)
}
if state.APBSSID != "AA:BB:CC:DD:EE:FF" {
t.Errorf("Expected BSSID 'AA:BB:CC:DD:EE:FF', got '%s'", state.APBSSID)
}
if state.APChannel != 6 {
t.Errorf("Expected channel 6, got %d", state.APChannel)
}
}
// TestVirtualNodeStore_DuplicateID tests duplicate node ID rejection
func TestVirtualNodeStore_DuplicateID(t *testing.T) {
store, _ := tempStore(t)
defer store.Close()
position := NewPoint(1.0, 2.0, 1.5)
_, err := store.CreateVirtualNode("node-1", "First", position)
if err != nil {
t.Fatalf("Failed to create first node: %v", err)
}
// Try to create with same ID
_, err = store.CreateVirtualNode("node-1", "Second", NewPoint(2.0, 3.0, 1.0))
if err == nil {
t.Error("Expected error when creating duplicate node ID")
}
}
// TestVirtualNodeStore_InvalidPosition tests position validation
func TestVirtualNodeStore_InvalidPosition(t *testing.T) {
store, _ := tempStore(t)
defer store.Close()
// Position outside space bounds (default space is 6x5x2.5)
invalidPos := NewPoint(10.0, 10.0, 10.0)
_, err := store.CreateVirtualNode("node-1", "Invalid", invalidPos)
if err == nil {
t.Error("Expected error for position outside space bounds")
}
}
// TestVirtualNodeStore_GetNode tests node retrieval
func TestVirtualNodeStore_GetNode(t *testing.T) {
store, _ := tempStore(t)
defer store.Close()
position := NewPoint(1.0, 2.0, 1.5)
_, err := store.CreateVirtualNode("node-1", "Test Node", position)
if err != nil {
t.Fatalf("Failed to create node: %v", err)
}
// Get existing node
state, err := store.GetNode("node-1")
if err != nil {
t.Fatalf("Failed to get node: %v", err)
}
if state.Name != "Test Node" {
t.Errorf("Expected name 'Test Node', got '%s'", state.Name)
}
// Get non-existent node
_, err = store.GetNode("non-existent")
if err == nil {
t.Error("Expected error for non-existent node")
}
}
// TestVirtualNodeStore_UpdateNodePosition tests position updates
func TestVirtualNodeStore_UpdateNodePosition(t *testing.T) {
store, _ := tempStore(t)
defer store.Close()
position := NewPoint(1.0, 2.0, 1.5)
_, err := store.CreateVirtualNode("node-1", "Test Node", position)
if err != nil {
t.Fatalf("Failed to create node: %v", err)
}
// Update position
newPos := NewPoint(3.0, 4.0, 2.0)
err = store.UpdateNodePosition("node-1", newPos)
if err != nil {
t.Fatalf("Failed to update position: %v", err)
}
// Verify update
state, _ := store.GetNode("node-1")
if state.Position.X != 3.0 || state.Position.Y != 4.0 || state.Position.Z != 2.0 {
t.Errorf("Position not updated: got (%f, %f, %f)",
state.Position.X, state.Position.Y, state.Position.Z)
}
// Try invalid position
invalidPos := NewPoint(100.0, 100.0, 100.0)
err = store.UpdateNodePosition("node-1", invalidPos)
if err == nil {
t.Error("Expected error for invalid position update")
}
}
// TestVirtualNodeStore_UpdateNodeRole tests role updates
func TestVirtualNodeStore_UpdateNodeRole(t *testing.T) {
store, _ := tempStore(t)
defer store.Close()
position := NewPoint(1.0, 2.0, 1.5)
_, err := store.CreateVirtualNode("node-1", "Test Node", position)
if err != nil {
t.Fatalf("Failed to create node: %v", err)
}
// Update role
err = store.UpdateNodeRole("node-1", RoleRX)
if err != nil {
t.Fatalf("Failed to update role: %v", err)
}
// Verify update
state, _ := store.GetNode("node-1")
if state.Role != RoleRX {
t.Errorf("Expected role '%s', got '%s'", RoleRX, state.Role)
}
}
// TestVirtualNodeStore_SetNodeEnabled tests enable/disable
func TestVirtualNodeStore_SetNodeEnabled(t *testing.T) {
store, _ := tempStore(t)
defer store.Close()
position := NewPoint(1.0, 2.0, 1.5)
_, err := store.CreateVirtualNode("node-1", "Test Node", position)
if err != nil {
t.Fatalf("Failed to create node: %v", err)
}
// Disable node
err = store.SetNodeEnabled("node-1", false)
if err != nil {
t.Fatalf("Failed to disable node: %v", err)
}
state, _ := store.GetNode("node-1")
if state.Enabled {
t.Error("Node should be disabled")
}
// Re-enable
err = store.SetNodeEnabled("node-1", true)
if err != nil {
t.Fatalf("Failed to enable node: %v", err)
}
state, _ = store.GetNode("node-1")
if !state.Enabled {
t.Error("Node should be enabled")
}
}
// TestVirtualNodeStore_UpdateNodeMetadata tests metadata updates
func TestVirtualNodeStore_UpdateNodeMetadata(t *testing.T) {
store, _ := tempStore(t)
defer store.Close()
position := NewPoint(1.0, 2.0, 1.5)
_, err := store.CreateVirtualNode("node-1", "Test Node", position)
if err != nil {
t.Fatalf("Failed to create node: %v", err)
}
// Update metadata
metadata := map[string]interface{}{
"location": "kitchen",
"priority": 1,
"notes": "Near window",
"installed": "2024-01-15",
}
err = store.UpdateNodeMetadata("node-1", metadata)
if err != nil {
t.Fatalf("Failed to update metadata: %v", err)
}
// Verify metadata
state, _ := store.GetNode("node-1")
if state.Metadata["location"] != "kitchen" {
t.Errorf("Metadata not updated: expected 'kitchen', got '%v'", state.Metadata["location"])
}
if state.Metadata["priority"] != 1 {
t.Errorf("Priority metadata incorrect: expected 1, got %v", state.Metadata["priority"])
}
}
// TestVirtualNodeStore_Tags tests tag management
func TestVirtualNodeStore_Tags(t *testing.T) {
store, _ := tempStore(t)
defer store.Close()
position := NewPoint(1.0, 2.0, 1.5)
_, err := store.CreateVirtualNode("node-1", "Test Node", position)
if err != nil {
t.Fatalf("Failed to create node: %v", err)
}
// Add tags
tags := []string{"kitchen", "window", "testing"}
for _, tag := range tags {
if err := store.AddTag("node-1", tag); err != nil {
t.Fatalf("Failed to add tag '%s': %v", tag, err)
}
}
// Verify tags
state, _ := store.GetNode("node-1")
if len(state.Tags) != 3 {
t.Errorf("Expected 3 tags, got %d", len(state.Tags))
}
// Add duplicate tag (should not duplicate)
if err := store.AddTag("node-1", "kitchen"); err != nil {
t.Fatalf("Failed to add duplicate tag: %v", err)
}
state, _ = store.GetNode("node-1")
if len(state.Tags) != 3 {
t.Errorf("Duplicate tag should not increase count: got %d", len(state.Tags))
}
// Remove tag
if err := store.RemoveTag("node-1", "window"); err != nil {
t.Fatalf("Failed to remove tag: %v", err)
}
state, _ = store.GetNode("node-1")
if len(state.Tags) != 2 {
t.Errorf("Expected 2 tags after removal, got %d", len(state.Tags))
}
// Remove non-existent tag (should be no-op)
if err := store.RemoveTag("node-1", "nonexistent"); err != nil {
t.Error("Removing non-existent tag should not error")
}
}
// TestVirtualNodeStore_DeleteNode tests node deletion
func TestVirtualNodeStore_DeleteNode(t *testing.T) {
store, _ := tempStore(t)
defer store.Close()
position := NewPoint(1.0, 2.0, 1.5)
_, err := store.CreateVirtualNode("node-1", "Test Node", position)
if err != nil {
t.Fatalf("Failed to create node: %v", err)
}
// Delete node
err = store.DeleteNode("node-1")
if err != nil {
t.Fatalf("Failed to delete node: %v", err)
}
// Verify deletion
if store.Count() != 0 {
t.Errorf("Expected 0 nodes after deletion, got %d", store.Count())
}
_, err = store.GetNode("node-1")
if err == nil {
t.Error("Expected error when getting deleted node")
}
// Delete non-existent node
err = store.DeleteNode("non-existent")
if err == nil {
t.Error("Expected error when deleting non-existent node")
}
}
// TestVirtualNodeStore_ListNodes tests listing nodes
func TestVirtualNodeStore_ListNodes(t *testing.T) {
store, _ := tempStore(t)
defer store.Close()
// Create multiple nodes
for i := 1; i <= 5; i++ {
position := NewPoint(float64(i), float64(i), 1.5)
_, err := store.CreateVirtualNode(
string(rune('0'+i)),
string(rune('A'+i)),
position,
)
if err != nil {
t.Fatalf("Failed to create node %d: %v", i, err)
}
}
// List all nodes
allNodes := store.ListNodes()
if len(allNodes) != 5 {
t.Errorf("Expected 5 nodes, got %d", len(allNodes))
}
// Disable one node
if err := store.SetNodeEnabled("3", false); err != nil {
t.Fatalf("Failed to disable node: %v", err)
}
// List enabled nodes
enabledNodes := store.ListEnabledNodes()
if len(enabledNodes) != 4 {
t.Errorf("Expected 4 enabled nodes, got %d", len(enabledNodes))
}
}
// TestVirtualNodeStore_ListNodesByType tests filtering by type
func TestVirtualNodeStore_ListNodesByType(t *testing.T) {
store, _ := tempStore(t)
defer store.Close()
// Create virtual nodes
for i := 1; i <= 3; i++ {
position := NewPoint(float64(i), 0, 1.5)
_, err := store.CreateVirtualNode(
string(rune('0'+i)),
string(rune('A'+i)),
position,
)
if err != nil {
t.Fatalf("Failed to create virtual node: %v", err)
}
}
// Create AP node
position := NewPoint(0, 0, 2.5)
_, err := store.CreateAPNode("ap-1", "Router", "AA:BB:CC:DD:EE:FF", 6, position)
if err != nil {
t.Fatalf("Failed to create AP node: %v", err)
}
// List virtual nodes
virtualNodes := store.ListNodesByType(NodeTypeVirtual)
if len(virtualNodes) != 3 {
t.Errorf("Expected 3 virtual nodes, got %d", len(virtualNodes))
}
// List AP nodes
apNodes := store.ListNodesByType(NodeTypeAP)
if len(apNodes) != 1 {
t.Errorf("Expected 1 AP node, got %d", len(apNodes))
}
}
// TestVirtualNodeStore_ListNodesByTag tests filtering by tag
func TestVirtualNodeStore_ListNodesByTag(t *testing.T) {
store, _ := tempStore(t)
defer store.Close()
// Create nodes with different tags
for i := 1; i <= 3; i++ {
position := NewPoint(float64(i), 0, 1.5)
_, err := store.CreateVirtualNode(
string(rune('0'+i)),
string(rune('A'+i)),
position,
)
if err != nil {
t.Fatalf("Failed to create node: %v", err)
}
}
// Tag first two nodes as "kitchen"
store.AddTag("1", "kitchen")
store.AddTag("2", "kitchen")
store.AddTag("3", "bedroom")
// List by tag
kitchenNodes := store.ListNodesByTag("kitchen")
if len(kitchenNodes) != 2 {
t.Errorf("Expected 2 kitchen nodes, got %d", len(kitchenNodes))
}
bedroomNodes := store.ListNodesByTag("bedroom")
if len(bedroomNodes) != 1 {
t.Errorf("Expected 1 bedroom node, got %d", len(bedroomNodes))
}
}
// TestVirtualNodeStore_Clear tests clearing all nodes
func TestVirtualNodeStore_Clear(t *testing.T) {
store, _ := tempStore(t)
defer store.Close()
// Create some nodes
for i := 1; i <= 3; i++ {
position := NewPoint(float64(i), 0, 1.5)
_, err := store.CreateVirtualNode(
string(rune('0'+i)),
string(rune('A'+i)),
position,
)
if err != nil {
t.Fatalf("Failed to create node: %v", err)
}
}
// Clear all
if err := store.Clear(); err != nil {
t.Fatalf("Failed to clear store: %v", err)
}
if store.Count() != 0 {
t.Errorf("Expected 0 nodes after clear, got %d", store.Count())
}
}
// TestVirtualNodeStore_Persistence tests saving and loading
func TestVirtualNodeStore_Persistence(t *testing.T) {
tmpDir := t.TempDir()
// Create first store and add nodes
space := DefaultSpace()
store1, err := NewVirtualNodeStore(StoreConfig{
DataDir: tmpDir,
Space: space,
})
if err != nil {
t.Fatalf("Failed to create first store: %v", err)
}
position := NewPoint(1.0, 2.0, 1.5)
_, err = store1.CreateVirtualNode("node-1", "Persisted Node", position)
if err != nil {
t.Fatalf("Failed to create node: %v", err)
}
store1.AddTag("node-1", "persistent")
store1.UpdateNodeMetadata("node-1", map[string]interface{}{
"test": "value",
})
// Close first store
if err := store1.Close(); err != nil {
t.Fatalf("Failed to close store: %v", err)
}
// Create new store (should load from disk)
store2, err := NewVirtualNodeStore(StoreConfig{
DataDir: tmpDir,
Space: space,
})
if err != nil {
t.Fatalf("Failed to create second store: %v", err)
}
defer store2.Close()
// Verify loaded state
if store2.Count() != 1 {
t.Errorf("Expected 1 loaded node, got %d", store2.Count())
}
state, err := store2.GetNode("node-1")
if err != nil {
t.Fatalf("Failed to get loaded node: %v", err)
}
if state.Name != "Persisted Node" {
t.Errorf("Expected name 'Persisted Node', got '%s'", state.Name)
}
if len(state.Tags) != 1 || state.Tags[0] != "persistent" {
t.Errorf("Tags not persisted: got %v", state.Tags)
}
if state.Metadata["test"] != "value" {
t.Errorf("Metadata not persisted: got %v", state.Metadata)
}
}
// TestVirtualNodeStore_UpdateSpace tests space updates
func TestVirtualNodeStore_UpdateSpace(t *testing.T) {
store, _ := tempStore(t)
defer store.Close()
// Create a node
position := NewPoint(1.0, 2.0, 1.5)
_, err := store.CreateVirtualNode("node-1", "Test Node", position)
if err != nil {
t.Fatalf("Failed to create node: %v", err)
}
// Update space to smaller bounds (node at (1.0, 2.0, 1.5) is now outside since Y=2.0 > MaxY=1.5)
newSpace := &Space{
ID: "smaller",
Name: "Smaller Space",
Rooms: []Room{{
ID: "room-1",
Name: "Small Room",
MinX: 0, MinY: 0, MinZ: 0,
MaxX: 1.5, MaxY: 1.5, MaxZ: 1.5,
}},
}
err = store.UpdateSpace(newSpace)
if err != nil {
t.Fatalf("Failed to update space: %v", err)
}
// Node should be disabled since it's outside the new bounds
state, _ := store.GetNode("node-1")
if state.Enabled {
t.Error("Node outside new bounds (Y=2.0 > MaxY=1.5) should be disabled")
}
// Shrink space further (node now outside)
tinySpace := &Space{
ID: "tiny",
Name: "Tiny Space",
Rooms: []Room{{
ID: "room-1",
Name: "Tiny Room",
MinX: 0, MinY: 0, MinZ: 0,
MaxX: 0.5, MaxY: 0.5, MaxZ: 0.5,
}},
}
err = store.UpdateSpace(tinySpace)
if err != nil {
t.Fatalf("Failed to shrink space: %v", err)
}
// Node should now be disabled
state, _ = store.GetNode("node-1")
if state.Enabled {
t.Error("Node outside new bounds should be disabled")
}
}
// TestVirtualNodeStore_ToNodeSet tests conversion to NodeSet
func TestVirtualNodeStore_ToNodeSet(t *testing.T) {
store, _ := tempStore(t)
defer store.Close()
// Create various nodes
position := NewPoint(1.0, 2.0, 1.5)
_, err := store.CreateVirtualNode("virtual-1", "Virtual Node", position)
if err != nil {
t.Fatalf("Failed to create virtual node: %v", err)
}
position = NewPoint(0, 0, 2.5)
_, err = store.CreateAPNode("ap-1", "Router", "AA:BB:CC:DD:EE:FF", 6, position)
if err != nil {
t.Fatalf("Failed to create AP node: %v", err)
}
// Convert to NodeSet
nodeSet := store.ToNodeSet()
if nodeSet.Count() != 2 {
t.Errorf("Expected 2 nodes in NodeSet, got %d", nodeSet.Count())
}
// Verify virtual node
virtualNode := nodeSet.GetByID("virtual-1")
if virtualNode == nil {
t.Error("Virtual node not in NodeSet")
} else if !virtualNode.IsVirtual() {
t.Error("Node should be marked as virtual")
}
// Verify AP node
apNode := nodeSet.GetByID("ap-1")
if apNode == nil {
t.Error("AP node not in NodeSet")
} else if !apNode.IsAP() {
t.Error("Node should be marked as AP")
}
}
// TestVirtualNodeStore_ImportFromNodeSet tests importing from NodeSet
func TestVirtualNodeStore_ImportFromNodeSet(t *testing.T) {
store, _ := tempStore(t)
defer store.Close()
// Create a NodeSet
nodeSet := NewNodeSet()
nodeSet.AddVirtualNode("import-1", "Imported Virtual", NewPoint(1.0, 2.0, 1.5))
nodeSet.AddAPNode("import-2", "Imported AP", "BB:CC:DD:EE:FF:00", 11, NewPoint(0, 0, 2))
// Import
if err := store.ImportFromNodeSet(nodeSet); err != nil {
t.Fatalf("Failed to import NodeSet: %v", err)
}
if store.Count() != 2 {
t.Errorf("Expected 2 nodes after import, got %d", store.Count())
}
// Verify imported nodes
state1, _ := store.GetNode("import-1")
if state1.Name != "Imported Virtual" {
t.Errorf("Expected name 'Imported Virtual', got '%s'", state1.Name)
}
state2, _ := store.GetNode("import-2")
if state2.APBSSID != "BB:CC:DD:EE:FF:00" {
t.Errorf("Expected BSSID 'BB:CC:DD:EE:FF:00', got '%s'", state2.APBSSID)
}
}
// TestVirtualNodeStore_Summary tests summary generation
func TestVirtualNodeStore_Summary(t *testing.T) {
store, _ := tempStore(t)
defer store.Close()
// Create various nodes
store.CreateVirtualNode("node-1", "Node 1", NewPoint(1.0, 1.0, 1.5))
store.CreateVirtualNode("node-2", "Node 2", NewPoint(3.0, 4.0, 2.0))
store.CreateAPNode("ap-1", "Router", "AA:BB:CC:DD:EE:FF", 6, NewPoint(0, 0, 2.5))
store.AddTag("node-1", "kitchen")
store.AddTag("node-2", "kitchen")
// Disable one node
store.SetNodeEnabled("node-2", false)
// Get summary
summary := store.Summary()
if summary.TotalCount != 3 {
t.Errorf("Expected total count 3, got %d", summary.TotalCount)
}
if summary.EnabledCount != 2 {
t.Errorf("Expected enabled count 2, got %d", summary.EnabledCount)
}
if summary.VirtualCount != 2 {
t.Errorf("Expected virtual count 2, got %d", summary.VirtualCount)
}
if summary.APCount != 1 {
t.Errorf("Expected AP count 1, got %d", summary.APCount)
}
if summary.ByType["virtual"] != 2 {
t.Errorf("Expected 2 virtual nodes by type, got %d", summary.ByType["virtual"])
}
if summary.ByTag["kitchen"] != 2 {
t.Errorf("Expected 2 nodes with kitchen tag, got %d", summary.ByTag["kitchen"])
}
// Check bounding box
if summary.BoundingBox.MinX != 0 {
t.Errorf("Expected min X 0, got %f", summary.BoundingBox.MinX)
}
if summary.BoundingBox.MaxX != 3.0 {
t.Errorf("Expected max X 3.0, got %f", summary.BoundingBox.MaxX)
}
// Check timestamps
if summary.FirstCreated == nil {
t.Error("FirstCreated should not be nil")
}
if summary.LastUpdated == nil {
t.Error("LastUpdated should not be nil")
}
}
// TestVirtualNodeStore_Close tests store closing
func TestVirtualNodeStore_Close(t *testing.T) {
store, _ := tempStore(t)
// Create a node
position := NewPoint(1.0, 2.0, 1.5)
_, err := store.CreateVirtualNode("node-1", "Test Node", position)
if err != nil {
t.Fatalf("Failed to create node: %v", err)
}
// Close store
if err := store.Close(); err != nil {
t.Fatalf("Failed to close store: %v", err)
}
// Operations should fail after close
_, err = store.CreateVirtualNode("node-2", "Should Fail", NewPoint(2.0, 3.0, 1.5))
if err == nil {
t.Error("Expected error when creating node after close")
}
_, err = store.GetNode("node-1")
if err == nil {
t.Error("Expected error when getting node after close")
}
// Double close should be safe
if err := store.Close(); err != nil {
t.Errorf("Double close should be safe: %v", err)
}
}
// TestVirtualNodeStore_Immutability tests that returned states are copies
func TestVirtualNodeStore_Immutability(t *testing.T) {
store, _ := tempStore(t)
defer store.Close()
position := NewPoint(1.0, 2.0, 1.5)
_, err := store.CreateVirtualNode("node-1", "Test Node", position)
if err != nil {
t.Fatalf("Failed to create node: %v", err)
}
// Get node
state1, err := store.GetNode("node-1")
if err != nil {
t.Fatalf("Failed to get node: %v", err)
}
// Modify returned state
state1.Name = "Modified"
state1.Position.X = 999.0
state1.Metadata["test"] = "injected"
// Get node again
state2, err := store.GetNode("node-1")
if err != nil {
t.Fatalf("Failed to get node: %v", err)
}
// Should have original values
if state2.Name == "Modified" {
t.Error("Returned state should be a copy, modifications should not affect stored state")
}
if state2.Position.X == 999.0 {
t.Error("Position modification should not affect stored state")
}
if _, exists := state2.Metadata["test"]; exists {
t.Error("Metadata injection should not affect stored state")
}
// ListNodes should also return copies
nodes := store.ListNodes()
nodes[0].Name = "ListModified"
state3, _ := store.GetNode("node-1")
if state3.Name == "ListModified" {
t.Error("ListNodes should return copies")
}
}
// TestVirtualNodeStore_StateIsolation tests that each node's state is independent
func TestVirtualNodeStore_StateIsolation(t *testing.T) {
store, _ := tempStore(t)
defer store.Close()
// Create two nodes
_, err := store.CreateVirtualNode("node-1", "Node 1", NewPoint(1.0, 1.0, 1.5))
if err != nil {
t.Fatalf("Failed to create node 1: %v", err)
}
_, err = store.CreateVirtualNode("node-2", "Node 2", NewPoint(3.0, 3.0, 1.5))
if err != nil {
t.Fatalf("Failed to create node 2: %v", err)
}
// Add tag to node 1
store.AddTag("node-1", "tag1")
// Add tag to node 2
store.AddTag("node-2", "tag2")
// Verify isolation
state1, _ := store.GetNode("node-1")
state2, _ := store.GetNode("node-2")
if len(state1.Tags) != 1 || state1.Tags[0] != "tag1" {
t.Errorf("Node 1 tags incorrect: %v", state1.Tags)
}
if len(state2.Tags) != 1 || state2.Tags[0] != "tag2" {
t.Errorf("Node 2 tags incorrect: %v", state2.Tags)
}
}