package node_test import ( "sync" "sync/atomic" "testing" toki "git.toki-labs.com/toki/proto-socket/go" edgenode "iop/apps/edge/internal/node" "iop/packages/go/config" ) func TestRegistry_RegisterAndCount(t *testing.T) { reg := edgenode.NewRegistry() entry := &edgenode.NodeEntry{ NodeID: "node-001", Alias: "local-node", Client: &toki.TcpClient{}, } reg.Register(entry) if reg.Count() != 1 { t.Fatalf("expected 1 node, got %d", reg.Count()) } } func TestRegistryRegisterIfAbsentRejectsConcurrentDuplicate(t *testing.T) { reg := edgenode.NewRegistry() var accepted int32 var wg sync.WaitGroup start := make(chan struct{}) for i := 0; i < 32; i++ { wg.Add(1) go func() { defer wg.Done() <-start if reg.RegisterIfAbsent(&edgenode.NodeEntry{ NodeID: "node-dup", Alias: "dup", Client: &toki.TcpClient{}, }) { atomic.AddInt32(&accepted, 1) } }() } close(start) wg.Wait() if accepted != 1 { t.Fatalf("accepted registrations: got %d want 1", accepted) } if reg.Count() != 1 { t.Fatalf("registry count: got %d want 1", reg.Count()) } } func TestRegistryUnregisterIfClientIgnoresStaleConnection(t *testing.T) { reg := edgenode.NewRegistry() live := &toki.TcpClient{} stale := &toki.TcpClient{} reg.Register(&edgenode.NodeEntry{NodeID: "node-1", Alias: "alias-1", Client: live}) if reg.UnregisterIfClient("node-1", stale) { t.Fatal("stale client should not unregister live entry") } entry, ok := reg.Get("node-1") if !ok { t.Fatal("live entry was removed by stale client") } if entry.Client != live { t.Fatal("live entry client changed unexpectedly") } if !reg.UnregisterIfClient("node-1", live) { t.Fatal("live client should unregister entry") } if reg.Count() != 0 { t.Fatalf("registry count after live unregister: got %d want 0", reg.Count()) } } func TestRegistry_Resolve_ByAliasOrID(t *testing.T) { reg := edgenode.NewRegistry() entry := &edgenode.NodeEntry{ NodeID: "node-001", Alias: "local-node", } reg.Register(entry) // Resolve by ID if e, err := reg.Resolve("node-001"); err != nil || e.NodeID != "node-001" { t.Errorf("failed to resolve by ID: %v", err) } // Resolve by Alias if e, err := reg.Resolve("local-node"); err != nil || e.NodeID != "node-001" { t.Errorf("failed to resolve by Alias: %v", err) } // Resolve non-existent if _, err := reg.Resolve("unknown"); err == nil { t.Error("expected error for unknown node") } } func TestRegistry_Resolve_ByDisplayLabel(t *testing.T) { reg := edgenode.NewRegistry() reg.Register(&edgenode.NodeEntry{NodeID: "node-001", Alias: "local-node"}) reg.Register(&edgenode.NodeEntry{NodeID: "node-002", Alias: "remote-node"}) if e, err := reg.Resolve("node0"); err != nil || e.NodeID != "node-001" { t.Fatalf("Resolve(node0) = %+v, %v; want node-001", e, err) } if e, err := reg.Resolve("node1"); err != nil || e.NodeID != "node-002" { t.Fatalf("Resolve(node1) = %+v, %v; want node-002", e, err) } } func TestRegistry_Resolve_ImplicitSingleNodeOnly(t *testing.T) { reg := edgenode.NewRegistry() // Empty if _, err := reg.Resolve(""); err == nil { t.Error("expected error on empty registry") } // Single node reg.Register(&edgenode.NodeEntry{NodeID: "node-1"}) if e, err := reg.Resolve(""); err != nil || e.NodeID != "node-1" { t.Errorf("failed implicit resolve for single node: %v", err) } // Multiple nodes reg.Register(&edgenode.NodeEntry{NodeID: "node-2"}) if _, err := reg.Resolve(""); err == nil { t.Error("expected error for implicit resolve with multiple nodes") } } func TestRegistryRegisterPreservesAgentKind(t *testing.T) { reg := edgenode.NewRegistry() reg.Register(&edgenode.NodeEntry{ NodeID: "node-explicit", Alias: "explicit", AgentKind: config.AgentKindGenericNode, }) entry, ok := reg.Get("node-explicit") if !ok { t.Fatal("expected explicit entry to be registered") } if entry.AgentKind != config.AgentKindGenericNode { t.Fatalf("agent kind: got %q want %q", entry.AgentKind, config.AgentKindGenericNode) } if entry.LifecycleState != edgenode.LifecycleConnected { t.Fatalf("lifecycle: got %q want %q", entry.LifecycleState, edgenode.LifecycleConnected) } } func TestRegistryRegisterDefaultsKindAndLifecycle(t *testing.T) { reg := edgenode.NewRegistry() entry := &edgenode.NodeEntry{NodeID: "node-001", Alias: "local-node"} reg.Register(entry) if entry.AgentKind != config.AgentKindGenericNode { t.Fatalf("agent kind default: got %q want %q", entry.AgentKind, config.AgentKindGenericNode) } if entry.LifecycleState != edgenode.LifecycleConnected { t.Fatalf("lifecycle default: got %q want %q", entry.LifecycleState, edgenode.LifecycleConnected) } } func TestRegistry_UnregisterRemovesAlias(t *testing.T) { reg := edgenode.NewRegistry() reg.Register(&edgenode.NodeEntry{NodeID: "node-1", Alias: "alias-1"}) reg.Unregister("node-1") if reg.Count() != 0 { t.Fatalf("expected 0 nodes, got %d", reg.Count()) } if _, err := reg.Resolve("alias-1"); err == nil { t.Error("alias should be removed after unregister") } } func TestRegistryUpdateLifecycle(t *testing.T) { reg := edgenode.NewRegistry() if ok := reg.UpdateLifecycle("non-existent", edgenode.LifecycleOnline); ok { t.Error("expected false for updating non-existent node") } entry := &edgenode.NodeEntry{NodeID: "node-1", Alias: "alias-1"} reg.Register(entry) if ok := reg.UpdateLifecycle("node-1", edgenode.LifecycleOnline); !ok { t.Error("expected true for updating existing node") } retrieved, ok := reg.Get("node-1") if !ok { t.Fatal("failed to get node") } if retrieved.LifecycleState != edgenode.LifecycleOnline { t.Errorf("lifecycle: got %q, want %q", retrieved.LifecycleState, edgenode.LifecycleOnline) } }