Spaces:
Sleeping
Sleeping
| 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] | |