12
12
# See the License for the specific language governing permissions and
13
13
# limitations under the License.
14
14
15
- from inspect import Signature
15
+ from inspect import Parameter , Signature
16
16
from typing import Any , Callable , Optional
17
17
from unittest .mock import AsyncMock , MagicMock
18
18
19
19
import pytest
20
20
21
- from toolbox_core .protocol import ParameterSchema
22
21
from toolbox_core .tool import ToolboxTool
23
22
24
23
@@ -30,26 +29,38 @@ def mock_session(self) -> MagicMock: # Added self
30
29
return session
31
30
32
31
@pytest .fixture
33
- def tool_details (self ) -> dict :
34
- base_url = "http://fake-toolbox.com"
35
- tool_name = "test_tool"
36
- params = [
37
- ParameterSchema ("arg1" , Parameter .POSITIONAL_OR_KEYWORD , annotation = str ),
38
- ParameterSchema (
32
+ def tool_params (self ) -> list [Parameter ]:
33
+ return [
34
+ Parameter ("arg1" , Parameter .POSITIONAL_OR_KEYWORD , annotation = str ),
35
+ Parameter (
39
36
"opt_arg" ,
40
37
Parameter .POSITIONAL_OR_KEYWORD ,
41
38
default = 123 ,
42
39
annotation = Optional [int ],
43
40
),
41
+ Parameter ("req_kwarg" , Parameter .KEYWORD_ONLY , annotation = bool ), # Added back
44
42
]
43
+
44
+ @pytest .fixture
45
+ def tool_details (self , tool_params : list [Parameter ]) -> dict [str , Any ]:
46
+ """Provides common details for constructing the test tool."""
47
+ base_url = "http://fake-toolbox.com"
48
+ tool_name = "test_tool"
49
+ params = tool_params
50
+ full_signature = Signature (parameters = params , return_annotation = str )
51
+ public_signature = Signature (parameters = params , return_annotation = str )
52
+ full_annotations = {"arg1" : str , "opt_arg" : Optional [int ], "req_kwarg" : bool }
53
+ public_annotations = full_annotations .copy ()
54
+
45
55
return {
46
56
"base_url" : base_url ,
47
57
"name" : tool_name ,
48
58
"desc" : "A tool for testing." ,
49
59
"params" : params ,
50
- "signature " : Signature ( parameters = params , return_annotation = str ) ,
60
+ "full_signature " : full_signature ,
51
61
"expected_url" : f"{ base_url } /api/tool/{ tool_name } /invoke" ,
52
- "annotations" : {"arg1" : str , "opt_arg" : Optional [int ]},
62
+ "public_signature" : public_signature ,
63
+ "public_annotations" : public_annotations ,
53
64
}
54
65
55
66
@pytest .fixture
@@ -60,6 +71,7 @@ def tool(self, mock_session: MagicMock, tool_details: dict) -> ToolboxTool:
60
71
name = tool_details ["name" ],
61
72
desc = tool_details ["desc" ],
62
73
params = tool_details ["params" ],
74
+ bound_params = None ,
63
75
)
64
76
65
77
@pytest .fixture
@@ -82,9 +94,9 @@ async def test_initialization_and_introspection(
82
94
assert tool .__name__ == tool_details ["name" ]
83
95
assert tool .__doc__ == tool_details ["desc" ]
84
96
assert tool ._ToolboxTool__url == tool_details ["expected_url" ]
85
- assert tool ._ToolboxTool__session is tool . _ToolboxTool__session
86
- assert tool .__signature__ == tool_details ["signature " ]
87
- assert tool .__annotations__ == tool_details [ "annotations" ]
97
+ assert tool .__signature__ == tool_details [ "public_signature" ]
98
+ assert tool .__annotations__ == tool_details ["public_annotations " ]
99
+ assert tool ._ToolboxTool__bound_params == {}
88
100
# assert hasattr(tool, "__qualname__")
89
101
90
102
@pytest .mark .asyncio
@@ -100,77 +112,13 @@ async def test_call_success(
100
112
101
113
arg1_val = "test_value"
102
114
opt_arg_val = 456
103
- result = await tool (arg1_val , opt_arg = opt_arg_val )
104
-
105
- assert result == expected_result
106
- mock_session .post .assert_called_once_with (
107
- tool_details ["expected_url" ],
108
- json = {"arg1" : arg1_val , "opt_arg" : opt_arg_val },
109
- )
110
- mock_session .post .return_value .__aenter__ .return_value .json .assert_awaited_once ()
111
-
112
- @pytest .mark .asyncio
113
- async def test_call_success_with_defaults (
114
- self ,
115
- tool : ToolboxTool ,
116
- mock_session : MagicMock ,
117
- tool_details : dict ,
118
- configure_mock_response : Callable ,
119
- ):
120
- expected_result = "Default success!"
121
- configure_mock_response ({"result" : expected_result })
122
-
123
- arg1_val = "another_test"
124
- default_opt_val = tool_details ["params" ][1 ].default
125
- result = await tool (arg1_val )
115
+ req_kwarg_val = True
116
+ result = await tool (arg1_val , opt_arg = opt_arg_val , req_kwarg = req_kwarg_val )
126
117
127
118
assert result == expected_result
128
119
mock_session .post .assert_called_once_with (
129
120
tool_details ["expected_url" ],
130
- json = {"arg1" : arg1_val , "opt_arg" : default_opt_val },
131
- )
132
- mock_session .post .return_value .__aenter__ .return_value .json .assert_awaited_once ()
133
-
134
- @pytest .mark .asyncio
135
- async def test_call_api_error (
136
- self ,
137
- tool : ToolboxTool ,
138
- mock_session : MagicMock ,
139
- tool_details : dict ,
140
- configure_mock_response : Callable ,
141
- ):
142
- error_message = "Tool execution failed on server"
143
- configure_mock_response ({"error" : error_message })
144
- default_opt_val = tool_details ["params" ][1 ].default
145
-
146
- with pytest .raises (Exception ) as exc_info :
147
- await tool ("some_arg" )
148
-
149
- assert str (exc_info .value ) == error_message
150
- mock_session .post .assert_called_once_with (
151
- tool_details ["expected_url" ],
152
- json = {"arg1" : "some_arg" , "opt_arg" : default_opt_val },
153
- )
154
- mock_session .post .return_value .__aenter__ .return_value .json .assert_awaited_once ()
155
-
156
- @pytest .mark .asyncio
157
- async def test_call_missing_result_key (
158
- self ,
159
- tool : ToolboxTool ,
160
- mock_session : MagicMock ,
161
- tool_details : dict ,
162
- configure_mock_response : Callable ,
163
- ):
164
- fallback_response = {"status" : "completed" , "details" : "some info" }
165
- configure_mock_response (fallback_response )
166
- default_opt_val = tool_details ["params" ][1 ].default
167
-
168
- result = await tool ("value_for_arg1" )
169
-
170
- assert result == fallback_response
171
- mock_session .post .assert_called_once_with (
172
- tool_details ["expected_url" ],
173
- json = {"arg1" : "value_for_arg1" , "opt_arg" : default_opt_val },
121
+ payload = {"arg1" : arg1_val , "opt_arg" : opt_arg_val , "req_kwarg" : req_kwarg_val },
174
122
)
175
123
mock_session .post .return_value .__aenter__ .return_value .json .assert_awaited_once ()
176
124
@@ -189,6 +137,7 @@ async def test_call_invalid_arguments_type_error(
189
137
190
138
mock_session .post .assert_not_called ()
191
139
140
+ # Bound Params tests
192
141
@pytest .fixture
193
142
def bound_arg1_value (self ) -> str :
194
143
return "statically_bound_arg1"
@@ -197,17 +146,16 @@ def bound_arg1_value(self) -> str:
197
146
def tool_with_bound_arg1 (
198
147
self , mock_session : MagicMock , tool_details : dict [str , Any ], bound_arg1_value : str
199
148
) -> ToolboxTool :
200
- """Provides a tool with 'arg1' statically bound."""
201
149
bound_params = {"arg1" : bound_arg1_value }
202
150
return ToolboxTool (
203
151
session = mock_session ,
204
152
base_url = tool_details ["base_url" ],
205
153
name = tool_details ["name" ],
206
154
desc = tool_details ["desc" ],
207
- params = tool_details ["params" ],
155
+ params = tool_details ["params" ], # Use corrected params
208
156
bound_params = bound_params ,
209
157
)
210
-
158
+ @ pytest . mark . asyncio
211
159
async def test_bound_parameter_static_value_call (
212
160
self ,
213
161
tool_with_bound_arg1 : ToolboxTool ,
@@ -221,190 +169,16 @@ async def test_bound_parameter_static_value_call(
221
169
configure_mock_response (json_data = {"result" : expected_result })
222
170
223
171
opt_arg_val = 789
224
- req_kwarg_val = True
225
- default_opt_val = tool_details ["params" ][1 ].default # Not used here, but for clarity
172
+ req_kwarg_val = True # The only remaining required arg
226
173
227
- # Call *without* providing arg1
174
+ # Call *without* providing arg1, but provide the others
228
175
result = await tool_with_bound_arg1 (opt_arg = opt_arg_val , req_kwarg = req_kwarg_val )
229
176
230
177
assert result == expected_result
231
178
mock_session .post .assert_called_once_with (
232
179
tool_details ["expected_url" ],
233
180
# Payload should include the bound value for arg1
234
- json = {"arg1" : bound_arg1_value , "opt_arg" : opt_arg_val , "req_kwarg" : req_kwarg_val },
235
- )
236
- mock_session .post .return_value .__aenter__ .return_value .json .assert_awaited_once ()
237
-
238
- async def test_bound_parameter_static_value_introspection (
239
- self , tool_with_bound_arg1 : ToolboxTool , tool_details : dict [str , Any ]
240
- ):
241
- """Verify the public signature excludes the bound parameter 'arg1'."""
242
- assert "arg1" not in tool_with_bound_arg1 .__signature__ .parameters
243
- assert "arg1" not in tool_with_bound_arg1 .__annotations__
244
-
245
- # Check remaining parameters are present
246
- assert "opt_arg" in tool_with_bound_arg1 .__signature__ .parameters
247
- assert "req_kwarg" in tool_with_bound_arg1 .__signature__ .parameters
248
- assert tool_with_bound_arg1 .__signature__ .parameters ["opt_arg" ].annotation == Optional [int ]
249
- assert tool_with_bound_arg1 .__signature__ .parameters ["req_kwarg" ].annotation == bool
250
-
251
- async def test_bound_parameter_callable_value_call (
252
- self ,
253
- mock_session : MagicMock ,
254
- tool_details : dict [str , Any ],
255
- configure_mock_response : Callable ,
256
- ):
257
- """Test calling a tool with a parameter bound to a callable."""
258
- callable_value = "dynamic_value"
259
- callable_mock = MagicMock (return_value = callable_value )
260
- bound_params = {"arg1" : callable_mock }
261
-
262
- tool_bound_callable = ToolboxTool (
263
- session = mock_session ,
264
- base_url = tool_details ["base_url" ],
265
- name = tool_details ["name" ],
266
- desc = tool_details ["desc" ],
267
- params = tool_details ["params" ],
268
- bound_params = bound_params ,
269
- )
270
-
271
- expected_result = "Callable bound success!"
272
- configure_mock_response (json_data = {"result" : expected_result })
273
-
274
- opt_arg_val = 999
275
- req_kwarg_val = False
276
-
277
- # Call *without* providing arg1
278
- result = await tool_bound_callable (opt_arg = opt_arg_val , req_kwarg = req_kwarg_val )
279
-
280
- assert result == expected_result
281
- # Verify the callable was executed exactly once
282
- callable_mock .assert_called_once ()
283
-
284
- mock_session .post .assert_called_once_with (
285
- tool_details ["expected_url" ],
286
- # Payload should include the *result* of the callable
287
- json = {"arg1" : callable_value , "opt_arg" : opt_arg_val , "req_kwarg" : req_kwarg_val },
181
+ payload = {"arg1" : bound_arg1_value , "opt_arg" : opt_arg_val , "req_kwarg" : req_kwarg_val },
288
182
)
289
183
mock_session .post .return_value .__aenter__ .return_value .json .assert_awaited_once ()
290
184
291
- async def test_bound_parameter_callable_evaluation_error (
292
- self ,
293
- mock_session : MagicMock ,
294
- tool_details : dict [str , Any ],
295
- ):
296
- """Test that RuntimeError is raised if bound callable evaluation fails."""
297
- error_message = "Callable evaluation failed!"
298
-
299
- def failing_callable ():
300
- raise ValueError (error_message )
301
-
302
- bound_params = {"arg1" : failing_callable }
303
- tool_bound_failing = ToolboxTool (
304
- session = mock_session ,
305
- base_url = tool_details ["base_url" ],
306
- name = tool_details ["name" ],
307
- desc = tool_details ["desc" ],
308
- params = tool_details ["params" ],
309
- bound_params = bound_params ,
310
- )
311
-
312
- with pytest .raises (RuntimeError ) as exc_info :
313
- await tool_bound_failing (opt_arg = 1 , req_kwarg = True ) # Provide other args
314
-
315
- # Check that the original exception message is part of the RuntimeError
316
- assert error_message in str (exc_info .value )
317
- assert "Error evaluating argument 'arg1'" in str (exc_info .value )
318
-
319
- # Ensure the API call was *not* made
320
- mock_session .post .assert_not_called ()
321
-
322
- async def test_bound_parameter_conflict_error (
323
- self , tool_with_bound_arg1 : ToolboxTool , mock_session : MagicMock , bound_arg1_value : str
324
- ):
325
- """Test TypeError when providing an argument that is already bound."""
326
- conflicting_arg1_val = "call_time_value"
327
-
328
- with pytest .raises (TypeError ) as exc_info :
329
- # Attempt to provide 'arg1' again during the call
330
- await tool_with_bound_arg1 (arg1 = conflicting_arg1_val , req_kwarg = True )
331
-
332
- assert "Cannot provide value during call for already bound argument(s): arg1" in str (exc_info .value )
333
-
334
- # Ensure the API call was *not* made
335
- mock_session .post .assert_not_called ()
336
-
337
- async def test_bound_parameter_overrides_default (
338
- self ,
339
- mock_session : MagicMock ,
340
- tool_details : dict [str , Any ],
341
- configure_mock_response : Callable ,
342
- ):
343
- """Test that a bound value for a parameter with a default overrides the default."""
344
- bound_opt_arg_value = 999 # Different from the default of 123
345
- bound_params = {"opt_arg" : bound_opt_arg_value }
346
-
347
- tool_bound_default = ToolboxTool (
348
- session = mock_session ,
349
- base_url = tool_details ["base_url" ],
350
- name = tool_details ["name" ],
351
- desc = tool_details ["desc" ],
352
- params = tool_details ["params" ],
353
- bound_params = bound_params ,
354
- )
355
-
356
- expected_result = "Default override success!"
357
- configure_mock_response (json_data = {"result" : expected_result })
358
-
359
- arg1_val = "required_arg_val"
360
- req_kwarg_val = True
361
-
362
- # Call *without* providing opt_arg
363
- result = await tool_bound_default (arg1_val , req_kwarg = req_kwarg_val )
364
-
365
- assert result == expected_result
366
- mock_session .post .assert_called_once_with (
367
- tool_details ["expected_url" ],
368
- # Payload should include the bound value for opt_arg, not the default
369
- json = {"arg1" : arg1_val , "opt_arg" : bound_opt_arg_value , "req_kwarg" : req_kwarg_val },
370
- )
371
-
372
- async def test_multiple_bound_parameters (
373
- self ,
374
- mock_session : MagicMock ,
375
- tool_details : dict [str , Any ],
376
- configure_mock_response : Callable ,
377
- ):
378
- """Test binding multiple parameters."""
379
- bound_arg1 = "multi_bound_1"
380
- bound_opt_arg = 555
381
- bound_params = {
382
- "arg1" : bound_arg1 ,
383
- "opt_arg" : bound_opt_arg ,
384
- }
385
-
386
- tool_multi_bound = ToolboxTool (
387
- session = mock_session ,
388
- base_url = tool_details ["base_url" ],
389
- name = tool_details ["name" ],
390
- desc = tool_details ["desc" ],
391
- params = tool_details ["params" ],
392
- bound_params = bound_params ,
393
- )
394
-
395
- # Check introspection - only req_kwarg should remain
396
- assert list (tool_multi_bound .__signature__ .parameters .keys ()) == ["req_kwarg" ]
397
-
398
- expected_result = "Multi-bound success!"
399
- configure_mock_response (json_data = {"result" : expected_result })
400
-
401
- req_kwarg_val = False
402
- # Call providing only the remaining unbound argument
403
- result = await tool_multi_bound (req_kwarg = req_kwarg_val )
404
-
405
- assert result == expected_result
406
- mock_session .post .assert_called_once_with (
407
- tool_details ["expected_url" ],
408
- # Payload should include both bound values and the called value
409
- json = {"arg1" : bound_arg1 , "opt_arg" : bound_opt_arg , "req_kwarg" : req_kwarg_val },
410
- )
0 commit comments