# apps/api/tests/test_lifespan_failover_wiring.py | 2026-04-25 @ Asia/Taipei # 2026-04-25 P1.2 by Claude Engineer-A2 — failover 整合到 ai_router + lifespan """ Ollama Failover Lifespan Wiring 測試 ===================================== 驗收: 1. get_ollama_failover_manager singleton 呼叫了 set_recovery_callback 2. get_ollama_auto_recovery_service singleton 的 start() 被呼叫 3. shutdown 時 recovery service 的 stop() 被呼叫 4. failover_manager.set_recovery_callback 收到的是 recovery_svc.set_current_primary 測試分類:unit(mock get_ollama_failover_manager / get_ollama_auto_recovery_service) 不啟動完整 FastAPI app(避免 DB / Redis 依賴),直接測試 wiring 邏輯。 """ from __future__ import annotations from unittest.mock import AsyncMock, MagicMock, patch import pytest # ============================================================================= # Wiring 邏輯測試 # ============================================================================= @pytest.mark.asyncio async def test_lifespan_wires_recovery_callback_and_starts_service(): """ lifespan startup 邏輯: - failover_mgr.set_recovery_callback(recovery_svc.set_current_primary) 被呼叫 - recovery_svc.start() 被呼叫 """ mock_failover_mgr = MagicMock() mock_failover_mgr.set_recovery_callback = MagicMock() mock_recovery_svc = MagicMock() mock_recovery_svc.set_current_primary = AsyncMock() mock_recovery_svc.start = AsyncMock() mock_recovery_svc.stop = AsyncMock() with ( patch( "src.services.ollama_failover_manager.get_ollama_failover_manager", return_value=mock_failover_mgr, ), patch( "src.services.ollama_auto_recovery.get_ollama_auto_recovery_service", return_value=mock_recovery_svc, ), ): # 直接執行 lifespan 邏輯(不啟動完整 FastAPI) from src.services.ollama_failover_manager import get_ollama_failover_manager from src.services.ollama_auto_recovery import get_ollama_auto_recovery_service _failover_mgr = get_ollama_failover_manager() _recovery_svc = get_ollama_auto_recovery_service() _failover_mgr.set_recovery_callback(_recovery_svc.set_current_primary) await _recovery_svc.start() # 驗收 1: set_recovery_callback 被呼叫一次 mock_failover_mgr.set_recovery_callback.assert_called_once_with( mock_recovery_svc.set_current_primary ) # 驗收 2: start() 被呼叫一次 mock_recovery_svc.start.assert_awaited_once() @pytest.mark.asyncio async def test_lifespan_stops_recovery_service_on_shutdown(): """ lifespan shutdown 邏輯: - recovery_svc.stop() 被呼叫 """ mock_recovery_svc = MagicMock() mock_recovery_svc.stop = AsyncMock() with patch( "src.services.ollama_auto_recovery.get_ollama_auto_recovery_service", return_value=mock_recovery_svc, ): from src.services.ollama_auto_recovery import get_ollama_auto_recovery_service svc = get_ollama_auto_recovery_service() await svc.stop() mock_recovery_svc.stop.assert_awaited_once() @pytest.mark.asyncio async def test_set_recovery_callback_wires_correct_method(): """ set_recovery_callback 收到的 callable 是 recovery_svc.set_current_primary 確保 wiring 的物件同一性正確 """ from src.services.ollama_failover_manager import ( OllamaFailoverManager, reset_ollama_failover_manager, ) from src.services.ollama_auto_recovery import ( OllamaAutoRecoveryService, reset_ollama_auto_recovery_service, ) try: mock_monitor = MagicMock() failover_mgr = OllamaFailoverManager(health_monitor=mock_monitor) mock_health_monitor = MagicMock() mock_failover_mgr_inner = MagicMock() recovery_svc = OllamaAutoRecoveryService( health_monitor=mock_health_monitor, failover_manager=mock_failover_mgr_inner, ) # Wire callback failover_mgr.set_recovery_callback(recovery_svc.set_current_primary) # 驗收:_recovery_callback 是 recovery_svc.set_current_primary # 注意:bound methods 每次屬性存取都建立新物件,必須用 __func__ + __self__ 比對 cb = failover_mgr._recovery_callback assert cb is not None assert cb.__func__ is recovery_svc.set_current_primary.__func__ assert cb.__self__ is recovery_svc # 驗收:呼叫 callback 等同於呼叫 set_current_primary # (用 mock 驗證真實 set_current_primary 被呼叫) with patch.object(recovery_svc, "set_current_primary", new_callable=AsyncMock) as mock_scp: failover_mgr.set_recovery_callback(recovery_svc.set_current_primary) await failover_mgr._recovery_callback("gemini") mock_scp.assert_awaited_once_with("gemini") finally: reset_ollama_failover_manager() reset_ollama_auto_recovery_service()