ForcePilot/backend/test/unit/channels/test_circuit_breaker.py
Kris 3264900bc9 test: 新增多渠道单元测试用例并配置测试环境变量
新增了Twitch、Telegram、Discord、Slack、Mattermost、WeChat、Zalo等多渠道的单元测试用例,覆盖了令牌处理、速率限制、消息去重、会话解析、格式转换、安全策略等模块
同时在测试配置中添加了测试用的OpenAI API密钥环境变量
2026-05-12 00:56:47 +08:00

173 lines
5.0 KiB
Python

from __future__ import annotations
import asyncio
import pytest
from yuxi.channels.infra.circuit_breaker import CircuitBreaker, CircuitBreakerOpenError, CircuitState
class TestCircuitBreakerInit:
def test_default_values(self):
cb = CircuitBreaker()
assert cb.failure_threshold == 5
assert cb.recovery_timeout == 60.0
assert cb.half_open_max_calls == 3
assert cb.state == CircuitState.CLOSED
assert cb._failure_count == 0
def test_custom_values(self):
cb = CircuitBreaker(failure_threshold=3, recovery_timeout=10.0, half_open_max_calls=2)
assert cb.failure_threshold == 3
assert cb.recovery_timeout == 10.0
assert cb.half_open_max_calls == 2
class TestCircuitStateTransitions:
@pytest.mark.asyncio
async def test_closed_to_open_on_threshold(self):
cb = CircuitBreaker(failure_threshold=3)
for _ in range(3):
await cb.record_failure()
assert cb.state == CircuitState.OPEN
@pytest.mark.asyncio
async def test_not_open_before_threshold(self):
cb = CircuitBreaker(failure_threshold=5)
for _ in range(3):
await cb.record_failure()
assert cb.state == CircuitState.CLOSED
@pytest.mark.asyncio
async def test_half_open_on_recovery_timeout(self):
cb = CircuitBreaker(failure_threshold=1, recovery_timeout=0.0)
await cb.record_failure()
assert cb.state == CircuitState.OPEN
async def dummy():
return "ok"
result = await cb.call(dummy)
assert result == "ok"
assert cb.state == CircuitState.CLOSED
@pytest.mark.asyncio
async def test_half_open_to_closed_on_success(self):
cb = CircuitBreaker(failure_threshold=1, recovery_timeout=0.0)
await cb.record_failure()
async def ok_func():
return "ok"
await cb.call(ok_func)
assert cb.state == CircuitState.CLOSED
@pytest.mark.asyncio
async def test_half_open_to_open_on_failure(self):
cb = CircuitBreaker(failure_threshold=1, recovery_timeout=0.0)
await cb.record_failure()
async def failing():
raise RuntimeError("boom")
with pytest.raises(RuntimeError, match="boom"):
await cb.call(failing)
assert cb.state == CircuitState.OPEN
@pytest.mark.asyncio
async def test_record_success_resets_closed_count(self):
cb = CircuitBreaker(failure_threshold=5)
for _ in range(3):
await cb.record_failure()
await cb.record_success()
assert cb._failure_count == 0
assert cb.state == CircuitState.CLOSED
class TestCircuitBreakerCall:
@pytest.mark.asyncio
async def test_call_passes_result(self):
cb = CircuitBreaker()
async def func():
return "hello"
result = await cb.call(func)
assert result == "hello"
@pytest.mark.asyncio
async def test_call_records_failure_on_exception(self):
cb = CircuitBreaker()
async def func():
raise RuntimeError("fail")
with pytest.raises(RuntimeError, match="fail"):
await cb.call(func)
assert cb._failure_count == 1
@pytest.mark.asyncio
async def test_call_blocks_when_open(self):
cb = CircuitBreaker(failure_threshold=1, recovery_timeout=999.0)
await cb.record_failure()
assert cb.state == CircuitState.OPEN
async def ok_func():
return "ok"
with pytest.raises(CircuitBreakerOpenError):
await cb.call(ok_func)
@pytest.mark.asyncio
async def test_call_recovers_after_timeout(self):
cb = CircuitBreaker(failure_threshold=1, recovery_timeout=0.0)
await cb.record_failure()
async def ok_func():
return "ok"
await cb.call(ok_func)
assert cb.state == CircuitState.CLOSED
class TestHalfOpenMaxCalls:
@pytest.mark.asyncio
async def test_half_open_max_calls_reached(self):
cb = CircuitBreaker(half_open_max_calls=2)
cb.state = CircuitState.HALF_OPEN
cb._half_open_calls = 2
async def ok_func():
return "ok"
with pytest.raises(CircuitBreakerOpenError):
await cb.call(ok_func)
class TestConcurrentSafety:
@pytest.mark.asyncio
async def test_concurrent_failures(self):
cb = CircuitBreaker(failure_threshold=3)
async with cb._lock:
pass
results = await asyncio.gather(
cb.record_failure(),
cb.record_failure(),
cb.record_failure(),
)
assert cb._failure_count == 3
@pytest.mark.asyncio
async def test_concurrent_record_success(self):
cb = CircuitBreaker(failure_threshold=5)
for _ in range(3):
await cb.record_failure()
assert cb._failure_count == 3
await asyncio.gather(
cb.record_success(),
cb.record_success(),
cb.record_success(),
)
assert cb._failure_count == 0
assert cb.state == CircuitState.CLOSED