Files
anti-plagiarism/services/worker-gpu/tests/test_ollama_client.py
jze9 426796a9a7 test(gpu): покрыть L4 OllamaClient (парсинг парафраза, 12 тестов)
Сеть замокана (monkeypatch httpx.post/get). Проверяется устойчивость слоя LLM:
- нормализация ответа: is_paraphrase→bool, confidence→float, reason→str;
- кривой/невалидный JSON от модели → безопасные дефолты, не падение;
- отсутствие ключа "response" → пустой объект → безопасные значения;
- таймаут/ConnectError → «LLM недоступна»; прочие ошибки перехвачены;
- summarize: strip ответа и фолбэк в аннотацию/заголовок при ошибке;
- is_available: 200 → True, исключение → False.

Тестов всего: 53 (indexer 19, gost 17, gpu 17). httpx добавлен в requirements-test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-11 20:10:34 +05:00

130 lines
4.6 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Юнит-тесты 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