Первоначальная версия VidConf

This commit is contained in:
2026-07-23 01:04:01 +03:00
commit 896455381a
335 changed files with 61527 additions and 0 deletions

View File

@@ -0,0 +1,206 @@
"""Тесты `OpenAICompatClient` на мок-транспорте `httpx.MockTransport` (без сети).
Покрывает: успешный запрос, retry после 5xx и после сетевой ошибки → успех,
исчерпание попыток → `LlmUnavailableError`, открытие circuit breaker после
серии неудач (следующий вызов падает без HTTP-запроса) и его закрытие после
`breaker_cooldown_s`, а также явное закрытие базового `httpx.Client`
(`close()` и поддержка контекстного менеджера).
"""
import time
from collections.abc import Callable
import httpx
import pytest
from core.summarization import llm_client as llm_client_module
from core.summarization.llm_client import LlmUnavailableError, OpenAICompatClient
@pytest.fixture(autouse=True)
def _fast_backoff(monkeypatch: pytest.MonkeyPatch) -> None:
"""Свести паузы retry к нулю, чтобы тесты не ждали реальный backoff."""
monkeypatch.setattr(llm_client_module, "_INITIAL_BACKOFF_S", 0.0)
def _make_client(
handler: Callable[[httpx.Request], httpx.Response], **kwargs: object
) -> OpenAICompatClient:
transport = httpx.MockTransport(handler)
return OpenAICompatClient(
base_url="http://llm.test/v1",
model="qwen2.5-3b-instruct-q4_k_m",
transport=transport,
**kwargs, # type: ignore[arg-type]
)
def _ok_response(content: str = "готовое резюме") -> httpx.Response:
return httpx.Response(
200,
json={"choices": [{"message": {"content": content}}]},
)
def test_complete_returns_content_on_success() -> None:
calls: list[httpx.Request] = []
def handler(request: httpx.Request) -> httpx.Response:
calls.append(request)
assert request.url == "http://llm.test/v1/chat/completions"
body = request.read()
assert b'"role": "user"' in body or b'"role":"user"' in body
return _ok_response("успех")
client = _make_client(handler)
result = client.complete("промпт")
assert result == "успех"
assert len(calls) == 1
def test_complete_uses_per_call_max_tokens_override() -> None:
"""`max_tokens` явного вызова `complete()` перекрывает дефолт конструктора клиента
(ADR-004: разные лимиты map/reduce у `QwenLocal`, один и тот же клиент)."""
import json
captured: list[int] = []
def handler(request: httpx.Request) -> httpx.Response:
body = json.loads(request.read())
captured.append(body["max_tokens"])
return _ok_response("ок")
client = _make_client(handler, max_tokens=1024)
client.complete("промпт map", max_tokens=1024)
client.complete("промпт reduce", max_tokens=2048)
client.complete("промпт без переопределения")
assert captured == [1024, 2048, 1024]
def test_complete_retries_after_5xx_then_succeeds() -> None:
attempts = {"count": 0}
def handler(request: httpx.Request) -> httpx.Response:
attempts["count"] += 1
if attempts["count"] < 3:
return httpx.Response(503)
return _ok_response("успех после 503")
client = _make_client(handler, max_attempts=3)
result = client.complete("промпт")
assert result == "успех после 503"
assert attempts["count"] == 3
def test_complete_retries_after_network_error_then_succeeds() -> None:
attempts = {"count": 0}
def handler(request: httpx.Request) -> httpx.Response:
attempts["count"] += 1
if attempts["count"] == 1:
raise httpx.ConnectError("соединение разорвано", request=request)
return _ok_response("успех после обрыва сети")
client = _make_client(handler, max_attempts=3)
result = client.complete("промпт")
assert result == "успех после обрыва сети"
assert attempts["count"] == 2
def test_complete_raises_llm_unavailable_after_exhausting_attempts() -> None:
attempts = {"count": 0}
def handler(request: httpx.Request) -> httpx.Response:
attempts["count"] += 1
return httpx.Response(500)
client = _make_client(handler, max_attempts=3, breaker_threshold=100)
with pytest.raises(LlmUnavailableError):
client.complete("промпт")
assert attempts["count"] == 3
def test_breaker_opens_after_threshold_and_blocks_without_http_call() -> None:
attempts = {"count": 0}
def handler(request: httpx.Request) -> httpx.Response:
attempts["count"] += 1
return httpx.Response(500)
client = _make_client(handler, max_attempts=1, breaker_threshold=2, breaker_cooldown_s=60.0)
with pytest.raises(LlmUnavailableError):
client.complete("промпт 1")
with pytest.raises(LlmUnavailableError):
client.complete("промпт 2")
assert attempts["count"] == 2 # обе неудачи дошли до сервера — порог достигнут
with pytest.raises(LlmUnavailableError):
client.complete("промпт 3")
assert attempts["count"] == 2 # третий вызов не сделал HTTP-запрос — breaker открыт
def test_breaker_closes_after_cooldown_elapses(monkeypatch: pytest.MonkeyPatch) -> None:
attempts = {"count": 0}
def handler(request: httpx.Request) -> httpx.Response:
attempts["count"] += 1
if attempts["count"] <= 2:
return httpx.Response(500)
return _ok_response("успех после закрытия breaker")
client = _make_client(handler, max_attempts=1, breaker_threshold=2, breaker_cooldown_s=10.0)
with pytest.raises(LlmUnavailableError):
client.complete("промпт 1")
with pytest.raises(LlmUnavailableError):
client.complete("промпт 2")
assert attempts["count"] == 2
# Имитируем истечение окна отказа сдвигом монотонных часов вперёд (модуль
# `time` — общий синглтон интерпретатора, поэтому патчим его напрямую:
# изменение видно и внутри `core.summarization.llm_client`).
real_monotonic = time.monotonic()
monkeypatch.setattr(time, "monotonic", lambda: real_monotonic + 11.0)
result = client.complete("промпт 3")
assert result == "успех после закрытия breaker"
def test_close_closes_underlying_http_client() -> None:
"""`close()` закрывает базовый `httpx.Client` (освобождает пул соединений)."""
def handler(request: httpx.Request) -> httpx.Response:
return _ok_response()
client = _make_client(handler)
client.close()
assert client._client.is_closed
def test_context_manager_closes_client_on_exit() -> None:
"""`OpenAICompatClient` работает как контекстный менеджер, закрывающий клиент на выходе."""
def handler(request: httpx.Request) -> httpx.Response:
return _ok_response("успех в контексте")
with _make_client(handler) as client:
assert client.complete("промпт") == "успех в контексте"
assert not client._client.is_closed
assert client._client.is_closed