520 lines
13 KiB
Go
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")
|
|
}
|
|
} |