Files
BACKBEAT/pkg/sdk/examples/examples_test.go
2025-10-17 08:56:25 +11:00

520 lines
13 KiB
Go

package examples
import (
"context"
"crypto/ed25519"
"crypto/rand"
"fmt"
"testing"
"time"
"github.com/chorus-services/backbeat/pkg/sdk"
)
var testCounter int
// generateUniqueAgentID generates unique agent IDs for tests to avoid expvar conflicts
func generateUniqueAgentID(prefix string) string {
testCounter++
return fmt.Sprintf("%s-%d", prefix, testCounter)
}
// Test helper interface for both *testing.T and *testing.B
type testHelper interface {
Fatalf(format string, args ...interface{})
}
// Test helper to create a test client configuration
func createTestConfig(t testHelper, agentIDPrefix string) *sdk.Config {
_, signingKey, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
t.Fatalf("Failed to generate signing key: %v", err)
}
config := sdk.DefaultConfig()
config.ClusterID = "test-cluster"
config.AgentID = generateUniqueAgentID(agentIDPrefix)
config.NATSUrl = "nats://localhost:4222" // Assumes NATS is running for tests
config.SigningKey = signingKey
return config
}
// TestSimpleAgentPattern tests the simple agent usage pattern
func TestSimpleAgentPattern(t *testing.T) {
config := createTestConfig(t, "test-simple-agent")
client := sdk.NewClient(config)
// Context for timeout control (used in full integration tests)
_ = context.Background()
// Track callback invocations
var beatCount, downbeatCount int
// Register callbacks
err := client.OnBeat(func(beat sdk.BeatFrame) {
beatCount++
t.Logf("Beat received: %d (downbeat: %v)", beat.BeatIndex, beat.Downbeat)
})
if err != nil {
t.Fatalf("Failed to register beat callback: %v", err)
}
err = client.OnDownbeat(func(beat sdk.BeatFrame) {
downbeatCount++
t.Logf("Downbeat received: %d", beat.BeatIndex)
})
if err != nil {
t.Fatalf("Failed to register downbeat callback: %v", err)
}
// Use variables to prevent unused warnings
_ = beatCount
_ = downbeatCount
// This test only checks if the client can be configured and started
// without errors. Full integration tests would require running services.
// Test health status before starting
health := client.Health()
if health.Connected {
t.Error("Client should not be connected before Start()")
}
// Test that we can create status claims
err = client.EmitStatusClaim(sdk.StatusClaim{
State: "planning",
BeatsLeft: 10,
Progress: 0.0,
Notes: "Test status claim",
})
// This should fail because client isn't started
if err == nil {
t.Error("EmitStatusClaim should fail when client not started")
}
}
// TestBeatBudgetPattern tests the beat budget usage pattern
func TestBeatBudgetPattern(t *testing.T) {
config := createTestConfig(t, "test-budget-agent")
client := sdk.NewClient(config)
// Test beat budget without starting client (should work for timeout logic)
err := client.WithBeatBudget(2, func() error {
time.Sleep(100 * time.Millisecond) // Quick task
return nil
})
// This may fail due to no beat timing available, but shouldn't panic
if err != nil {
t.Logf("Beat budget failed as expected (no timing): %v", err)
}
// Test invalid budget
err = client.WithBeatBudget(0, func() error {
return nil
})
if err == nil {
t.Error("WithBeatBudget should fail with zero budget")
}
err = client.WithBeatBudget(-1, func() error {
return nil
})
if err == nil {
t.Error("WithBeatBudget should fail with negative budget")
}
}
// TestClientConfiguration tests various client configuration scenarios
func TestClientConfiguration(t *testing.T) {
// Test with minimal config
config := &sdk.Config{
ClusterID: "test",
AgentID: "test-agent",
NATSUrl: "nats://localhost:4222",
}
client := sdk.NewClient(config)
if client == nil {
t.Fatal("NewClient should not return nil")
}
// Test health before start
health := client.Health()
if health.Connected {
t.Error("New client should not be connected")
}
// Test utilities with no beat data
beat := client.GetCurrentBeat()
if beat != 0 {
t.Errorf("GetCurrentBeat should return 0 initially, got %d", beat)
}
window := client.GetCurrentWindow()
if window != "" {
t.Errorf("GetCurrentWindow should return empty string initially, got %s", window)
}
// Test IsInWindow
if client.IsInWindow("any-window") {
t.Error("IsInWindow should return false with no current window")
}
}
// TestStatusClaimValidation tests status claim validation
func TestStatusClaimValidation(t *testing.T) {
config := createTestConfig(t, "test-validation")
client := sdk.NewClient(config)
// Test various invalid status claims
testCases := []struct {
name string
claim sdk.StatusClaim
wantErr bool
}{
{
name: "valid claim",
claim: sdk.StatusClaim{
State: "executing",
BeatsLeft: 5,
Progress: 0.5,
Notes: "Test note",
},
wantErr: false, // Will still error due to no connection, but validation should pass
},
{
name: "invalid state",
claim: sdk.StatusClaim{
State: "invalid",
BeatsLeft: 5,
Progress: 0.5,
Notes: "Test note",
},
wantErr: true,
},
{
name: "negative progress",
claim: sdk.StatusClaim{
State: "executing",
BeatsLeft: 5,
Progress: -0.1,
Notes: "Test note",
},
wantErr: true,
},
{
name: "progress too high",
claim: sdk.StatusClaim{
State: "executing",
BeatsLeft: 5,
Progress: 1.1,
Notes: "Test note",
},
wantErr: true,
},
{
name: "negative beats left",
claim: sdk.StatusClaim{
State: "executing",
BeatsLeft: -1,
Progress: 0.5,
Notes: "Test note",
},
wantErr: true,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
err := client.EmitStatusClaim(tc.claim)
if tc.wantErr && err == nil {
t.Error("Expected error but got none")
}
// Note: All will error due to no connection, but we're testing validation
if err != nil {
t.Logf("Error (expected): %v", err)
}
})
}
}
// BenchmarkStatusClaimEmission benchmarks status claim creation and validation
func BenchmarkStatusClaimEmission(b *testing.B) {
config := createTestConfig(b, "benchmark-agent")
client := sdk.NewClient(config)
claim := sdk.StatusClaim{
State: "executing",
BeatsLeft: 10,
Progress: 0.75,
Notes: "Benchmark test claim",
}
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
// This will fail due to no connection, but measures validation overhead
client.EmitStatusClaim(claim)
}
})
}
// BenchmarkBeatCallbacks benchmarks callback execution
func BenchmarkBeatCallbacks(b *testing.B) {
config := createTestConfig(b, "callback-benchmark")
client := sdk.NewClient(config)
// Register a simple callback
client.OnBeat(func(beat sdk.BeatFrame) {
// Minimal processing
_ = beat.BeatIndex
})
// Create a mock beat frame
beatFrame := sdk.BeatFrame{
Type: "backbeat.beatframe.v1",
ClusterID: "test",
BeatIndex: 1,
Downbeat: false,
Phase: "test",
HLC: "123-0",
WindowID: "test-window",
TempoBPM: 2, // 30-second beats - much more reasonable for testing
}
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
// Simulate callback execution
// Note: This doesn't actually invoke callbacks since client isn't started
_ = beatFrame
}
})
}
// TestDetermineState tests the state determination logic from simple_agent.go
func TestDetermineState(t *testing.T) {
tests := []struct {
total int64
completed int64
expected string
}{
{0, 0, "waiting"},
{5, 5, "done"},
{5, 3, "executing"},
{5, 0, "planning"},
{10, 8, "executing"},
{1, 1, "done"},
}
for _, test := range tests {
result := determineState(test.total, test.completed)
if result != test.expected {
t.Errorf("determineState(%d, %d) = %s; expected %s",
test.total, test.completed, result, test.expected)
}
}
}
// TestCalculateBeatsLeft tests the beats remaining calculation from simple_agent.go
func TestCalculateBeatsLeft(t *testing.T) {
tests := []struct {
total int64
completed int64
expected int
}{
{0, 0, 0},
{5, 5, 0},
{5, 3, 10}, // (5-3) * 5 = 10
{10, 0, 50}, // 10 * 5 = 50
{1, 0, 5}, // 1 * 5 = 5
}
for _, test := range tests {
result := calculateBeatsLeft(test.total, test.completed)
if result != test.expected {
t.Errorf("calculateBeatsLeft(%d, %d) = %d; expected %d",
test.total, test.completed, result, test.expected)
}
}
}
// TestTaskStructure tests Task struct from task_processor.go
func TestTaskStructure(t *testing.T) {
task := &Task{
ID: "test-task-123",
Description: "Test processing task",
BeatBudget: 8,
WorkTime: 3 * time.Second,
Created: time.Now(),
}
if task.ID == "" {
t.Error("Expected task ID to be set")
}
if task.Description == "" {
t.Error("Expected task description to be set")
}
if task.BeatBudget <= 0 {
t.Error("Expected positive beat budget")
}
if task.WorkTime <= 0 {
t.Error("Expected positive work time")
}
if task.Created.IsZero() {
t.Error("Expected creation time to be set")
}
}
// TestServiceHealthStructure tests ServiceHealth struct from service_monitor.go
func TestServiceHealthStructure(t *testing.T) {
health := &ServiceHealth{
ServiceName: "test-service",
Status: "healthy",
LastCheck: time.Now(),
ResponseTime: 150 * time.Millisecond,
ErrorCount: 0,
Uptime: 5 * time.Minute,
}
if health.ServiceName == "" {
t.Error("Expected service name to be set")
}
validStatuses := []string{"healthy", "degraded", "unhealthy", "unknown"}
validStatus := false
for _, status := range validStatuses {
if health.Status == status {
validStatus = true
break
}
}
if !validStatus {
t.Errorf("Expected valid status, got: %s", health.Status)
}
if health.ResponseTime < 0 {
t.Error("Expected non-negative response time")
}
if health.ErrorCount < 0 {
t.Error("Expected non-negative error count")
}
}
// TestSystemMetricsStructure tests SystemMetrics struct from service_monitor.go
func TestSystemMetricsStructure(t *testing.T) {
metrics := &SystemMetrics{
CPUPercent: 25.5,
MemoryPercent: 67.8,
GoroutineCount: 42,
HeapSizeMB: 128.5,
}
if metrics.CPUPercent < 0 || metrics.CPUPercent > 100 {
t.Error("Expected CPU percentage between 0 and 100")
}
if metrics.MemoryPercent < 0 || metrics.MemoryPercent > 100 {
t.Error("Expected memory percentage between 0 and 100")
}
if metrics.GoroutineCount < 0 {
t.Error("Expected non-negative goroutine count")
}
if metrics.HeapSizeMB < 0 {
t.Error("Expected non-negative heap size")
}
}
// TestHealthScoreCalculation tests calculateHealthScore from service_monitor.go
func TestHealthScoreCalculation(t *testing.T) {
tests := []struct {
summary map[string]int
expected float64
}{
{map[string]int{"healthy": 0, "degraded": 0, "unhealthy": 0, "unknown": 0}, 0.0},
{map[string]int{"healthy": 4, "degraded": 0, "unhealthy": 0, "unknown": 0}, 1.0},
{map[string]int{"healthy": 0, "degraded": 0, "unhealthy": 4, "unknown": 0}, 0.0},
{map[string]int{"healthy": 2, "degraded": 2, "unhealthy": 0, "unknown": 0}, 0.75},
{map[string]int{"healthy": 1, "degraded": 1, "unhealthy": 1, "unknown": 1}, 0.4375},
}
for i, test := range tests {
result := calculateHealthScore(test.summary)
if result != test.expected {
t.Errorf("Test %d: calculateHealthScore(%v) = %.4f; expected %.4f",
i, test.summary, result, test.expected)
}
}
}
// TestDetermineOverallState tests determineOverallState from service_monitor.go
func TestDetermineOverallState(t *testing.T) {
tests := []struct {
summary map[string]int
expected string
}{
{map[string]int{"healthy": 3, "degraded": 0, "unhealthy": 0, "unknown": 0}, "done"},
{map[string]int{"healthy": 2, "degraded": 1, "unhealthy": 0, "unknown": 0}, "executing"},
{map[string]int{"healthy": 1, "degraded": 1, "unhealthy": 1, "unknown": 0}, "failed"},
{map[string]int{"healthy": 0, "degraded": 0, "unhealthy": 0, "unknown": 3}, "waiting"},
{map[string]int{"healthy": 0, "degraded": 0, "unhealthy": 1, "unknown": 0}, "failed"},
}
for i, test := range tests {
result := determineOverallState(test.summary)
if result != test.expected {
t.Errorf("Test %d: determineOverallState(%v) = %s; expected %s",
i, test.summary, result, test.expected)
}
}
}
// TestFormatHealthSummary tests formatHealthSummary from service_monitor.go
func TestFormatHealthSummary(t *testing.T) {
summary := map[string]int{
"healthy": 3,
"degraded": 2,
"unhealthy": 1,
"unknown": 0,
}
result := formatHealthSummary(summary)
expected := "H:3 D:2 U:1 ?:0"
if result != expected {
t.Errorf("formatHealthSummary() = %s; expected %s", result, expected)
}
}
// TestCollectSystemMetrics tests collectSystemMetrics from service_monitor.go
func TestCollectSystemMetrics(t *testing.T) {
metrics := collectSystemMetrics()
if metrics.GoroutineCount <= 0 {
t.Error("Expected positive goroutine count")
}
if metrics.HeapSizeMB < 0 {
t.Error("Expected non-negative heap size")
}
// Note: CPU and Memory percentages are simplified in the example implementation
if metrics.CPUPercent < 0 {
t.Error("Expected non-negative CPU percentage")
}
if metrics.MemoryPercent < 0 {
t.Error("Expected non-negative memory percentage")
}
}