"""熔断器测试.""" from unittest.mock import patch from src.ai.providers.circuit_breaker import CircuitBreaker, CircuitState class TestCircuitBreaker: """CircuitBreaker 状态机测试.""" def test_initial_state_closed(self) -> None: """初始状态为 CLOSED.""" cb = CircuitBreaker() assert cb.get_state("openai") == CircuitState.CLOSED def test_record_success_resets_failures(self) -> None: """成功重置失败计数.""" cb = CircuitBreaker() cb.record_failure("openai") cb.record_failure("openai") cb.record_success("openai") assert cb.get_state("openai") == CircuitState.CLOSED status = cb.status() assert status["openai"]["failures"] == 0 def test_threshold_opens_circuit(self) -> None: """达阈值触发 OPEN.""" cb = CircuitBreaker(failure_threshold=3) cb.record_failure("p1") cb.record_failure("p1") assert cb.get_state("p1") == CircuitState.CLOSED cb.record_failure("p1") assert cb.get_state("p1") == CircuitState.OPEN def test_open_blocks_calls(self) -> None: """OPEN 状态禁止调用.""" cb = CircuitBreaker(failure_threshold=1) cb.record_failure("p1") assert cb.is_closed("p1") is False def test_closed_allows_calls(self) -> None: """CLOSED 状态允许调用.""" cb = CircuitBreaker() assert cb.is_closed("p1") is True def test_half_open_after_cooldown(self) -> None: """冷却后进入 HALF_OPEN.""" cb = CircuitBreaker(failure_threshold=1, cooldown_seconds=60.0) with patch("src.ai.providers.circuit_breaker.time.monotonic", return_value=1000.0): cb.record_failure("p1") assert cb.get_state("p1") == CircuitState.OPEN # 时间推进超过冷却期 with patch("src.ai.providers.circuit_breaker.time.monotonic", return_value=1061.0): assert cb.get_state("p1") == CircuitState.HALF_OPEN def test_half_open_allows_one_call(self) -> None: """HALF_OPEN 允许 1 次试探.""" cb = CircuitBreaker(failure_threshold=1, cooldown_seconds=60.0, half_open_max_calls=1) with patch("src.ai.providers.circuit_breaker.time.monotonic", return_value=1000.0): cb.record_failure("p1") # 推进时间触发 HALF_OPEN with patch("src.ai.providers.circuit_breaker.time.monotonic", return_value=1061.0): cb.get_state("p1") # HALF_OPEN assert cb.is_closed("p1") is True # 第 1 次允许 assert cb.is_closed("p1") is False # 第 2 次拒绝 def test_half_open_success_closes(self) -> None: """HALF_OPEN 成功 → CLOSED.""" cb = CircuitBreaker(failure_threshold=1, cooldown_seconds=60.0) with patch("src.ai.providers.circuit_breaker.time.monotonic", return_value=1000.0): cb.record_failure("p1") with patch("src.ai.providers.circuit_breaker.time.monotonic", return_value=1061.0): cb.get_state("p1") # HALF_OPEN cb.record_success("p1") assert cb.get_state("p1") == CircuitState.CLOSED def test_half_open_failure_reopens(self) -> None: """HALF_OPEN 失败 → 重新 OPEN.""" cb = CircuitBreaker(failure_threshold=1, cooldown_seconds=60.0) with patch("src.ai.providers.circuit_breaker.time.monotonic", return_value=1000.0): cb.record_failure("p1") with patch("src.ai.providers.circuit_breaker.time.monotonic", return_value=1061.0): cb.get_state("p1") # HALF_OPEN cb.record_failure("p1") assert cb.get_state("p1") == CircuitState.OPEN def test_reset_clears_state(self) -> None: """reset 清除 Provider 状态.""" cb = CircuitBreaker(failure_threshold=1) cb.record_failure("p1") cb.reset("p1") assert cb.get_state("p1") == CircuitState.CLOSED assert "p1" not in cb.status() def test_status_snapshot(self) -> None: """status() 返回快照.""" cb = CircuitBreaker(failure_threshold=3) cb.record_failure("p1") cb.record_failure("p1") cb.record_failure("p2") status = cb.status() assert "p1" in status assert status["p1"]["failures"] == 2 assert "p2" in status def test_different_providers_independent(self) -> None: """不同 Provider 状态独立.""" cb = CircuitBreaker(failure_threshold=1) cb.record_failure("p1") assert cb.get_state("p1") == CircuitState.OPEN assert cb.get_state("p2") == CircuitState.CLOSED