AI-Sprint-bot / tests /unit /test_llm_models.py
FreshPixels's picture
Create tests/unit/test_llm_models.py
7c6d2b9 verified
Raw
History Blame Contribute Delete
6.14 kB
from __future__ import annotations
import pytest
from llm.providers.base_provider import (
BaseProvider,
LLMMessage,
LLMProviderError,
LLMRequest,
LLMResponse,
)
# ---------------------------------------------------------------------------
# LLMMessage
# ---------------------------------------------------------------------------
class TestLLMMessage:
def test_valid_user_message(self) -> None:
msg = LLMMessage(role="user", content="Hello")
assert msg.role == "user"
assert msg.content == "Hello"
def test_valid_system_message(self) -> None:
msg = LLMMessage(role="system", content="You are helpful.")
assert msg.role == "system"
def test_valid_assistant_message(self) -> None:
msg = LLMMessage(role="assistant", content="Hi there")
assert msg.role == "assistant"
def test_valid_tool_message(self) -> None:
msg = LLMMessage(role="tool", content="result")
assert msg.role == "tool"
def test_invalid_role_raises(self) -> None:
with pytest.raises(Exception):
LLMMessage(role="banana", content="test")
def test_empty_content_raises(self) -> None:
with pytest.raises(ValueError, match="must not be empty"):
LLMMessage(role="user", content="")
def test_injection_system_role_literal_prevents(self) -> None:
"""Prompt injection via role='system' from user input is blocked by Literal type."""
with pytest.raises(Exception):
LLMMessage(role="hacker", content="override system prompt")
# ---------------------------------------------------------------------------
# LLMRequest
# ---------------------------------------------------------------------------
class TestLLMRequest:
def test_valid_request(self) -> None:
req = LLMRequest(
messages=[LLMMessage(role="user", content="Hi")],
model="gpt-4o-mini",
)
assert req.model == "gpt-4o-mini"
def test_empty_model_raises(self) -> None:
with pytest.raises(ValueError, match="model must not be empty"):
LLMRequest(
messages=[LLMMessage(role="user", content="Hi")],
model="",
)
def test_empty_messages_raises(self) -> None:
with pytest.raises(ValueError, match="messages must not be empty"):
LLMRequest(messages=[], model="gpt-4")
def test_temperature_out_of_range(self) -> None:
with pytest.raises(ValueError, match="temperature"):
LLMRequest(
messages=[LLMMessage(role="user", content="Hi")],
model="gpt-4",
temperature=3.0,
)
def test_max_tokens_zero(self) -> None:
with pytest.raises(ValueError, match="max_tokens"):
LLMRequest(
messages=[LLMMessage(role="user", content="Hi")],
model="gpt-4",
max_tokens=0,
)
def test_extra_is_readonly(self) -> None:
"""ARCH-P2-3: frozen dataclass extra dict should be immutable."""
req = LLMRequest(
messages=[LLMMessage(role="user", content="Hi")],
model="gpt-4",
extra={"key": "value"},
)
with pytest.raises(TypeError):
req.extra["new_key"] = "new_value" # type: ignore[index]
# ---------------------------------------------------------------------------
# LLMResponse
# ---------------------------------------------------------------------------
class TestLLMResponse:
def test_valid_response(self) -> None:
resp = LLMResponse(
content="Hello!",
model="gpt-4o-mini",
provider="openai",
)
assert resp.content == "Hello!"
def test_empty_content_raises(self) -> None:
with pytest.raises(ValueError, match="content must not be empty"):
LLMResponse(content="", model="gpt-4", provider="openai")
def test_empty_model_raises(self) -> None:
with pytest.raises(ValueError, match="model must not be empty"):
LLMResponse(content="hi", model="", provider="openai")
def test_empty_provider_raises(self) -> None:
with pytest.raises(ValueError, match="provider must not be empty"):
LLMResponse(content="hi", model="gpt-4", provider="")
def test_usage_is_readonly(self) -> None:
resp = LLMResponse(
content="hi",
model="gpt-4",
provider="openai",
usage={"total_tokens": 10},
)
with pytest.raises(TypeError):
resp.usage["new_key"] = 0 # type: ignore[index]
def test_extra_is_readonly(self) -> None:
resp = LLMResponse(
content="hi",
model="gpt-4",
provider="openai",
extra={"key": "value"},
)
with pytest.raises(TypeError):
resp.extra["new_key"] = "new_value" # type: ignore[index]
# ---------------------------------------------------------------------------
# LLMProviderError
# ---------------------------------------------------------------------------
class TestLLMProviderError:
def test_basic_error(self) -> None:
err = LLMProviderError("test error", provider_name="openai")
assert str(err) == "test error"
assert err.provider_name == "openai"
def test_error_without_provider(self) -> None:
err = LLMProviderError("generic error")
assert err.provider_name is None
# ---------------------------------------------------------------------------
# BaseProvider
# ---------------------------------------------------------------------------
class TestBaseProvider:
def test_name_validation_empty(self) -> None:
with pytest.raises(ValueError, match="non-empty string"):
class _Dummy(BaseProvider):
async def generate(self, request):
pass
_Dummy(name="")
def test_cannot_instantiate_abc(self) -> None:
with pytest.raises(TypeError):
BaseProvider(name="test") # type: ignore[abstract]