e4dcfc49aa
Tests / Import Check (Python 3.13) (push) Has been cancelled
Tests / Import Check (Python 3.14) (push) Has been cancelled
Tests / Python Tests (Python 3.11) (push) Has been cancelled
Tests / Python Tests (Python 3.12) (push) Has been cancelled
Tests / Python Tests (Python 3.14) (push) Has been cancelled
Tests / Test Summary (push) Has been cancelled
Tests / Lint and Format (push) Has been cancelled
Tests / Web Node Tests (push) Has been cancelled
Tests / Import Check (Python 3.11) (push) Has been cancelled
Tests / Import Check (Python 3.12) (push) Has been cancelled
Tests / Python Tests (Python 3.13) (push) Has been cancelled
195 lines
5.2 KiB
Python
195 lines
5.2 KiB
Python
"""Tests for cloud provider helpers."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import importlib
|
|
from types import TracebackType
|
|
|
|
from _pytest.monkeypatch import MonkeyPatch
|
|
import pytest
|
|
|
|
from deeptutor.services.llm.exceptions import LLMAPIError
|
|
|
|
cloud_provider = importlib.import_module("deeptutor.services.llm.cloud_provider")
|
|
|
|
|
|
class _AsyncIterator:
|
|
def __init__(self, items: list[bytes]) -> None:
|
|
self._items = items
|
|
self._index = 0
|
|
|
|
def __aiter__(self):
|
|
return self
|
|
|
|
async def __anext__(self) -> bytes:
|
|
if self._index >= len(self._items):
|
|
raise StopAsyncIteration
|
|
item = self._items[self._index]
|
|
self._index += 1
|
|
return item
|
|
|
|
|
|
class _FakeResponse:
|
|
def __init__(self, status: int, json_data: dict[str, object]) -> None:
|
|
self.status = status
|
|
self._json_data = json_data
|
|
self.content = _AsyncIterator([])
|
|
|
|
async def __aenter__(self):
|
|
return self
|
|
|
|
async def __aexit__(
|
|
self,
|
|
exc_type: type[BaseException] | None,
|
|
exc: BaseException | None,
|
|
tb: TracebackType | None,
|
|
) -> None:
|
|
return None
|
|
|
|
async def json(self):
|
|
return self._json_data
|
|
|
|
async def text(self) -> str:
|
|
return ""
|
|
|
|
|
|
class _FakeStreamResponse(_FakeResponse):
|
|
def __init__(self, status: int, lines: list[bytes]) -> None:
|
|
super().__init__(status, {})
|
|
self.content = _AsyncIterator(lines)
|
|
|
|
|
|
class _FakeSession:
|
|
def __init__(self, response: _FakeResponse) -> None:
|
|
self._response = response
|
|
|
|
async def __aenter__(self):
|
|
return self
|
|
|
|
async def __aexit__(
|
|
self,
|
|
exc_type: type[BaseException] | None,
|
|
exc: BaseException | None,
|
|
tb: TracebackType | None,
|
|
) -> None:
|
|
return None
|
|
|
|
def post(self, _url: str, **_kwargs: object) -> _FakeResponse:
|
|
return self._response
|
|
|
|
def get(self, _url: str, **_kwargs: object) -> _FakeResponse:
|
|
return self._response
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_cloud_complete_fallback(monkeypatch: MonkeyPatch) -> None:
|
|
"""Fallback path should parse JSON content from aiohttp responses."""
|
|
fake_response = _FakeResponse(
|
|
200,
|
|
{
|
|
"choices": [
|
|
{"message": {"content": "ok"}},
|
|
]
|
|
},
|
|
)
|
|
|
|
monkeypatch.setattr(
|
|
cloud_provider.aiohttp,
|
|
"ClientSession",
|
|
lambda *args, **kwargs: _FakeSession(fake_response),
|
|
)
|
|
|
|
result = await cloud_provider.complete(
|
|
prompt="hello",
|
|
model="gpt-test",
|
|
api_key="",
|
|
base_url="https://api.openai.com/v1",
|
|
binding="openai",
|
|
)
|
|
|
|
assert result == "ok"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_cloud_stream_yields_chunks(monkeypatch: MonkeyPatch) -> None:
|
|
"""Streaming should yield delta content from SSE lines."""
|
|
lines = [
|
|
b'data: {"choices": [{"delta": {"content": "hi"}}]}\n\n',
|
|
b"data: [DONE]\n\n",
|
|
]
|
|
fake_response = _FakeStreamResponse(200, lines)
|
|
|
|
monkeypatch.setattr(
|
|
cloud_provider.aiohttp,
|
|
"ClientSession",
|
|
lambda *args, **kwargs: _FakeSession(fake_response),
|
|
)
|
|
|
|
chunks = []
|
|
async for chunk in cloud_provider.stream(
|
|
prompt="hello",
|
|
model="gpt-test",
|
|
api_key="",
|
|
base_url="https://api.openai.com/v1",
|
|
binding="openai",
|
|
):
|
|
chunks.append(chunk)
|
|
|
|
assert "".join(chunks) == "hi"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_cloud_complete_error(monkeypatch: MonkeyPatch) -> None:
|
|
"""Non-200 responses should raise LLMAPIError."""
|
|
response = _FakeResponse(500, {})
|
|
|
|
monkeypatch.setattr(
|
|
cloud_provider.aiohttp,
|
|
"ClientSession",
|
|
lambda *args, **kwargs: _FakeSession(response),
|
|
)
|
|
|
|
with pytest.raises(LLMAPIError):
|
|
await cloud_provider.complete(
|
|
prompt="hello",
|
|
model="gpt-test",
|
|
api_key="",
|
|
base_url="https://api.openai.com/v1",
|
|
binding="openai",
|
|
)
|
|
|
|
|
|
def test_cloud_helpers(monkeypatch: MonkeyPatch) -> None:
|
|
"""Helper coercion and SSL connector paths should behave as expected."""
|
|
assert cloud_provider._coerce_float(True, 0.5) == 0.5
|
|
assert cloud_provider._coerce_float(2, 0.5) == 2.0
|
|
assert cloud_provider._coerce_int(True, None) is None
|
|
assert cloud_provider._coerce_int(3, None) == 3
|
|
|
|
monkeypatch.delenv("DISABLE_SSL_VERIFY", raising=False)
|
|
assert cloud_provider._get_aiohttp_connector() is None
|
|
|
|
class _FakeConnector:
|
|
pass
|
|
|
|
monkeypatch.setenv("DISABLE_SSL_VERIFY", "1")
|
|
monkeypatch.setitem(cloud_provider.__dict__, "_ssl_warning_logged", False)
|
|
monkeypatch.setattr(cloud_provider.aiohttp, "TCPConnector", lambda **_kw: _FakeConnector())
|
|
connector = cloud_provider._get_aiohttp_connector()
|
|
assert connector is not None
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_cloud_fetch_models(monkeypatch: MonkeyPatch) -> None:
|
|
"""Fetch models should parse model lists from the response."""
|
|
response = _FakeResponse(200, {"data": [{"id": "m1"}, {"id": "m2"}]})
|
|
monkeypatch.setattr(
|
|
cloud_provider.aiohttp,
|
|
"ClientSession",
|
|
lambda *args, **kwargs: _FakeSession(response),
|
|
)
|
|
|
|
models = await cloud_provider.fetch_models("https://api.openai.com/v1")
|
|
|
|
assert models == ["m1", "m2"]
|