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") } }