package actuator import ( "testing" "time" ) func TestNewCircuitBreakerDefaults(t *testing.T) { cb := newCircuitBreaker(0, 0) if cb.threshold != 3 { t.Errorf("threshold = %d, want 3", cb.threshold) } if cb.cooldownS != 300 { t.Errorf("cooldownS = %d, want 300", cb.cooldownS) } cb = newCircuitBreaker(5, 60) if cb.threshold != 5 { t.Errorf("threshold = %d, want 5", cb.threshold) } if cb.cooldownS != 60 { t.Errorf("cooldownS = %d, want 60", cb.cooldownS) } } func TestCircuitBreakerIsOpenFresh(t *testing.T) { cb := newCircuitBreaker(3, 60) if cb.isOpen("host:A") { t.Errorf("fresh circuit should be closed, got open") } } func TestCircuitBreakerOpensAtThreshold(t *testing.T) { cb := newCircuitBreaker(3, 60) // threshold-1 failures → still closed cb.recordFailure("host:A") cb.recordFailure("host:A") if cb.isOpen("host:A") { t.Fatalf("circuit should be closed after threshold-1 failures") } // one more → open cb.recordFailure("host:A") if !cb.isOpen("host:A") { t.Fatalf("circuit should be open after threshold failures") } } func TestCircuitBreakerClosesAfterCooldown(t *testing.T) { cb := newCircuitBreaker(1, 60) // Force open cb.recordFailure("host:A") if !cb.isOpen("host:A") { t.Fatalf("circuit should be open") } // Manipulate the cooldown timestamp to the past to simulate expiry. cb.mu.Lock() cb.cooldowns["host:A"] = time.Now().Add(-1 * time.Second) cb.mu.Unlock() if cb.isOpen("host:A") { t.Fatalf("circuit should be closed after cooldown expired") } // Failure count should have been reset by isOpen. cb.mu.Lock() got := cb.failures["host:A"] cb.mu.Unlock() if got != 0 { t.Errorf("failure count after cooldown reset = %d, want 0", got) } } func TestCircuitBreakerRecordSuccessResets(t *testing.T) { cb := newCircuitBreaker(3, 60) cb.recordFailure("host:A") cb.recordFailure("host:A") cb.recordSuccess("host:A") cb.mu.Lock() got := cb.failures["host:A"] cb.mu.Unlock() if got != 0 { t.Errorf("failure count after success = %d, want 0", got) } if cb.isOpen("host:A") { t.Errorf("circuit should be closed after success reset") } } func TestCircuitBreakerPerTargetIsolation(t *testing.T) { cb := newCircuitBreaker(2, 60) cb.recordFailure("host:A") cb.recordFailure("host:A") // host:A now at threshold → open if !cb.isOpen("host:A") { t.Fatalf("host:A should be open") } if cb.isOpen("host:B") { t.Errorf("host:B should be closed (isolated from host:A)") } // host:B has no failures recorded cb.mu.Lock() gotB := cb.failures["host:B"] cb.mu.Unlock() if gotB != 0 { t.Errorf("host:B failure count = %d, want 0", gotB) } }