"""RerankerService 单元测试(mock httpx,不发起真实网络请求)""" from typing import Any import httpx import pytest from app.config import settings from app.services.reranker import OllamaRerankerService, create_reranker_service 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/rerank") 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 def _client(monkeypatch: pytest.MonkeyPatch, captured: dict, **kw: Any) -> None: monkeypatch.setattr(httpx, "AsyncClient", lambda **k: _FakeAsyncClient(captured, **k, **kw)) class TestRerank: async def test_returns_scores_aligned_to_documents(self, monkeypatch: pytest.MonkeyPatch) -> None: captured: dict = {} _client( monkeypatch, captured, response=_make_response( 200, { "results": [ {"index": 0, "relevance_score": 0.2}, {"index": 1, "relevance_score": 0.9}, {"index": 2, "relevance_score": 0.5}, ] }, ), ) service = OllamaRerankerService(base_url="http://localhost:11434/", model="qwen3-reranker:0.6b") scores = await service.rerank("查询", ["doc-a", "doc-b", "doc-c"]) assert scores == [0.2, 0.9, 0.5] assert captured["url"] == "http://localhost:11434/api/rerank" assert captured["json"] == { "model": "qwen3-reranker:0.6b", "query": "查询", "documents": ["doc-a", "doc-b", "doc-c"], } async def test_empty_documents_returns_empty(self, monkeypatch: pytest.MonkeyPatch) -> None: captured: dict = {} _client(monkeypatch, captured, response=_make_response(200, {"results": []})) service = OllamaRerankerService(base_url="http://localhost:11434", model="qwen3-reranker:0.6b") scores = await service.rerank("查询", []) assert scores == [] # 空文档不发起请求 assert "url" not in captured async def test_partial_results_zero_fill_missing(self, monkeypatch: pytest.MonkeyPatch) -> None: captured: dict = {} # 仅返回 index 1,index 0/2 缺失 → 补 0 _client( monkeypatch, captured, response=_make_response(200, {"results": [{"index": 1, "relevance_score": 0.7}]}), ) service = OllamaRerankerService(base_url="http://localhost:11434", model="qwen3-reranker:0.6b") scores = await service.rerank("查询", ["a", "b", "c"]) assert scores == [0.0, 0.7, 0.0] async def test_http_error_raises(self, monkeypatch: pytest.MonkeyPatch) -> None: _client(monkeypatch, {}, response=_make_response(500)) service = OllamaRerankerService(base_url="http://localhost:11434", model="qwen3-reranker:0.6b") with pytest.raises(httpx.HTTPStatusError): await service.rerank("查询", ["a", "b"]) class TestCreateRerankerService: def test_disabled_returns_none(self, monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr(settings, "reranker_enabled", False) assert create_reranker_service() is None def test_enabled_returns_service(self, monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr(settings, "reranker_enabled", True) monkeypatch.setattr(settings, "reranker_model", "qwen3-reranker:0.6b") monkeypatch.setattr(settings, "ollama_base_url", "http://ollama:11434") monkeypatch.setattr(settings, "reranker_timeout", 30.0) service = create_reranker_service() assert isinstance(service, OllamaRerankerService) assert service.model == "qwen3-reranker:0.6b"