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/handlers/admin_metrics_test.go
Jeffrey Smith 349ff5c80a
All checks were successful
CI/CD / detect-changes (pull_request) Successful in 4s
CI/CD / test-frontend (pull_request) Successful in 5s
CI/CD / test-go-pg (pull_request) Successful in 2m46s
CI/CD / test-sqlite (pull_request) Successful in 2m53s
CI/CD / build-and-deploy (pull_request) Successful in 1m42s
Fix health tab: node identity indicator + cluster card overflow
Add node_id to metrics snapshot so the health tab shows which instance
the user is viewing from. Hoist nodeID computation so it works on both
SQLite and Postgres deployments. Fix cluster node card overflow with
text-overflow ellipsis and proper flex constraints.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 13:40:18 +00:00

188 lines
4.8 KiB
Go

package handlers
import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/gin-gonic/gin"
"switchboard-core/metrics"
"switchboard-core/store"
)
// mockHub implements metrics.ConnCounter for tests.
type mockHub struct{ count int }
func (m *mockHub) ConnCount() int { return m.count }
// mockBus implements metrics.BusCounter for tests.
type mockBus struct{ pub, del int64 }
func (m *mockBus) PublishCount() int64 { return m.pub }
func (m *mockBus) DeliverCount() int64 { return m.del }
func TestMetrics_SQLiteShape(t *testing.T) {
gin.SetMode(gin.TestMode)
collector := metrics.NewCollector(
"test-node",
nil, // no DB
&mockHub{count: 5},
&mockBus{pub: 10, del: 20},
store.Stores{}, // no cluster store
func() (uint64, uint64, float64) { return 42, 3, 12.5 },
func() int64 { return 7 },
time.Now().Add(-10*time.Minute),
)
h := NewMetricsHandler(collector)
r := gin.New()
r.GET("/api/v1/admin/metrics", h.GetMetrics)
req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/metrics", nil)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
if w.Code != 200 {
t.Fatalf("status = %d, want 200", w.Code)
}
var snap metrics.Snapshot
if err := json.Unmarshal(w.Body.Bytes(), &snap); err != nil {
t.Fatalf("unmarshal: %v", err)
}
// Node ID
if snap.NodeID != "test-node" {
t.Errorf("node_id = %q, want %q", snap.NodeID, "test-node")
}
// Runtime
if snap.Runtime.WSClients != 5 {
t.Errorf("ws_clients = %d, want 5", snap.Runtime.WSClients)
}
if snap.Runtime.UptimeSec < 600 {
t.Errorf("uptime_sec = %f, want >= 600", snap.Runtime.UptimeSec)
}
// Cluster should be nil (SQLite)
if snap.Cluster != nil {
t.Errorf("cluster should be nil for SQLite, got %+v", snap.Cluster)
}
// Extensions
if snap.Extensions.StarlarkExecTotal != 42 {
t.Errorf("starlark_exec_total = %d, want 42", snap.Extensions.StarlarkExecTotal)
}
if snap.Extensions.StarlarkErrorsTotal != 3 {
t.Errorf("starlark_errors_total = %d, want 3", snap.Extensions.StarlarkErrorsTotal)
}
if snap.Extensions.StarlarkAvgDuration != 12.5 {
t.Errorf("starlark_avg_duration_ms = %f, want 12.5", snap.Extensions.StarlarkAvgDuration)
}
if snap.Extensions.TriggerFiresTotal != 7 {
t.Errorf("trigger_fires_total = %d, want 7", snap.Extensions.TriggerFiresTotal)
}
if snap.Extensions.EventBusPublished != 10 {
t.Errorf("event_bus_published = %d, want 10", snap.Extensions.EventBusPublished)
}
if snap.Extensions.EventBusDelivered != 20 {
t.Errorf("event_bus_delivered = %d, want 20", snap.Extensions.EventBusDelivered)
}
}
func TestMetrics_WithCluster(t *testing.T) {
gin.SetMode(gin.TestMode)
mock := &mockClusterStore{
nodes: []store.ClusterNode{
{
NodeID: "node-1",
Endpoint: "http://node-1:8080",
Heartbeat: time.Now(),
Stats: json.RawMessage(`{"uptime_sec":120,"ws_clients":3}`),
},
},
}
collector := metrics.NewCollector(
"test-node",
nil,
&mockHub{count: 3},
&mockBus{},
store.Stores{Cluster: mock},
func() (uint64, uint64, float64) { return 0, 0, 0 },
func() int64 { return 0 },
time.Now(),
)
h := NewMetricsHandler(collector)
r := gin.New()
r.GET("/api/v1/admin/metrics", h.GetMetrics)
req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/metrics", nil)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
if w.Code != 200 {
t.Fatalf("status = %d, want 200", w.Code)
}
var snap metrics.Snapshot
if err := json.Unmarshal(w.Body.Bytes(), &snap); err != nil {
t.Fatalf("unmarshal: %v", err)
}
if snap.Cluster == nil {
t.Fatal("cluster should not be nil with cluster store")
}
if snap.Cluster.Size != 1 {
t.Errorf("cluster.size = %d, want 1", snap.Cluster.Size)
}
if snap.Cluster.Nodes[0].NodeID != "node-1" {
t.Errorf("node_id = %q, want %q", snap.Cluster.Nodes[0].NodeID, "node-1")
}
if snap.Cluster.Nodes[0].UptimeSec != 120 {
t.Errorf("uptime_sec = %f, want 120", snap.Cluster.Nodes[0].UptimeSec)
}
}
func TestMetrics_ExtensionCounters(t *testing.T) {
gin.SetMode(gin.TestMode)
collector := metrics.NewCollector(
"test-node",
nil,
&mockHub{},
&mockBus{pub: 100, del: 500},
store.Stores{},
func() (uint64, uint64, float64) { return 1000, 50, 8.3 },
func() int64 { return 25 },
time.Now(),
)
h := NewMetricsHandler(collector)
r := gin.New()
r.GET("/api/v1/admin/metrics", h.GetMetrics)
req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/metrics", nil)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
var snap metrics.Snapshot
_ = json.Unmarshal(w.Body.Bytes(), &snap)
if snap.Extensions.StarlarkExecTotal != 1000 {
t.Errorf("exec total = %d, want 1000", snap.Extensions.StarlarkExecTotal)
}
if snap.Extensions.EventBusPublished != 100 {
t.Errorf("bus published = %d, want 100", snap.Extensions.EventBusPublished)
}
if snap.Extensions.EventBusDelivered != 500 {
t.Errorf("bus delivered = %d, want 500", snap.Extensions.EventBusDelivered)
}
}