brickfrog commited on
Commit
d04007f
·
verified ·
1 Parent(s): 7505a0b

Upload folder using huggingface_hub

Browse files
Files changed (1) hide show
  1. tests/test_llm_interface.py +208 -0
tests/test_llm_interface.py ADDED
@@ -0,0 +1,208 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pytest
2
+ from unittest.mock import AsyncMock, MagicMock
3
+ from ankigen.llm_interface import OpenAIClientManager, OpenAIRateLimiter
4
+ from openai import OpenAIError
5
+
6
+ # --- OpenAIClientManager Tests ---
7
+
8
+
9
+ @pytest.mark.anyio
10
+ async def test_initialize_client_valid_key(mocker):
11
+ manager = OpenAIClientManager()
12
+ # Mock AsyncOpenAI class
13
+ mock_openai_class = mocker.patch("ankigen.llm_interface.AsyncOpenAI", autospec=True)
14
+
15
+ await manager.initialize_client("sk-valid-key")
16
+
17
+ assert manager._api_key == "sk-valid-key"
18
+ assert manager._client is not None
19
+ mock_openai_class.assert_called_once_with(api_key="sk-valid-key")
20
+
21
+
22
+ @pytest.mark.anyio
23
+ async def test_initialize_client_invalid_key():
24
+ manager = OpenAIClientManager()
25
+ # Key not starting with sk-
26
+ with pytest.raises(ValueError, match="Invalid OpenAI API key format."):
27
+ await manager.initialize_client("invalid-key")
28
+ assert manager._client is None
29
+
30
+
31
+ @pytest.mark.anyio
32
+ async def test_initialize_client_empty_key():
33
+ manager = OpenAIClientManager()
34
+ with pytest.raises(ValueError, match="Invalid OpenAI API key format."):
35
+ await manager.initialize_client("")
36
+ assert manager._client is None
37
+
38
+
39
+ @pytest.mark.anyio
40
+ async def test_initialize_client_openai_error(mocker):
41
+ manager = OpenAIClientManager()
42
+ mocker.patch(
43
+ "ankigen.llm_interface.AsyncOpenAI", side_effect=OpenAIError("API Error")
44
+ )
45
+
46
+ with pytest.raises(OpenAIError):
47
+ await manager.initialize_client("sk-error")
48
+
49
+ assert manager._client is None
50
+
51
+
52
+ @pytest.mark.anyio
53
+ async def test_initialize_client_unexpected_error(mocker):
54
+ manager = OpenAIClientManager()
55
+ mocker.patch(
56
+ "ankigen.llm_interface.AsyncOpenAI", side_effect=Exception("Unexpected")
57
+ )
58
+
59
+ with pytest.raises(
60
+ RuntimeError, match="Unexpected error initializing AsyncOpenAI client."
61
+ ):
62
+ await manager.initialize_client("sk-unexpected")
63
+
64
+ assert manager._client is None
65
+
66
+
67
+ def test_get_client_not_initialized():
68
+ manager = OpenAIClientManager()
69
+ with pytest.raises(RuntimeError, match="AsyncOpenAI client is not initialized"):
70
+ manager.get_client()
71
+
72
+
73
+ @pytest.mark.anyio
74
+ async def test_get_client_initialized(mocker):
75
+ manager = OpenAIClientManager()
76
+ mocker.patch("ankigen.llm_interface.AsyncOpenAI")
77
+ await manager.initialize_client("sk-valid")
78
+
79
+ client = manager.get_client()
80
+ assert client is not None
81
+
82
+
83
+ def test_context_manager_sync(mocker):
84
+ manager = OpenAIClientManager()
85
+ mock_close = mocker.patch.object(manager, "close")
86
+
87
+ with manager as m:
88
+ assert m is manager
89
+
90
+ mock_close.assert_called_once()
91
+
92
+
93
+ @pytest.mark.anyio
94
+ async def test_context_manager_async(mocker):
95
+ manager = OpenAIClientManager()
96
+ mock_aclose = mocker.patch.object(manager, "aclose", new_callable=AsyncMock)
97
+
98
+ async with manager as m:
99
+ assert m is manager
100
+
101
+ mock_aclose.assert_called_once()
102
+
103
+
104
+ def test_close_method(mocker):
105
+ manager = OpenAIClientManager()
106
+ mock_client = MagicMock()
107
+ # Ensure it has 'close' method
108
+ mock_client.close = MagicMock()
109
+ manager._client = mock_client
110
+
111
+ manager.close()
112
+
113
+ mock_client.close.assert_called_once()
114
+ assert manager._client is None
115
+
116
+
117
+ @pytest.mark.anyio
118
+ async def test_aclose_method(mocker):
119
+ manager = OpenAIClientManager()
120
+ mock_client = AsyncMock()
121
+ # Ensure it has 'aclose' method
122
+ mock_client.aclose = AsyncMock()
123
+ manager._client = mock_client
124
+
125
+ await manager.aclose()
126
+
127
+ mock_client.aclose.assert_called_once()
128
+ assert manager._client is None
129
+
130
+
131
+ @pytest.mark.anyio
132
+ async def test_aclose_method_fallback_to_sync(mocker):
133
+ manager = OpenAIClientManager()
134
+ # Create a client mock that only exposes 'close', so 'aclose' is truly absent
135
+ mock_client = MagicMock(spec_set=["close"])
136
+ mock_client.close = MagicMock()
137
+ manager._client = mock_client
138
+
139
+ await manager.aclose()
140
+
141
+ mock_client.close.assert_called_once()
142
+ assert manager._client is None
143
+
144
+
145
+ # --- OpenAIRateLimiter Tests ---
146
+
147
+
148
+ @pytest.mark.anyio
149
+ async def test_rate_limiter_within_limits():
150
+ limiter = OpenAIRateLimiter(tokens_per_minute=100)
151
+ # Should not wait
152
+ await limiter.wait_if_needed(50)
153
+ assert limiter.tokens_used_current_window == 50
154
+
155
+ await limiter.wait_if_needed(40)
156
+ assert limiter.tokens_used_current_window == 90
157
+
158
+
159
+ @pytest.mark.anyio
160
+ async def test_rate_limiter_window_reset(mocker):
161
+ # Mock monotonic to simulate time passing
162
+ mock_monotonic = mocker.patch("time.monotonic")
163
+ mock_monotonic.return_value = 0.0
164
+
165
+ limiter = OpenAIRateLimiter(tokens_per_minute=100)
166
+ await limiter.wait_if_needed(80)
167
+ assert limiter.tokens_used_current_window == 80
168
+
169
+ # Advance time by 61 seconds
170
+ mock_monotonic.return_value = 61.0
171
+
172
+ await limiter.wait_if_needed(10)
173
+
174
+ # Should have reset
175
+ assert limiter.tokens_used_current_window == 10
176
+ assert limiter.current_window_start_time == 61.0
177
+
178
+
179
+ @pytest.mark.anyio
180
+ async def test_rate_limiter_wait_needed(mocker):
181
+ mock_sleep = mocker.patch("asyncio.sleep", new_callable=AsyncMock)
182
+ mock_monotonic = mocker.patch("time.monotonic")
183
+
184
+ # Start at time 0
185
+ mock_monotonic.return_value = 0.0
186
+ limiter = OpenAIRateLimiter(tokens_per_minute=100)
187
+
188
+ # Use 90 tokens
189
+ await limiter.wait_if_needed(90)
190
+ assert limiter.tokens_used_current_window == 90
191
+
192
+ # Next token request of 20 would exceed 100 (90 + 20 = 110)
193
+ # Current time is 0.0. window_start is 0.0.
194
+ # time_to_wait = (0.0 + 60.0) - 0.0 = 60.0
195
+
196
+ # Instead of side_effect list, we use a more robust approach
197
+ # asyncio loop also calls time.monotonic(), so return_value is safer
198
+ # we update it when sleep is called.
199
+ async def side_effect_sleep(delay):
200
+ mock_monotonic.return_value = 60.0 # Fast forward time
201
+
202
+ mock_sleep.side_effect = side_effect_sleep
203
+
204
+ await limiter.wait_if_needed(20)
205
+
206
+ mock_sleep.assert_called_once_with(60.0)
207
+ assert limiter.tokens_used_current_window == 20
208
+ assert limiter.current_window_start_time == 60.0