FreshPixels commited on
Commit
7c6d2b9
·
verified ·
1 Parent(s): 6fd04ab

Create tests/unit/test_llm_models.py

Browse files
Files changed (1) hide show
  1. tests/unit/test_llm_models.py +176 -0
tests/unit/test_llm_models.py ADDED
@@ -0,0 +1,176 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import pytest
4
+
5
+ from llm.providers.base_provider import (
6
+ BaseProvider,
7
+ LLMMessage,
8
+ LLMProviderError,
9
+ LLMRequest,
10
+ LLMResponse,
11
+ )
12
+
13
+
14
+ # ---------------------------------------------------------------------------
15
+ # LLMMessage
16
+ # ---------------------------------------------------------------------------
17
+
18
+ class TestLLMMessage:
19
+ def test_valid_user_message(self) -> None:
20
+ msg = LLMMessage(role="user", content="Hello")
21
+ assert msg.role == "user"
22
+ assert msg.content == "Hello"
23
+
24
+ def test_valid_system_message(self) -> None:
25
+ msg = LLMMessage(role="system", content="You are helpful.")
26
+ assert msg.role == "system"
27
+
28
+ def test_valid_assistant_message(self) -> None:
29
+ msg = LLMMessage(role="assistant", content="Hi there")
30
+ assert msg.role == "assistant"
31
+
32
+ def test_valid_tool_message(self) -> None:
33
+ msg = LLMMessage(role="tool", content="result")
34
+ assert msg.role == "tool"
35
+
36
+ def test_invalid_role_raises(self) -> None:
37
+ with pytest.raises(Exception):
38
+ LLMMessage(role="banana", content="test")
39
+
40
+ def test_empty_content_raises(self) -> None:
41
+ with pytest.raises(ValueError, match="must not be empty"):
42
+ LLMMessage(role="user", content="")
43
+
44
+ def test_injection_system_role_literal_prevents(self) -> None:
45
+ """Prompt injection via role='system' from user input is blocked by Literal type."""
46
+ with pytest.raises(Exception):
47
+ LLMMessage(role="hacker", content="override system prompt")
48
+
49
+
50
+ # ---------------------------------------------------------------------------
51
+ # LLMRequest
52
+ # ---------------------------------------------------------------------------
53
+
54
+ class TestLLMRequest:
55
+ def test_valid_request(self) -> None:
56
+ req = LLMRequest(
57
+ messages=[LLMMessage(role="user", content="Hi")],
58
+ model="gpt-4o-mini",
59
+ )
60
+ assert req.model == "gpt-4o-mini"
61
+
62
+ def test_empty_model_raises(self) -> None:
63
+ with pytest.raises(ValueError, match="model must not be empty"):
64
+ LLMRequest(
65
+ messages=[LLMMessage(role="user", content="Hi")],
66
+ model="",
67
+ )
68
+
69
+ def test_empty_messages_raises(self) -> None:
70
+ with pytest.raises(ValueError, match="messages must not be empty"):
71
+ LLMRequest(messages=[], model="gpt-4")
72
+
73
+ def test_temperature_out_of_range(self) -> None:
74
+ with pytest.raises(ValueError, match="temperature"):
75
+ LLMRequest(
76
+ messages=[LLMMessage(role="user", content="Hi")],
77
+ model="gpt-4",
78
+ temperature=3.0,
79
+ )
80
+
81
+ def test_max_tokens_zero(self) -> None:
82
+ with pytest.raises(ValueError, match="max_tokens"):
83
+ LLMRequest(
84
+ messages=[LLMMessage(role="user", content="Hi")],
85
+ model="gpt-4",
86
+ max_tokens=0,
87
+ )
88
+
89
+ def test_extra_is_readonly(self) -> None:
90
+ """ARCH-P2-3: frozen dataclass extra dict should be immutable."""
91
+ req = LLMRequest(
92
+ messages=[LLMMessage(role="user", content="Hi")],
93
+ model="gpt-4",
94
+ extra={"key": "value"},
95
+ )
96
+ with pytest.raises(TypeError):
97
+ req.extra["new_key"] = "new_value" # type: ignore[index]
98
+
99
+
100
+ # ---------------------------------------------------------------------------
101
+ # LLMResponse
102
+ # ---------------------------------------------------------------------------
103
+
104
+ class TestLLMResponse:
105
+ def test_valid_response(self) -> None:
106
+ resp = LLMResponse(
107
+ content="Hello!",
108
+ model="gpt-4o-mini",
109
+ provider="openai",
110
+ )
111
+ assert resp.content == "Hello!"
112
+
113
+ def test_empty_content_raises(self) -> None:
114
+ with pytest.raises(ValueError, match="content must not be empty"):
115
+ LLMResponse(content="", model="gpt-4", provider="openai")
116
+
117
+ def test_empty_model_raises(self) -> None:
118
+ with pytest.raises(ValueError, match="model must not be empty"):
119
+ LLMResponse(content="hi", model="", provider="openai")
120
+
121
+ def test_empty_provider_raises(self) -> None:
122
+ with pytest.raises(ValueError, match="provider must not be empty"):
123
+ LLMResponse(content="hi", model="gpt-4", provider="")
124
+
125
+ def test_usage_is_readonly(self) -> None:
126
+ resp = LLMResponse(
127
+ content="hi",
128
+ model="gpt-4",
129
+ provider="openai",
130
+ usage={"total_tokens": 10},
131
+ )
132
+ with pytest.raises(TypeError):
133
+ resp.usage["new_key"] = 0 # type: ignore[index]
134
+
135
+ def test_extra_is_readonly(self) -> None:
136
+ resp = LLMResponse(
137
+ content="hi",
138
+ model="gpt-4",
139
+ provider="openai",
140
+ extra={"key": "value"},
141
+ )
142
+ with pytest.raises(TypeError):
143
+ resp.extra["new_key"] = "new_value" # type: ignore[index]
144
+
145
+
146
+ # ---------------------------------------------------------------------------
147
+ # LLMProviderError
148
+ # ---------------------------------------------------------------------------
149
+
150
+ class TestLLMProviderError:
151
+ def test_basic_error(self) -> None:
152
+ err = LLMProviderError("test error", provider_name="openai")
153
+ assert str(err) == "test error"
154
+ assert err.provider_name == "openai"
155
+
156
+ def test_error_without_provider(self) -> None:
157
+ err = LLMProviderError("generic error")
158
+ assert err.provider_name is None
159
+
160
+
161
+ # ---------------------------------------------------------------------------
162
+ # BaseProvider
163
+ # ---------------------------------------------------------------------------
164
+
165
+ class TestBaseProvider:
166
+ def test_name_validation_empty(self) -> None:
167
+ with pytest.raises(ValueError, match="non-empty string"):
168
+ class _Dummy(BaseProvider):
169
+ async def generate(self, request):
170
+ pass
171
+ _Dummy(name="")
172
+
173
+ def test_cannot_instantiate_abc(self) -> None:
174
+ with pytest.raises(TypeError):
175
+ BaseProvider(name="test") # type: ignore[abstract]
176
+