This repository has been archived on 2026-04-03. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
core/server/cluster/registry_test.go
Jeffrey Smith 35d2309450
All checks were successful
CI/CD / detect-changes (pull_request) Successful in 21s
CI/CD / test-frontend (pull_request) Successful in 5s
CI/CD / test-go-pg (pull_request) Successful in 2m58s
CI/CD / test-sqlite (pull_request) Successful in 3m2s
CI/CD / build-and-deploy (pull_request) Successful in 1m15s
Feat v0.6.0 cluster registry + HA
PG-backed cluster registry for horizontal scaling — zero new
infrastructure (no etcd/Consul/Redis). MVP convergence point.

- node_registry UNLOGGED table (migration 013)
- Self-registration, heartbeat tick (10s), stale sweep (30s),
  self-eviction (os.Exit on 0 rows → K8s restarts)
- GET /api/v1/admin/cluster returns nodes with runtime stats
- Health endpoints include node_id + cluster size/peers
- cluster-dashboard admin surface with auto-refresh
- 3-replica E2E test (docker-compose-e2e.yml + ci/e2e-cluster-test.sh)
- chat + cluster-dashboard added to curated default bundle
- 5 new tests (3 unit + 2 handler), all passing

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 22:20:56 +00:00

113 lines
2.8 KiB
Go

package cluster
import (
"context"
"encoding/json"
"testing"
"time"
"switchboard-core/store"
)
// mockClusterStore implements store.ClusterStore for unit tests.
type mockClusterStore struct {
nodes []store.ClusterNode
heartbeatRows int64
heartbeatErr error
sweepCalled bool
registerCalled bool
lastStats json.RawMessage
}
func (m *mockClusterStore) Register(_ context.Context, nodeID, endpoint string) error {
m.registerCalled = true
return nil
}
func (m *mockClusterStore) Heartbeat(_ context.Context, nodeID string, stats json.RawMessage) (int64, error) {
m.lastStats = stats
return m.heartbeatRows, m.heartbeatErr
}
func (m *mockClusterStore) SweepStale(_ context.Context, _ time.Duration) (int64, error) {
m.sweepCalled = true
return 0, nil
}
func (m *mockClusterStore) ListNodes(_ context.Context) ([]store.ClusterNode, error) {
return m.nodes, nil
}
func (m *mockClusterStore) Deregister(_ context.Context, _ string) error {
return nil
}
// mockHub implements ConnCounter.
type mockHub struct{ count int }
func (h *mockHub) ConnCount() int { return h.count }
func TestCollectStats(t *testing.T) {
hub := &mockHub{count: 5}
reg := NewRegistry("test-node", "http://localhost:8080", nil, hub, RegistryConfig{
HeartbeatInterval: 10 * time.Second,
StaleThreshold: 30 * time.Second,
})
data := reg.collectStats()
var stats map[string]any
if err := json.Unmarshal(data, &stats); err != nil {
t.Fatalf("collectStats returned invalid JSON: %v", err)
}
// Verify expected keys
expectedKeys := []string{"goroutines", "heap_alloc", "heap_sys", "gc_cycles", "gc_pause_ns", "uptime_sec", "ws_clients"}
for _, key := range expectedKeys {
if _, ok := stats[key]; !ok {
t.Errorf("missing expected stats key: %s", key)
}
}
// ws_clients should reflect hub count
if ws, ok := stats["ws_clients"].(float64); !ok || int(ws) != 5 {
t.Errorf("ws_clients = %v, want 5", stats["ws_clients"])
}
}
func TestRegistryStartStop(t *testing.T) {
ms := &mockClusterStore{heartbeatRows: 1}
hub := &mockHub{count: 0}
reg := NewRegistry("test-node", "", ms, hub, RegistryConfig{
HeartbeatInterval: 50 * time.Millisecond,
StaleThreshold: 150 * time.Millisecond,
})
if err := reg.Start(); err != nil {
t.Fatalf("Start() error: %v", err)
}
if !ms.registerCalled {
t.Error("Register was not called on Start")
}
// Let at least one tick fire
time.Sleep(100 * time.Millisecond)
reg.Stop()
if !ms.sweepCalled {
t.Error("SweepStale was never called during tick")
}
if ms.lastStats == nil {
t.Error("Heartbeat was never called with stats")
}
}
func TestNodeID(t *testing.T) {
reg := NewRegistry("my-node-123", "", nil, &mockHub{}, RegistryConfig{})
if got := reg.NodeID(); got != "my-node-123" {
t.Errorf("NodeID() = %q, want %q", got, "my-node-123")
}
}