Skip to content

Commit cac3878

Browse files
committed
basic bound params works as expected.
1 parent 8e96af0 commit cac3878

File tree

1 file changed

+34
-260
lines changed

1 file changed

+34
-260
lines changed

packages/toolbox-core/tests/test_tool.py

Lines changed: 34 additions & 260 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,12 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15-
from inspect import Signature
15+
from inspect import Parameter, Signature
1616
from typing import Any, Callable, Optional
1717
from unittest.mock import AsyncMock, MagicMock
1818

1919
import pytest
2020

21-
from toolbox_core.protocol import ParameterSchema
2221
from toolbox_core.tool import ToolboxTool
2322

2423

@@ -30,26 +29,38 @@ def mock_session(self) -> MagicMock: # Added self
3029
return session
3130

3231
@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(
3936
"opt_arg",
4037
Parameter.POSITIONAL_OR_KEYWORD,
4138
default=123,
4239
annotation=Optional[int],
4340
),
41+
Parameter("req_kwarg", Parameter.KEYWORD_ONLY, annotation=bool), # Added back
4442
]
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+
4555
return {
4656
"base_url": base_url,
4757
"name": tool_name,
4858
"desc": "A tool for testing.",
4959
"params": params,
50-
"signature": Signature(parameters=params, return_annotation=str),
60+
"full_signature": full_signature,
5161
"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,
5364
}
5465

5566
@pytest.fixture
@@ -60,6 +71,7 @@ def tool(self, mock_session: MagicMock, tool_details: dict) -> ToolboxTool:
6071
name=tool_details["name"],
6172
desc=tool_details["desc"],
6273
params=tool_details["params"],
74+
bound_params=None,
6375
)
6476

6577
@pytest.fixture
@@ -82,9 +94,9 @@ async def test_initialization_and_introspection(
8294
assert tool.__name__ == tool_details["name"]
8395
assert tool.__doc__ == tool_details["desc"]
8496
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 == {}
88100
# assert hasattr(tool, "__qualname__")
89101

90102
@pytest.mark.asyncio
@@ -100,77 +112,13 @@ async def test_call_success(
100112

101113
arg1_val = "test_value"
102114
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)
126117

127118
assert result == expected_result
128119
mock_session.post.assert_called_once_with(
129120
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},
174122
)
175123
mock_session.post.return_value.__aenter__.return_value.json.assert_awaited_once()
176124

@@ -189,6 +137,7 @@ async def test_call_invalid_arguments_type_error(
189137

190138
mock_session.post.assert_not_called()
191139

140+
# Bound Params tests
192141
@pytest.fixture
193142
def bound_arg1_value(self) -> str:
194143
return "statically_bound_arg1"
@@ -197,17 +146,16 @@ def bound_arg1_value(self) -> str:
197146
def tool_with_bound_arg1(
198147
self, mock_session: MagicMock, tool_details: dict[str, Any], bound_arg1_value: str
199148
) -> ToolboxTool:
200-
"""Provides a tool with 'arg1' statically bound."""
201149
bound_params = {"arg1": bound_arg1_value}
202150
return ToolboxTool(
203151
session=mock_session,
204152
base_url=tool_details["base_url"],
205153
name=tool_details["name"],
206154
desc=tool_details["desc"],
207-
params=tool_details["params"],
155+
params=tool_details["params"], # Use corrected params
208156
bound_params=bound_params,
209157
)
210-
158+
@pytest.mark.asyncio
211159
async def test_bound_parameter_static_value_call(
212160
self,
213161
tool_with_bound_arg1: ToolboxTool,
@@ -221,190 +169,16 @@ async def test_bound_parameter_static_value_call(
221169
configure_mock_response(json_data={"result": expected_result})
222170

223171
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
226173

227-
# Call *without* providing arg1
174+
# Call *without* providing arg1, but provide the others
228175
result = await tool_with_bound_arg1(opt_arg=opt_arg_val, req_kwarg=req_kwarg_val)
229176

230177
assert result == expected_result
231178
mock_session.post.assert_called_once_with(
232179
tool_details["expected_url"],
233180
# 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},
288182
)
289183
mock_session.post.return_value.__aenter__.return_value.json.assert_awaited_once()
290184

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

Comments
 (0)