diff options
Diffstat (limited to 'internal/llm/circuitbreaker_test.go')
| -rw-r--r-- | internal/llm/circuitbreaker_test.go | 103 |
1 files changed, 103 insertions, 0 deletions
diff --git a/internal/llm/circuitbreaker_test.go b/internal/llm/circuitbreaker_test.go new file mode 100644 index 0000000..17510b3 --- /dev/null +++ b/internal/llm/circuitbreaker_test.go @@ -0,0 +1,103 @@ +package llm + +import ( + "testing" + "time" +) + +func TestCircuitBreaker_NilIsNoOp(t *testing.T) { + var cb *circuitBreaker + if !cb.Allow() { + t.Fatal("nil breaker must allow") + } + // These must not panic on a nil receiver. + cb.recordFailure() + cb.recordSuccess() +} + +func TestCircuitBreaker_TripsAfterThreshold(t *testing.T) { + cb := newCircuitBreaker(3, time.Minute) + for i := 0; i < 2; i++ { + cb.recordFailure() + if !cb.Allow() { + t.Fatalf("breaker should stay closed before threshold (i=%d)", i) + } + } + cb.recordFailure() // third failure trips it + if cb.Allow() { + t.Fatal("breaker should be open after threshold failures") + } +} + +func TestCircuitBreaker_SuccessResets(t *testing.T) { + cb := newCircuitBreaker(2, time.Minute) + cb.recordFailure() + cb.recordSuccess() + cb.recordFailure() // only one failure since reset; should not trip + if !cb.Allow() { + t.Fatal("breaker should remain closed after success reset") + } +} + +func TestCircuitBreaker_HalfOpenAllowsSingleProbe(t *testing.T) { + now := time.Unix(0, 0) + cb := newCircuitBreaker(1, 10*time.Second) + cb.now = func() time.Time { return now } + + cb.recordFailure() // trips (threshold 1) + if cb.Allow() { + t.Fatal("should be open immediately after trip") + } + + // Advance past cooldown: first Allow transitions to half-open and permits one probe. + now = now.Add(11 * time.Second) + if !cb.Allow() { + t.Fatal("expected half-open probe to be allowed") + } + // A second concurrent probe is rejected. + if cb.Allow() { + t.Fatal("second probe must be rejected while half-open") + } +} + +func TestCircuitBreaker_HalfOpenProbeSuccessCloses(t *testing.T) { + now := time.Unix(0, 0) + cb := newCircuitBreaker(1, time.Second) + cb.now = func() time.Time { return now } + + cb.recordFailure() + now = now.Add(2 * time.Second) + if !cb.Allow() { + t.Fatal("expected probe allowed") + } + cb.recordSuccess() + if !cb.Allow() { + t.Fatal("breaker should be closed after successful probe") + } +} + +func TestCircuitBreaker_HalfOpenProbeFailureReopens(t *testing.T) { + now := time.Unix(0, 0) + cb := newCircuitBreaker(1, time.Second) + cb.now = func() time.Time { return now } + + cb.recordFailure() + now = now.Add(2 * time.Second) + if !cb.Allow() { + t.Fatal("expected probe allowed") + } + cb.recordFailure() // probe failed: reopen + if cb.Allow() { + t.Fatal("breaker should reopen after failed probe") + } +} + +func TestCircuitBreaker_ZeroThresholdNeverTrips(t *testing.T) { + cb := newCircuitBreaker(0, time.Minute) + for i := 0; i < 100; i++ { + cb.recordFailure() + } + if !cb.Allow() { + t.Fatal("threshold<=0 should never trip the breaker") + } +} |
