14bb60e918
注入 now 纯确定性,force_open 支持 401/403 直接熔断。 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
70 lines
2.7 KiB
Python
70 lines
2.7 KiB
Python
"""CircuitBreaker 单元测试。
|
||
|
||
验证内存级熔断器的核心行为:阈值触发、冷却半开、成功重置、强制熔断。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import pytest
|
||
|
||
from adapters.breaker import CircuitBreaker
|
||
|
||
|
||
@pytest.fixture()
|
||
def breaker() -> CircuitBreaker:
|
||
"""创建 threshold=3, cooldown=10s 的熔断器。"""
|
||
return CircuitBreaker(fail_threshold=3, cooldown_s=10.0)
|
||
|
||
|
||
class TestCircuitBreaker:
|
||
"""CircuitBreaker 行为验证。"""
|
||
|
||
def test_closed_by_default(self, breaker: CircuitBreaker) -> None:
|
||
"""初始状态为关闭(放行)。"""
|
||
assert breaker.is_open("llm", now=0.0) is False
|
||
|
||
def test_opens_after_threshold_failures(self, breaker: CircuitBreaker) -> None:
|
||
"""连续失败未达阈值不开路;达到阈值后开路。"""
|
||
# 2 次失败 < threshold=3,仍关闭
|
||
breaker.record_failure("llm", now=1.0)
|
||
breaker.record_failure("llm", now=2.0)
|
||
assert breaker.is_open("llm", now=2.5) is False
|
||
|
||
# 第 3 次失败 >= threshold=3,开路
|
||
breaker.record_failure("llm", now=3.0)
|
||
assert breaker.is_open("llm", now=3.5) is True
|
||
|
||
def test_half_open_after_cooldown(self, breaker: CircuitBreaker) -> None:
|
||
"""冷却期过后自动半开(放行试探)。"""
|
||
for t in range(3):
|
||
breaker.record_failure("llm", now=float(t))
|
||
# 开路期间
|
||
assert breaker.is_open("llm", now=5.0) is True
|
||
# cooldown=10s,第 3 次失败 now=2.0,open_until=12.0
|
||
# now=12.0 时冷却到期 → 半开
|
||
assert breaker.is_open("llm", now=12.0) is False
|
||
|
||
def test_success_resets(self, breaker: CircuitBreaker) -> None:
|
||
"""record_success 重置计数并关闭熔断器。"""
|
||
for t in range(3):
|
||
breaker.record_failure("llm", now=float(t))
|
||
assert breaker.is_open("llm", now=3.0) is True
|
||
|
||
breaker.record_success("llm")
|
||
assert breaker.is_open("llm", now=3.0) is False
|
||
|
||
def test_force_open_immediate(self, breaker: CircuitBreaker) -> None:
|
||
"""force_open 无需积累失败即可直接熔断。"""
|
||
breaker.force_open("llm", now=5.0)
|
||
assert breaker.is_open("llm", now=5.5) is True
|
||
|
||
def test_force_open_probe_failure_reopens(self, breaker: CircuitBreaker) -> None:
|
||
"""半开探针失败后重新熔断(因 fails 已置为 threshold)。"""
|
||
breaker.force_open("llm", now=0.0)
|
||
# cooldown=10s → open_until=10.0,now=10.0 半开
|
||
assert breaker.is_open("llm", now=10.0) is False
|
||
|
||
# 半开探针失败:一次 record_failure 即达阈值(fails 已=3),重新熔断
|
||
breaker.record_failure("llm", now=10.0)
|
||
assert breaker.is_open("llm", now=10.5) is True
|