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>
905 lines
23 KiB
Go
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)
|
|
}
|
|
}
|