"""Юнит-тесты OllamaClient (уровень 4 — LLM-анализ парафраза). Сеть замокана: проверяется устойчивость парсинга ответа модели и корректные фолбэки при недоступности/ошибках — кривой вывод LLM не должен ронять проверку. """ import json import httpx import pytest from app import ollama_client as oc class _FakeResponse: def __init__(self, payload: dict): self._payload = payload def raise_for_status(self) -> None: return None def json(self) -> dict: return self._payload def _returns(payload: dict): def _fake(*args, **kwargs): return _FakeResponse(payload) return _fake def _raises(exc: Exception): def _fake(*args, **kwargs): raise exc return _fake @pytest.fixture def client(): return oc.OllamaClient() def test_paraphrase_parses_and_normalizes(monkeypatch, client): payload = {"response": json.dumps( {"is_paraphrase": True, "confidence": 0.87, "reason": "те же идеи"} )} monkeypatch.setattr(oc.httpx, "post", _returns(payload)) assert client.check_paraphrase("оригинал текста", "перефраз того же") == { "is_paraphrase": True, "confidence": 0.87, "reason": "те же идеи", } def test_paraphrase_coerces_types(monkeypatch, client): # confidence пришёл как int, reason отсутствует payload = {"response": json.dumps({"is_paraphrase": False, "confidence": 1})} monkeypatch.setattr(oc.httpx, "post", _returns(payload)) out = client.check_paraphrase("a", "b") assert out["is_paraphrase"] is False assert isinstance(out["confidence"], float) and out["confidence"] == 1.0 assert out["reason"] == "" def test_paraphrase_malformed_json_is_safe(monkeypatch, client): monkeypatch.setattr(oc.httpx, "post", _returns({"response": "не JSON, а болтовня"})) out = client.check_paraphrase("a", "b") assert out["is_paraphrase"] is False assert out["confidence"] == 0.0 assert "парс" in out["reason"].lower() def test_paraphrase_missing_response_key_defaults(monkeypatch, client): # нет ключа "response" → дефолт "{}" → пустой объект → безопасные значения monkeypatch.setattr(oc.httpx, "post", _returns({})) assert client.check_paraphrase("a", "b") == { "is_paraphrase": False, "confidence": 0.0, "reason": "", } def test_paraphrase_timeout_returns_unavailable(monkeypatch, client): monkeypatch.setattr(oc.httpx, "post", _raises(httpx.TimeoutException("timeout"))) out = client.check_paraphrase("a", "b") assert out["is_paraphrase"] is False assert out["reason"] == "LLM недоступна" def test_paraphrase_connect_error_returns_unavailable(monkeypatch, client): monkeypatch.setattr(oc.httpx, "post", _raises(httpx.ConnectError("no route"))) assert client.check_paraphrase("a", "b")["reason"] == "LLM недоступна" def test_paraphrase_unexpected_error_is_caught(monkeypatch, client): monkeypatch.setattr(oc.httpx, "post", _raises(ValueError("boom"))) out = client.check_paraphrase("a", "b") assert out["is_paraphrase"] is False assert out["confidence"] == 0.0 assert out["reason"] == "boom" def test_summarize_returns_stripped_response(monkeypatch, client): monkeypatch.setattr(oc.httpx, "post", _returns({"response": " Краткое изложение. "})) assert client.summarize("Заголовок", "Аннотация") == "Краткое изложение." def test_summarize_falls_back_to_abstract_on_error(monkeypatch, client): monkeypatch.setattr(oc.httpx, "post", _raises(RuntimeError("down"))) assert client.summarize("Заголовок", "Аннотация про исследование") == ( "Аннотация про исследование" ) def test_summarize_falls_back_to_title_when_no_abstract(monkeypatch, client): monkeypatch.setattr(oc.httpx, "post", _raises(RuntimeError("down"))) assert client.summarize("Только заголовок", "") == "Только заголовок" def test_is_available_true_on_200(monkeypatch, client): class _R: status_code = 200 monkeypatch.setattr(oc.httpx, "get", lambda *a, **k: _R()) assert client.is_available() is True def test_is_available_false_on_exception(monkeypatch, client): monkeypatch.setattr(oc.httpx, "get", _raises(httpx.ConnectError("x"))) assert client.is_available() is False