""" Circuit Breaker 測試 ==================== ADR-038: OpenClaw 雙層保護機制 測試項目: - Circuit Breaker 狀態轉換 - Semaphore 並發控制 - Graceful Degradation """ import asyncio import pytest from src.core.circuit_breaker import ( CircuitBreakerConfig, CircuitState, OpenClawGuard, get_openclaw_guard, reset_openclaw_guard, ) class TestCircuitBreaker: """Circuit Breaker 核心功能測試""" def setup_method(self): """每個測試前重置全域 Guard""" reset_openclaw_guard() def test_initial_state_is_closed(self): """初始狀態應該是 CLOSED""" guard = OpenClawGuard() assert guard.state == CircuitState.CLOSED assert guard.failure_count == 0 assert not guard.is_circuit_open() def test_record_success_resets_failure_count(self): """成功應該重置失敗計數""" guard = OpenClawGuard() guard.failure_count = 3 guard.record_success() assert guard.failure_count == 0 def test_record_failure_increments_count(self): """失敗應該增加計數""" guard = OpenClawGuard() guard.record_failure() assert guard.failure_count == 1 guard.record_failure() assert guard.failure_count == 2 def test_circuit_opens_after_threshold(self): """連續失敗達到閾值後應該觸發斷路""" config = CircuitBreakerConfig(failure_threshold=3) guard = OpenClawGuard(config) guard.record_failure() guard.record_failure() assert guard.state == CircuitState.CLOSED guard.record_failure() assert guard.state == CircuitState.OPEN assert guard.is_circuit_open() def test_circuit_half_open_after_timeout(self): """冷卻期後應該切換到 HALF_OPEN""" config = CircuitBreakerConfig(failure_threshold=2, timeout_s=0.1) guard = OpenClawGuard(config) # 觸發斷路 guard.record_failure() guard.record_failure() assert guard.state == CircuitState.OPEN assert guard.is_circuit_open() # 等待冷卻 import time time.sleep(0.15) # 應該切換到 HALF_OPEN assert not guard.is_circuit_open() assert guard.state == CircuitState.HALF_OPEN def test_circuit_closes_after_success_in_half_open(self): """HALF_OPEN 狀態下成功應該恢復 CLOSED""" guard = OpenClawGuard() guard.state = CircuitState.HALF_OPEN guard.record_success() assert guard.state == CircuitState.CLOSED assert guard.failure_count == 0 def test_get_metrics(self): """應該正確返回指標""" guard = OpenClawGuard() guard.record_failure() guard.record_failure() metrics = guard.get_metrics() assert metrics["state"] == "closed" assert metrics["failure_count"] == 2 assert metrics["max_concurrent"] == 3 def test_singleton_pattern(self): """全域 Guard 應該是單例""" guard1 = get_openclaw_guard() guard2 = get_openclaw_guard() assert guard1 is guard2 def test_reset_clears_singleton(self): """reset 應該清除單例""" guard1 = get_openclaw_guard() reset_openclaw_guard() guard2 = get_openclaw_guard() assert guard1 is not guard2 class TestSemaphore: """Semaphore 並發控制測試""" def setup_method(self): reset_openclaw_guard() @pytest.mark.asyncio async def test_semaphore_limits_concurrency(self): """Semaphore 應該限制並發數""" config = CircuitBreakerConfig(max_concurrent=2) guard = OpenClawGuard(config) concurrent_count = 0 max_concurrent_seen = 0 results = [] async def worker(worker_id: int): nonlocal concurrent_count, max_concurrent_seen async with guard.semaphore: concurrent_count += 1 max_concurrent_seen = max(max_concurrent_seen, concurrent_count) await asyncio.sleep(0.05) results.append(worker_id) concurrent_count -= 1 # 啟動 5 個並發任務 tasks = [asyncio.create_task(worker(i)) for i in range(5)] await asyncio.gather(*tasks) # 確認最大並發數不超過限制 assert max_concurrent_seen <= 2 assert len(results) == 5 @pytest.mark.asyncio async def test_semaphore_created_lazily(self): """Semaphore 應該延遲建立""" guard = OpenClawGuard() assert guard._semaphore is None # 存取 semaphore 屬性會觸發建立 sem = guard.semaphore assert guard._semaphore is not None assert sem is guard._semaphore