@@ -43,12 +43,9 @@ def sync_client_environment():
43
43
# Force reset class state before the test.
44
44
# This ensures any client created will start a new loop/thread.
45
45
46
- assert (
47
- not original_loop or not original_loop .is_running ()
48
- ), "Loop should not be running"
49
- assert (
50
- not original_thread or not original_thread .is_alive ()
51
- ), "Thread should not be running"
46
+ # Ensure no loop/thread is running from a previous misbehaving test or setup
47
+ assert original_loop is None or not original_loop .is_running ()
48
+ assert original_thread is None or not original_thread .is_alive ()
52
49
53
50
ToolboxSyncClient ._ToolboxSyncClient__loop = None
54
51
ToolboxSyncClient ._ToolboxSyncClient__thread = None
@@ -62,16 +59,12 @@ def sync_client_environment():
62
59
if test_loop and test_loop .is_running ():
63
60
test_loop .call_soon_threadsafe (test_loop .stop )
64
61
if test_thread and test_thread .is_alive ():
65
- test_thread .join (timeout = 2 )
62
+ test_thread .join (timeout = 5 )
66
63
67
64
# Explicitly set to None to ensure a clean state for the next fixture use/test.
68
65
ToolboxSyncClient ._ToolboxSyncClient__loop = None
69
66
ToolboxSyncClient ._ToolboxSyncClient__thread = None
70
67
71
- # Restoring original_loop/thread could be risky if they were from a
72
- # non-test setup or a previous misbehaving test. For robust test-to-test
73
- # isolation, ensuring these are None after cleanup is generally preferred.
74
-
75
68
76
69
@pytest .fixture
77
70
def sync_client (sync_client_environment , request ):
@@ -185,70 +178,7 @@ def mock_tool_invoke(
185
178
aio_resp .post (url , payload = response_payload , status = status , callback = callback )
186
179
187
180
188
- # --- Tests for ToolboxSyncClient ---
189
-
190
-
191
- def test_sync_client_creation_in_isolated_env (sync_client ):
192
- """Tests that a client is initialized correctly by the sync_client fixture."""
193
- assert sync_client ._ToolboxSyncClient__loop is not None , "Loop should be created"
194
- assert (
195
- sync_client ._ToolboxSyncClient__thread is not None
196
- ), "Thread should be created"
197
- assert sync_client ._ToolboxSyncClient__thread .is_alive (), "Thread should be running"
198
- assert isinstance (
199
- sync_client ._ToolboxSyncClient__async_client , ToolboxClient
200
- ), "Async client should be ToolboxClient instance"
201
-
202
-
203
- @pytest .mark .usefixtures ("sync_client_environment" )
204
- def test_sync_client_close_method ():
205
- """
206
- Tests the close() method of ToolboxSyncClient when manually created.
207
- The sync_client_environment ensures loop/thread cleanup.
208
- """
209
- mock_async_client_instance = AsyncMock (spec = ToolboxClient )
210
- mock_async_client_instance .close = AsyncMock (return_value = None )
211
-
212
- with patch (
213
- "toolbox_core.sync_client.ToolboxClient" ,
214
- return_value = mock_async_client_instance ,
215
- ) as MockedAsyncClientConst :
216
- # Manually create client; sync_client_environment handles loop setup/teardown.
217
- client = ToolboxSyncClient (TEST_BASE_URL )
218
- MockedAsyncClientConst .assert_called_once_with (
219
- TEST_BASE_URL , client_headers = None
220
- )
221
-
222
- client .close () # This call closes the async_client's session.
223
- mock_async_client_instance .close .assert_awaited_once ()
224
- # The sync_client_environment fixture handles stopping the loop/thread.
225
-
226
-
227
- @pytest .mark .usefixtures ("sync_client_environment" )
228
- def test_sync_client_context_manager (aioresponses , tool_schema_minimal ):
229
- """
230
- Tests the context manager (__enter__ and __exit__) functionality.
231
- The sync_client_environment ensures loop/thread cleanup.
232
- """
233
- with patch .object (
234
- ToolboxSyncClient , "close" , wraps = ToolboxSyncClient .close , autospec = True
235
- ) as mock_close_method :
236
- with ToolboxSyncClient (TEST_BASE_URL ) as client : # Manually creating client
237
- assert isinstance (client , ToolboxSyncClient )
238
- mock_tool_load (aioresponses , "dummy_tool_ctx" , tool_schema_minimal )
239
- client .load_tool ("dummy_tool_ctx" )
240
- mock_close_method .assert_called_once ()
241
-
242
- with patch .object (
243
- ToolboxSyncClient , "close" , wraps = ToolboxSyncClient .close , autospec = True
244
- ) as mock_close_method_exc :
245
- with pytest .raises (ValueError , match = "Test exception" ):
246
- with ToolboxSyncClient (
247
- TEST_BASE_URL
248
- ) as client_exc : # Manually creating client
249
- raise ValueError ("Test exception" )
250
- mock_close_method_exc .assert_called_once ()
251
-
181
+ # --- Tests for General ToolboxSyncClient Functionality ---
252
182
253
183
def test_sync_load_tool_success (aioresponses , test_tool_str_schema , sync_client ):
254
184
TOOL_NAME = "test_tool_sync_1"
@@ -338,120 +268,154 @@ def test_sync_load_tool_not_found_in_manifest(
338
268
)
339
269
340
270
341
- def test_sync_add_headers_success (aioresponses , test_tool_str_schema , sync_client ):
342
- tool_name = "tool_after_add_headers_sync"
343
- manifest = ManifestSchema (
344
- serverVersion = "0.0.0" , tools = {tool_name : test_tool_str_schema }
345
- )
346
- expected_payload = {"result" : "added_sync_ok" }
347
- headers_to_add = {"X-Custom-SyncHeader" : "sync_value" }
348
-
349
- def get_callback (url , ** kwargs ):
350
- # The sync_client might have default headers. Check ours are present.
351
- assert kwargs .get ("headers" ) is not None
352
- for key , value in headers_to_add .items ():
353
- assert kwargs ["headers" ].get (key ) == value
354
- return CallbackResult (status = 200 , payload = manifest .model_dump ())
355
-
356
- aioresponses .get (f"{ TEST_BASE_URL } /api/tool/{ tool_name } " , callback = get_callback )
357
-
358
- def post_callback (url , ** kwargs ):
359
- assert kwargs .get ("headers" ) is not None
360
- for key , value in headers_to_add .items ():
361
- assert kwargs ["headers" ].get (key ) == value
362
- return CallbackResult (status = 200 , payload = expected_payload )
363
-
364
- aioresponses .post (
365
- f"{ TEST_BASE_URL } /api/tool/{ tool_name } /invoke" , callback = post_callback
366
- )
271
+ class TestSyncClientLifecycle :
272
+ """Tests for ToolboxSyncClient's specific lifecycle and internal management."""
273
+
274
+ def test_sync_client_creation_in_isolated_env (self , sync_client ):
275
+ """Tests that a client is initialized correctly by the sync_client fixture."""
276
+ assert sync_client ._ToolboxSyncClient__loop is not None , "Loop should be created"
277
+ assert (
278
+ sync_client ._ToolboxSyncClient__thread is not None
279
+ ), "Thread should be created"
280
+ assert sync_client ._ToolboxSyncClient__thread .is_alive (), "Thread should be running"
281
+ assert isinstance (
282
+ sync_client ._ToolboxSyncClient__async_client , ToolboxClient
283
+ ), "Async client should be ToolboxClient instance"
284
+
285
+ @pytest .mark .usefixtures ("sync_client_environment" )
286
+ def test_sync_client_close_method (self ):
287
+ """
288
+ Tests the close() method of ToolboxSyncClient when manually created.
289
+ The sync_client_environment ensures loop/thread cleanup.
290
+ """
291
+ mock_async_client_instance = AsyncMock (spec = ToolboxClient )
292
+ # AsyncMock methods are already AsyncMocks
293
+ # mock_async_client_instance.close = AsyncMock(return_value=None)
294
+
295
+ with patch (
296
+ "toolbox_core.sync_client.ToolboxClient" ,
297
+ return_value = mock_async_client_instance ,
298
+ ) as MockedAsyncClientConst :
299
+ client = ToolboxSyncClient (TEST_BASE_URL )
300
+ # The sync client passes its internal loop to the async client.
301
+ MockedAsyncClientConst .assert_called_once_with (
302
+ TEST_BASE_URL , client_headers = None
303
+ )
367
304
368
- sync_client .add_headers (headers_to_add )
369
- tool = sync_client .load_tool (tool_name )
370
- result = tool (param1 = "test" )
371
- assert result == expected_payload ["result" ]
305
+ client .close () # This call closes the async_client's session.
306
+ mock_async_client_instance .close .assert_awaited_once ()
307
+ # The sync_client_environment fixture handles stopping the loop/thread.
308
+
309
+ @pytest .mark .usefixtures ("sync_client_environment" )
310
+ def test_sync_client_context_manager (self , aioresponses , tool_schema_minimal ):
311
+ """
312
+ Tests the context manager (__enter__ and __exit__) functionality.
313
+ The sync_client_environment ensures loop/thread cleanup.
314
+ """
315
+ with patch .object (
316
+ ToolboxSyncClient , "close" , wraps = ToolboxSyncClient .close , autospec = True
317
+ ) as mock_close_method :
318
+ with ToolboxSyncClient (TEST_BASE_URL ) as client :
319
+ assert isinstance (client , ToolboxSyncClient )
320
+ mock_tool_load (aioresponses , "dummy_tool_ctx" , tool_schema_minimal )
321
+ client .load_tool ("dummy_tool_ctx" )
322
+ mock_close_method .assert_called_once ()
323
+
324
+ with patch .object (
325
+ ToolboxSyncClient , "close" , wraps = ToolboxSyncClient .close , autospec = True
326
+ ) as mock_close_method_exc :
327
+ with pytest .raises (ValueError , match = "Test exception" ):
328
+ with ToolboxSyncClient (
329
+ TEST_BASE_URL
330
+ ) as client_exc :
331
+ raise ValueError ("Test exception" )
332
+ mock_close_method_exc .assert_called_once ()
333
+
334
+ @pytest .mark .usefixtures ("sync_client_environment" )
335
+ def test_load_tool_raises_if_loop_or_thread_none (self ):
336
+ """
337
+ Tests that load_tool and load_toolset raise ValueError if the class-level
338
+ event loop or thread is None. sync_client_environment ensures a clean
339
+ slate before this test, and client creation will set up the loop/thread.
340
+ """
341
+ client = ToolboxSyncClient (TEST_BASE_URL ) # Loop/thread are started here.
342
+
343
+ original_class_loop = ToolboxSyncClient ._ToolboxSyncClient__loop
344
+ original_class_thread = ToolboxSyncClient ._ToolboxSyncClient__thread
345
+ assert (
346
+ original_class_loop is not None
347
+ ), "Loop should have been created by client init"
348
+ assert (
349
+ original_class_thread is not None
350
+ ), "Thread should have been created by client init"
351
+
352
+ # Manually break the class's loop to trigger the error condition in load_tool
353
+ ToolboxSyncClient ._ToolboxSyncClient__loop = None
354
+ with pytest .raises (ValueError , match = "Background loop or thread cannot be None." ):
355
+ client .load_tool ("any_tool_should_fail" )
356
+ ToolboxSyncClient ._ToolboxSyncClient__loop = (
357
+ original_class_loop # Restore for next check
358
+ )
372
359
360
+ ToolboxSyncClient ._ToolboxSyncClient__thread = None
361
+ with pytest .raises (ValueError , match = "Background loop or thread cannot be None." ):
362
+ client .load_toolset ("any_toolset_should_fail" )
363
+ ToolboxSyncClient ._ToolboxSyncClient__thread = original_class_thread # Restore
373
364
374
- @pytest .mark .usefixtures ("sync_client_environment" )
375
- def test_sync_add_headers_duplicate_fail ():
376
- """
377
- Tests that adding a duplicate header via add_headers raises ValueError.
378
- Manually create client to control initial headers.
379
- """
380
- initial_headers = {"X-Initial-Header" : "initial_value" }
381
- mock_async_client = AsyncMock (spec = ToolboxClient )
382
-
383
- # This mock simulates the behavior of the underlying async client's add_headers
384
- async def mock_add_headers_async_error (headers_to_add ):
385
- # Simulate error if header already exists in the "async client's current headers"
386
- if (
387
- "X-Initial-Header" in headers_to_add
388
- and hasattr (mock_async_client , "_current_headers" )
389
- and "X-Initial-Header" in mock_async_client ._current_headers
390
- ):
391
- raise ValueError ("Client header(s) `X-Initial-Header` already registered" )
365
+ client .close () # Clean up manually created client
366
+ # sync_client_environment will handle the final cleanup of original_class_loop/thread.
392
367
393
- mock_async_client .add_headers = (
394
- mock_add_headers_async_error # Assign as a coroutine
395
- )
396
368
397
- # Patch ToolboxClient constructor to inject initial_headers into the mock async_client state
398
- def side_effect_constructor (base_url , client_headers = None ):
399
- # Store the initial headers on the mock_async_client instance for the test
400
- mock_async_client ._current_headers = (
401
- client_headers .copy () if client_headers else {}
402
- )
403
- return mock_async_client
404
-
405
- with patch (
406
- "toolbox_core.sync_client.ToolboxClient" , side_effect = side_effect_constructor
407
- ) as MockedAsyncClientConst :
408
- # Client is created with initial_headers, which are passed to the (mocked) ToolboxClient
409
- client = ToolboxSyncClient (TEST_BASE_URL , client_headers = initial_headers )
410
- MockedAsyncClientConst .assert_called_with (
411
- TEST_BASE_URL , client_headers = initial_headers
369
+ class TestSyncClientHeaders :
370
+ """Additive tests for client header functionality specific to ToolboxSyncClient if any,
371
+ or counterparts to async client header tests."""
372
+
373
+ def test_sync_add_headers_success (self , aioresponses , test_tool_str_schema , sync_client ):
374
+ tool_name = "tool_after_add_headers_sync"
375
+ manifest = ManifestSchema (
376
+ serverVersion = "0.0.0" , tools = {tool_name : test_tool_str_schema }
412
377
)
378
+ expected_payload = {"result" : "added_sync_ok" }
379
+ headers_to_add = {"X-Custom-SyncHeader" : "sync_value" }
413
380
414
- with pytest . raises (
415
- ValueError ,
416
- match = "Client header \\ (s \\ ) `X-Initial-Header` already registered" ,
417
- ):
418
- # This call to client.add_headers will internally call mock_async_client.add_headers
419
- client . add_headers ({ "X-Initial-Header" : "another_value" } )
381
+ def get_callback ( url , ** kwargs ):
382
+ # The sync_client might have default headers. Check ours are present.
383
+ assert kwargs . get ( "headers" ) is not None
384
+ for key , value in headers_to_add . items ( ):
385
+ assert kwargs [ "headers" ]. get ( key ) == value
386
+ return CallbackResult ( status = 200 , payload = manifest . model_dump () )
420
387
388
+ aioresponses .get (f"{ TEST_BASE_URL } /api/tool/{ tool_name } " , callback = get_callback )
421
389
422
- @pytest .mark .usefixtures ("sync_client_environment" )
423
- def test_load_tool_raises_if_loop_or_thread_none ():
424
- """
425
- Tests that load_tool and load_toolset raise ValueError if the class-level
426
- event loop or thread is None. sync_client_environment ensures a clean
427
- slate before this test, and client creation will set up the loop/thread.
428
- """
429
- client = ToolboxSyncClient (TEST_BASE_URL ) # Loop/thread are started here.
430
-
431
- original_class_loop = ToolboxSyncClient ._ToolboxSyncClient__loop
432
- original_class_thread = ToolboxSyncClient ._ToolboxSyncClient__thread
433
- assert (
434
- original_class_loop is not None
435
- ), "Loop should have been created by client init"
436
- assert (
437
- original_class_thread is not None
438
- ), "Thread should have been created by client init"
439
-
440
- # Manually break the class's loop to trigger the error condition in load_tool
441
- ToolboxSyncClient ._ToolboxSyncClient__loop = None
442
- with pytest .raises (ValueError , match = "Background loop or thread cannot be None." ):
443
- client .load_tool ("any_tool_should_fail" )
444
- ToolboxSyncClient ._ToolboxSyncClient__loop = (
445
- original_class_loop # Restore for next check
446
- )
390
+ def post_callback (url , ** kwargs ):
391
+ assert kwargs .get ("headers" ) is not None
392
+ for key , value in headers_to_add .items ():
393
+ assert kwargs ["headers" ].get (key ) == value
394
+ return CallbackResult (status = 200 , payload = expected_payload )
447
395
448
- ToolboxSyncClient ._ToolboxSyncClient__thread = None
449
- with pytest .raises (ValueError , match = "Background loop or thread cannot be None." ):
450
- client .load_toolset ("any_toolset_should_fail" )
451
- ToolboxSyncClient ._ToolboxSyncClient__thread = original_class_thread # Restore
396
+ aioresponses .post (
397
+ f"{ TEST_BASE_URL } /api/tool/{ tool_name } /invoke" , callback = post_callback
398
+ )
399
+
400
+ sync_client .add_headers (headers_to_add )
401
+ tool = sync_client .load_tool (tool_name )
402
+ result = tool (param1 = "test" )
403
+ assert result == expected_payload ["result" ]
404
+
405
+ @pytest .mark .usefixtures ("sync_client_environment" )
406
+ def test_sync_add_headers_duplicate_fail (self ):
407
+ """
408
+ Tests that adding a duplicate header via add_headers raises ValueError.
409
+ Manually create client to control initial headers.
410
+ """
411
+ initial_headers = {"X-Initial-Header" : "initial_value" }
452
412
453
- client .close ()
454
- # sync_client_environment will handle the final cleanup of original_class_loop/thread.
413
+ with ToolboxSyncClient (TEST_BASE_URL , client_headers = initial_headers ) as client :
414
+ with pytest .raises (
415
+ ValueError ,
416
+ match = "Client header\\ (s\\ ) `X-Initial-Header` already registered" ,
417
+ ):
418
+ client .add_headers ({"X-Initial-Header" : "another_value" })
455
419
456
420
457
421
class TestSyncAuth :
@@ -550,7 +514,7 @@ def test_auth_with_load_tool_fail_no_token(
550
514
aioresponses .post (
551
515
f"{ TEST_BASE_URL } /api/tool/{ tool_name_auth } /invoke" ,
552
516
payload = {"error" : "Missing token" },
553
- status = 400 ,
517
+ status = 401 ,
554
518
)
555
519
556
520
tool = sync_client .load_tool (tool_name_auth )
0 commit comments