"""OllamaClient 单元测试(mock httpx,不发起真实网络请求)""" from typing import Any import httpx import pytest from app.services.ollama import OllamaClient def _make_response(status_code: int, data: dict | None = None) -> httpx.Response: """构造带 request 上下文的 httpx.Response(raise_for_status 依赖 request)""" request = httpx.Request("POST", "http://localhost:11434/api/generate") if data is None: return httpx.Response(status_code, request=request) return httpx.Response(status_code, json=data, request=request) class _FakeAsyncClient: """httpx.AsyncClient 替代品:记录请求参数,返回预设响应或抛出预设异常""" def __init__( self, captured: dict, response: httpx.Response | None = None, error: Exception | None = None, **kwargs: Any, ) -> None: self._captured = captured self._response = response self._error = error captured["timeout"] = kwargs.get("timeout") async def __aenter__(self) -> "_FakeAsyncClient": return self async def __aexit__(self, *args: object) -> bool: return False async def post(self, url: str, json: dict | None = None) -> httpx.Response: self._captured["url"] = url self._captured["json"] = json if self._error is not None: raise self._error assert self._response is not None return self._response async def get(self, url: str) -> httpx.Response: self._captured["url"] = url if self._error is not None: raise self._error assert self._response is not None return self._response class TestGenerate: async def test_returns_response_field(self, monkeypatch: pytest.MonkeyPatch) -> None: captured: dict = {} monkeypatch.setattr( httpx, "AsyncClient", lambda **kw: _FakeAsyncClient(captured, response=_make_response(200, {"response": "生成结果"}), **kw), ) client = OllamaClient(base_url="http://localhost:11434/", model="qwen2.5:1.5b") result = await client.generate("你好") assert result == "生成结果" # base_url 尾部斜杠被去除 assert captured["url"] == "http://localhost:11434/api/generate" assert captured["json"] == {"model": "qwen2.5:1.5b", "prompt": "你好", "stream": False} assert "format" not in captured["json"] assert captured["timeout"] == 120.0 async def test_json_mode_adds_format(self, monkeypatch: pytest.MonkeyPatch) -> None: captured: dict = {} monkeypatch.setattr( httpx, "AsyncClient", lambda **kw: _FakeAsyncClient(captured, response=_make_response(200, {"response": "{}"}), **kw), ) client = OllamaClient(base_url="http://localhost:11434", model="qwen2.5:1.5b") await client.generate("你好", json_mode=True) assert captured["json"]["format"] == "json" async def test_http_error_status_raises(self, monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr( httpx, "AsyncClient", lambda **kw: _FakeAsyncClient({}, response=_make_response(500), **kw) ) client = OllamaClient(base_url="http://localhost:11434", model="qwen2.5:1.5b") with pytest.raises(httpx.HTTPStatusError): await client.generate("你好") class TestIsAvailable: async def test_200_returns_true(self, monkeypatch: pytest.MonkeyPatch) -> None: captured: dict = {} monkeypatch.setattr( httpx, "AsyncClient", lambda **kw: _FakeAsyncClient(captured, response=_make_response(200, {"models": []}), **kw), ) client = OllamaClient(base_url="http://localhost:11434", model="qwen2.5:1.5b") assert await client.is_available() is True assert captured["url"] == "http://localhost:11434/api/tags" assert captured["timeout"] == 5.0 async def test_non_200_returns_false(self, monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr( httpx, "AsyncClient", lambda **kw: _FakeAsyncClient({}, response=_make_response(503), **kw) ) client = OllamaClient(base_url="http://localhost:11434", model="qwen2.5:1.5b") assert await client.is_available() is False async def test_connect_error_returns_false(self, monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr( httpx, "AsyncClient", lambda **kw: _FakeAsyncClient({}, error=httpx.ConnectError("连接被拒绝"), **kw) ) client = OllamaClient(base_url="http://localhost:11434", model="qwen2.5:1.5b") assert await client.is_available() is False