from __future__ import annotations from pathlib import Path from types import SimpleNamespace from typing import Any import pytest from src.services import chat_manager as chat_module from src.services.chat_manager import ChatManager class _FakeResponse: def __init__(self, content: str = "老闆,系統目前穩定。") -> None: self._content = content def raise_for_status(self) -> None: return None def json(self) -> dict[str, Any]: return { "message": {"content": self._content}, "prompt_eval_count": 11, "eval_count": 13, } class _FakeAsyncClient: posted: list[tuple[str, dict[str, Any]]] = [] def __init__(self, *args: Any, **kwargs: Any) -> None: self.args = args self.kwargs = kwargs async def __aenter__(self) -> _FakeAsyncClient: return self async def __aexit__(self, *args: Any) -> None: return None async def post(self, url: str, *, json: dict[str, Any]) -> _FakeResponse: self.posted.append((url, json)) return _FakeResponse() def _settings() -> SimpleNamespace: return SimpleNamespace(OPENCLAW_DEFAULT_MODEL="qwen3:14b") def _fake_ollama_order(_workload_type: str) -> tuple[SimpleNamespace, ...]: return ( SimpleNamespace(url="http://gcp-a:11435", provider_name="ollama_gcp_a"), SimpleNamespace(url="http://gcp-b:11436", provider_name="ollama_gcp_b"), SimpleNamespace(url="http://local-111:11434", provider_name="ollama_local"), ) @pytest.fixture(autouse=True) def _reset_fake_client() -> None: _FakeAsyncClient.posted = [] @pytest.mark.asyncio async def test_openclaw_chat_uses_ollama_interactive_lane( monkeypatch: pytest.MonkeyPatch, ) -> None: monkeypatch.setattr(chat_module.httpx, "AsyncClient", _FakeAsyncClient) monkeypatch.setattr(chat_module, "get_settings", _settings) monkeypatch.setattr( chat_module, "resolve_ollama_order", _fake_ollama_order, ) result = await ChatManager()._call_openclaw("system context", "幫我看狀態") assert result is not None assert "qwen3:14b" in result assert "免費" in result assert len(_FakeAsyncClient.posted) == 1 url, payload = _FakeAsyncClient.posted[0] assert url == "http://gcp-a:11435/api/chat" assert payload["model"] == "qwen3:14b" assert payload["messages"][0]["role"] == "system" assert payload["messages"][1] == {"role": "user", "content": "幫我看狀態"} @pytest.mark.asyncio async def test_nemoclaw_chat_uses_resolved_interactive_lane( monkeypatch: pytest.MonkeyPatch, ) -> None: monkeypatch.setattr(chat_module.httpx, "AsyncClient", _FakeAsyncClient) monkeypatch.setattr( chat_module, "resolve_ollama_order", _fake_ollama_order, ) result = await ChatManager()._call_nemotron("system context", "補充觀點") assert result is not None url, payload = _FakeAsyncClient.posted[0] assert url == "http://gcp-a:11435/api/chat" assert payload["model"] == "deepseek-r1:14b" assert payload["think"] is False def test_chat_manager_has_no_direct_gemini_generation_path() -> None: source_path = Path(chat_module.__file__).resolve() source = source_path.read_text(encoding="utf-8") assert "generativelanguage.googleapis.com" not in source assert "GEMINI_API_KEY" not in source