mirror of
https://github.com/msoedov/agentic_security.git
synced 2026-06-24 14:19:55 +02:00
210 lines
6.4 KiB
Python
210 lines
6.4 KiB
Python
"""Tests for CircuitBreaker."""
|
|
|
|
import time
|
|
from agentic_security.executor.circuit_breaker import CircuitBreaker
|
|
|
|
|
|
class TestCircuitBreaker:
|
|
"""Test CircuitBreaker functionality."""
|
|
|
|
def test_initialization(self):
|
|
"""Test circuit breaker initialization."""
|
|
breaker = CircuitBreaker(failure_threshold=0.5, recovery_timeout=30)
|
|
|
|
assert breaker.failure_threshold == 0.5
|
|
assert breaker.recovery_timeout == 30
|
|
assert breaker.state == "closed"
|
|
assert breaker.failures == 0
|
|
assert breaker.successes == 0
|
|
|
|
def test_record_success(self):
|
|
"""Test recording successful requests."""
|
|
breaker = CircuitBreaker()
|
|
|
|
breaker.record_success()
|
|
assert breaker.successes == 1
|
|
assert breaker.failures == 0
|
|
assert breaker.state == "closed"
|
|
|
|
def test_record_failure(self):
|
|
"""Test recording failed requests."""
|
|
breaker = CircuitBreaker()
|
|
|
|
breaker.record_failure()
|
|
assert breaker.failures == 1
|
|
assert breaker.successes == 0
|
|
assert breaker.last_failure_time is not None
|
|
|
|
def test_circuit_opens_on_failure_threshold(self):
|
|
"""Test that circuit opens when failure threshold is exceeded."""
|
|
breaker = CircuitBreaker(failure_threshold=0.5, recovery_timeout=30)
|
|
|
|
# Record 10 requests: 6 failures, 4 successes (60% failure rate)
|
|
for _ in range(4):
|
|
breaker.record_success()
|
|
for _ in range(6):
|
|
breaker.record_failure()
|
|
|
|
# Circuit should be open (60% > 50% threshold)
|
|
assert breaker.state == "open"
|
|
assert breaker.is_open() is True
|
|
|
|
def test_circuit_stays_closed_below_threshold(self):
|
|
"""Test that circuit stays closed when below threshold."""
|
|
breaker = CircuitBreaker(failure_threshold=0.5, recovery_timeout=30)
|
|
|
|
# Record 10 requests: 4 failures, 6 successes (40% failure rate)
|
|
for _ in range(6):
|
|
breaker.record_success()
|
|
for _ in range(4):
|
|
breaker.record_failure()
|
|
|
|
# Circuit should stay closed (40% < 50% threshold)
|
|
assert breaker.state == "closed"
|
|
assert breaker.is_open() is False
|
|
|
|
def test_minimum_sample_size_required(self):
|
|
"""Test that minimum sample size is required before opening."""
|
|
breaker = CircuitBreaker(failure_threshold=0.5)
|
|
|
|
# Only 5 failures (below minimum of 10 total requests)
|
|
for _ in range(5):
|
|
breaker.record_failure()
|
|
|
|
# Circuit should stay closed (not enough samples)
|
|
assert breaker.state == "closed"
|
|
assert breaker.is_open() is False
|
|
|
|
def test_circuit_recovery_after_timeout(self):
|
|
"""Test that circuit enters half-open state after recovery timeout."""
|
|
breaker = CircuitBreaker(failure_threshold=0.5, recovery_timeout=1)
|
|
|
|
# Open the circuit
|
|
for _ in range(4):
|
|
breaker.record_success()
|
|
for _ in range(6):
|
|
breaker.record_failure()
|
|
|
|
assert breaker.state == "open"
|
|
|
|
# Wait for recovery timeout
|
|
time.sleep(1.1)
|
|
|
|
# Check if circuit moves to half-open
|
|
is_open = breaker.is_open()
|
|
assert is_open is False
|
|
assert breaker.state == "half_open"
|
|
|
|
def test_half_open_to_closed_on_successes(self):
|
|
"""Test that circuit closes from half-open after enough successes."""
|
|
breaker = CircuitBreaker(failure_threshold=0.5, recovery_timeout=1)
|
|
|
|
# Open the circuit
|
|
for _ in range(4):
|
|
breaker.record_success()
|
|
for _ in range(6):
|
|
breaker.record_failure()
|
|
|
|
# Wait for recovery
|
|
time.sleep(1.1)
|
|
breaker.is_open() # Triggers transition to half-open
|
|
|
|
assert breaker.state == "half_open"
|
|
|
|
# Record 3 successes
|
|
breaker.record_success()
|
|
breaker.record_success()
|
|
breaker.record_success()
|
|
|
|
# Should transition to closed
|
|
assert breaker.state == "closed"
|
|
|
|
def test_get_state(self):
|
|
"""Test get_state method."""
|
|
breaker = CircuitBreaker()
|
|
|
|
assert breaker.get_state() == "closed"
|
|
|
|
# Open the circuit
|
|
for _ in range(10):
|
|
breaker.record_failure()
|
|
|
|
assert breaker.get_state() == "open"
|
|
|
|
def test_get_failure_rate(self):
|
|
"""Test get_failure_rate method."""
|
|
breaker = CircuitBreaker()
|
|
|
|
# No requests
|
|
assert breaker.get_failure_rate() == 0.0
|
|
|
|
# 3 failures, 7 successes (30% failure rate)
|
|
for _ in range(7):
|
|
breaker.record_success()
|
|
for _ in range(3):
|
|
breaker.record_failure()
|
|
|
|
assert breaker.get_failure_rate() == 0.3
|
|
|
|
def test_reset(self):
|
|
"""Test reset method."""
|
|
breaker = CircuitBreaker()
|
|
|
|
# Record some activity
|
|
breaker.record_success()
|
|
breaker.record_failure()
|
|
for _ in range(10):
|
|
breaker.record_failure()
|
|
|
|
# Reset
|
|
breaker.reset()
|
|
|
|
# Should be back to initial state
|
|
assert breaker.state == "closed"
|
|
assert breaker.failures == 0
|
|
assert breaker.successes == 0
|
|
assert breaker.last_failure_time is None
|
|
|
|
def test_exact_failure_threshold(self):
|
|
"""Test behavior at exact failure threshold."""
|
|
breaker = CircuitBreaker(failure_threshold=0.5)
|
|
|
|
# Exactly 50% failure rate (5 failures, 5 successes)
|
|
for _ in range(5):
|
|
breaker.record_success()
|
|
for _ in range(5):
|
|
breaker.record_failure()
|
|
|
|
# Should be open (>= threshold)
|
|
assert breaker.state == "open"
|
|
|
|
def test_high_failure_threshold(self):
|
|
"""Test with high failure threshold."""
|
|
breaker = CircuitBreaker(failure_threshold=0.9)
|
|
|
|
# 80% failure rate (8 failures, 2 successes)
|
|
for _ in range(2):
|
|
breaker.record_success()
|
|
for _ in range(8):
|
|
breaker.record_failure()
|
|
|
|
# Should stay closed (80% < 90%)
|
|
assert breaker.state == "closed"
|
|
|
|
def test_zero_recovery_timeout(self):
|
|
"""Test with zero recovery timeout."""
|
|
breaker = CircuitBreaker(failure_threshold=0.5, recovery_timeout=0)
|
|
|
|
# Open the circuit
|
|
for _ in range(10):
|
|
breaker.record_failure()
|
|
|
|
assert breaker.state == "open"
|
|
|
|
# Should immediately allow recovery attempt
|
|
time.sleep(0.01)
|
|
is_open = breaker.is_open()
|
|
|
|
assert is_open is False
|
|
assert breaker.state == "half_open"
|