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>
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
# Зависимости для юнит-тестов worker-gpu (FAISS-индекс L3, в памяти, без GPU).
|
||||
# torch/sentence-transformers НЕ нужны — тестируется только логика индекса.
|
||||
# Зависимости для юнит-тестов worker-gpu (FAISS-индекс L3 + OllamaClient L4).
|
||||
# torch/sentence-transformers НЕ нужны — тестируется только логика индекса и
|
||||
# парсинг ответа LLM (сеть в тестах замокана).
|
||||
pytest==8.2.0
|
||||
faiss-cpu==1.8.0
|
||||
numpy==1.26.4
|
||||
pydantic-settings==2.2.1
|
||||
httpx==0.27.0
|
||||
|
||||
129
services/worker-gpu/tests/test_ollama_client.py
Normal file
129
services/worker-gpu/tests/test_ollama_client.py
Normal file
@@ -0,0 +1,129 @@
|
||||
"""Юнит-тесты 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
|
||||
Reference in New Issue
Block a user