""" P0 DIAGNOSE Routing Tests ========================== 測試 AIRouter DIAGNOSE 路由 + require_local 隔離行為 建立時間: 2026-04-04 (台北時區) 建立者: Claude Code (P0 DIAGNOSE Privacy-First) 2026-04-05 v4.3: Ollama CPU-only 238s 不可用;DIAGNOSE 統一走 NIM (_full_fallback_chain) """ import os os.environ.setdefault("MOCK_MODE", "true") import pytest from unittest.mock import AsyncMock, MagicMock, patch class TestNemotronPerTaskTimeout: """Nemotron 支援 per-task timeout""" @pytest.mark.asyncio async def test_diagnose_uses_diagnose_timeout(self): """DIAGNOSE context 應使用 NEMOTRON_DIAGNOSE_TIMEOUT_SECONDS""" from src.services.ai_providers.nemotron import NemotronProvider provider = NemotronProvider() # 建立 mock nvidia provider mock_nvidia = MagicMock() mock_result = MagicMock() mock_result.tool_calls = [] mock_nvidia.tool_call = AsyncMock(return_value=mock_result) with patch.object(provider, '_get_nvidia', return_value=mock_nvidia): result = await provider.analyze( prompt="測試診斷", context={"task_type": "diagnose"}, ) assert result.success is True mock_nvidia.tool_call.assert_called_once() class TestLocalFallbackChain: """require_local=True 時 privacy 過濾生效,cloud provider 不被呼叫;全部失敗 → REJECT""" @pytest.mark.asyncio async def test_require_local_skips_cloud_providers(self): """require_local=True 時,cloud provider 不被呼叫""" import os from src.services.ai_router import AIRouterExecutor, AIProviderRegistry from src.services.ai_providers.interfaces import AIResult registry = AIProviderRegistry() # Mock: Ollama 成功 mock_ollama = AsyncMock() mock_ollama.name = "ollama" mock_ollama.privacy_level = "local" mock_ollama.is_enabled = True mock_ollama.capabilities = {"rca", "chat"} mock_ollama.analyze = AsyncMock(return_value=AIResult( raw_response="本地診斷結果", success=True, provider="ollama", )) mock_ollama.health_check = AsyncMock(return_value=True) # Mock: Gemini(不應該被呼叫) mock_gemini = AsyncMock() mock_gemini.name = "gemini" mock_gemini.privacy_level = "cloud" mock_gemini.is_enabled = True mock_gemini.analyze = AsyncMock(return_value=AIResult( raw_response="雲端結果", success=True, provider="gemini", )) registry._providers = { "ollama": mock_ollama, "gemini": mock_gemini, } executor = AIRouterExecutor(registry) # 暫時關閉 MOCK_MODE,測試真實執行路徑 with patch("src.services.ai_router._settings") as mock_settings: mock_settings.MOCK_MODE = False result = await executor.execute( prompt="診斷這個問題", provider_order=["ollama", "gemini"], require_local=True, ) assert result.success is True assert result.provider == "ollama" mock_gemini.analyze.assert_not_called() @pytest.mark.asyncio async def test_require_local_all_fail_returns_reject(self): """require_local=True 且所有 local provider 失敗 → 回傳明確錯誤""" import os from src.services.ai_router import AIRouterExecutor, AIProviderRegistry from src.services.ai_providers.interfaces import AIResult registry = AIProviderRegistry() # Mock: Ollama 失敗 mock_ollama = AsyncMock() mock_ollama.name = "ollama" mock_ollama.privacy_level = "local" mock_ollama.is_enabled = True mock_ollama.capabilities = {"rca", "chat"} mock_ollama.analyze = AsyncMock(return_value=AIResult( raw_response="", success=False, provider="ollama", error="timeout", )) mock_ollama.health_check = AsyncMock(return_value=False) registry._providers = { "ollama": mock_ollama, } executor = AIRouterExecutor(registry) # 暫時關閉 MOCK_MODE + 讓 telegram import 失敗(不影響主流程) with patch("src.services.ai_router._settings") as mock_settings: mock_settings.MOCK_MODE = False result = await executor.execute( prompt="診斷這個問題", provider_order=["ollama"], require_local=True, ) assert result.success is False assert result.error == "local_providers_unavailable" class TestDiagnoseIntentOverride: """DIAGNOSE intent 路由設定驗證""" def test_diagnose_override_is_ollama(self): """_intent_provider_overrides[DIAGNOSE] 應為 OLLAMA(2026-04-29 推翻 A2) 歷史脈絡: - 2026-04-12 ogt: NEMOTRON routing 暫停 — NIM tool_call 無 confidence 欄位 - 2026-04-16 ogt: 恢復 DIAGNOSE → OPENCLAW_NEMO — None 複雜度路由落入 Rule 6 → Ollama deepseek-r1:14b CPU 需 238s → timeout → degraded → 全部「待分析」 - 2026-04-27 Claude Sonnet 4.6 A2: 確立「Ollama 永久排除於 DIAGNOSE chain」 2026-04-29 推翻 A2 鐵律: - 統帥指令: 「主要優先用 111 主機的 Ollama」 - 統帥鐵律 feedback_ai_autonomous_direction.md: 以本地免費 LLM 為主 - 統帥鐵律 feedback_ollama_111_only.md: Ollama 唯一主機 = 111 - 新事實: prod Ollama 111 = M1 Pro Apple Silicon GPU + qwen2.5:7b-instruct VRAM 8.2GB 全載入,實測 hi 0.54s - 雲端全死: OpenClaw 500 / Gemini 429 / Claude 404 - 配套:openclaw.py 注入 task_type="diagnose" → Ollama 用 200s timeout """ from src.services.ai_router import AIRouter, AIProviderEnum from src.services.intent_classifier import IntentType router = AIRouter() override = router._intent_provider_overrides.get(IntentType.DIAGNOSE) assert override is AIProviderEnum.OLLAMA, ( f"統帥鐵律: DIAGNOSE 應為 OLLAMA(本地優先),實際為 {override}" )