Skip to content

Commit 05d512f

Browse files
committed
chore: Add additional test cases to cover tool invocation and better docstring validation.
1 parent 7038c1d commit 05d512f

File tree

1 file changed

+197
-1
lines changed

1 file changed

+197
-1
lines changed

packages/toolbox-core/tests/test_tools.py

Lines changed: 197 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,115 @@
1313
# limitations under the License.
1414

1515

16-
from toolbox_core.tool import create_docstring
16+
from typing import AsyncGenerator
17+
18+
import pytest
19+
import pytest_asyncio
20+
from aiohttp import ClientSession
21+
from aioresponses import aioresponses
22+
from pydantic import ValidationError
23+
24+
from toolbox_core.protocol import ParameterSchema
25+
from toolbox_core.tool import ToolboxTool, create_docstring
26+
27+
TEST_BASE_URL = "http://toolbox.example.com"
28+
TEST_TOOL_NAME = "sample_tool"
29+
30+
31+
@pytest.fixture
32+
def sample_tool_params() -> list[ParameterSchema]:
33+
"""Parameters for the sample tool."""
34+
return [
35+
ParameterSchema(
36+
name="message", type="string", description="A message to process"
37+
),
38+
ParameterSchema(name="count", type="integer", description="A number"),
39+
]
40+
41+
42+
@pytest.fixture
43+
def sample_tool_description() -> str:
44+
"""Description for the sample tool."""
45+
return "A sample tool that processes a message and a count."
46+
47+
48+
@pytest_asyncio.fixture
49+
async def http_session() -> AsyncGenerator[ClientSession, None]:
50+
"""Provides an aiohttp ClientSession that is closed after the test."""
51+
async with ClientSession() as session:
52+
yield session
53+
54+
55+
def test_create_docstring_one_param_real_schema():
56+
"""
57+
Tests create_docstring with one real ParameterSchema instance.
58+
"""
59+
description = "This tool does one thing."
60+
params = [
61+
ParameterSchema(
62+
name="input_file", type="string", description="Path to the input file."
63+
)
64+
]
65+
66+
result_docstring = create_docstring(description, params)
67+
68+
expected_docstring = (
69+
"This tool does one thing.\n\n"
70+
"Args:\n"
71+
" input_file (str): Path to the input file."
72+
)
73+
74+
assert result_docstring == expected_docstring
75+
76+
77+
def test_create_docstring_multiple_params_real_schema():
78+
"""
79+
Tests create_docstring with multiple real ParameterSchema instances.
80+
"""
81+
description = "This tool does multiple things."
82+
params = [
83+
ParameterSchema(name="query", type="string", description="The search query."),
84+
ParameterSchema(
85+
name="max_results", type="integer", description="Maximum results to return."
86+
),
87+
ParameterSchema(
88+
name="verbose", type="boolean", description="Enable verbose output."
89+
),
90+
]
91+
92+
result_docstring = create_docstring(description, params)
93+
94+
expected_docstring = (
95+
"This tool does multiple things.\n\n"
96+
"Args:\n"
97+
" query (str): The search query.\n"
98+
" max_results (int): Maximum results to return.\n"
99+
" verbose (bool): Enable verbose output."
100+
)
101+
102+
assert result_docstring == expected_docstring
103+
104+
105+
def test_create_docstring_no_description_real_schema():
106+
"""
107+
Tests create_docstring with empty description and one real ParameterSchema.
108+
"""
109+
description = ""
110+
params = [
111+
ParameterSchema(
112+
name="config_id", type="string", description="The ID of the configuration."
113+
)
114+
]
115+
116+
result_docstring = create_docstring(description, params)
117+
118+
expected_docstring = (
119+
"\n\nArgs:\n" " config_id (str): The ID of the configuration."
120+
)
121+
122+
assert result_docstring == expected_docstring
123+
assert result_docstring.startswith("\n\nArgs:")
124+
assert "config_id (str): The ID of the configuration." in result_docstring
17125

18126

19127
def test_create_docstring_no_params():
@@ -27,3 +135,91 @@ def test_create_docstring_no_params():
27135

28136
assert result_docstring == description
29137
assert "\n\nArgs:" not in result_docstring
138+
139+
140+
@pytest.mark.asyncio
141+
async def test_tool_creation_callable_and_run(
142+
http_session: ClientSession,
143+
sample_tool_params: list[ParameterSchema],
144+
sample_tool_description: str,
145+
):
146+
"""
147+
Tests creating a ToolboxTool, checks callability, and simulates a run.
148+
"""
149+
tool_name = TEST_TOOL_NAME
150+
base_url = TEST_BASE_URL
151+
invoke_url = f"{base_url}/api/tool/{tool_name}/invoke"
152+
153+
input_args = {"message": "hello world", "count": 5}
154+
expected_payload = input_args.copy()
155+
mock_server_response_body = {"result": "Processed: hello world (5 times)"}
156+
expected_tool_result = mock_server_response_body["result"]
157+
158+
with aioresponses() as m:
159+
m.post(invoke_url, status=200, payload=mock_server_response_body)
160+
161+
tool_instance = ToolboxTool(
162+
session=http_session,
163+
base_url=base_url,
164+
name=tool_name,
165+
description=sample_tool_description,
166+
params=sample_tool_params,
167+
required_authn_params={},
168+
auth_service_token_getters={},
169+
bound_params={},
170+
)
171+
172+
assert callable(tool_instance), "ToolboxTool instance should be callable"
173+
174+
assert "message" in tool_instance.__signature__.parameters
175+
assert "count" in tool_instance.__signature__.parameters
176+
assert tool_instance.__signature__.parameters["message"].annotation == str
177+
assert tool_instance.__signature__.parameters["count"].annotation == int
178+
179+
actual_result = await tool_instance("hello world", 5)
180+
181+
assert actual_result == expected_tool_result
182+
183+
m.assert_called_once_with(
184+
invoke_url, method="POST", json=expected_payload, headers={}
185+
)
186+
187+
188+
@pytest.mark.asyncio
189+
async def test_tool_run_with_pydantic_validation_error(
190+
http_session: ClientSession,
191+
sample_tool_params: list[ParameterSchema],
192+
sample_tool_description: str,
193+
):
194+
"""
195+
Tests that calling the tool with incorrect argument types raises an error
196+
due to Pydantic validation *before* making an HTTP request.
197+
"""
198+
tool_name = TEST_TOOL_NAME
199+
base_url = TEST_BASE_URL
200+
invoke_url = f"{base_url}/api/tool/{tool_name}/invoke"
201+
202+
with aioresponses() as m:
203+
m.post(invoke_url, status=200, payload={"result": "Should not be called"})
204+
205+
tool_instance = ToolboxTool(
206+
session=http_session,
207+
base_url=base_url,
208+
name=tool_name,
209+
description=sample_tool_description,
210+
params=sample_tool_params,
211+
required_authn_params={},
212+
auth_service_token_getters={},
213+
bound_params={},
214+
)
215+
216+
assert callable(tool_instance)
217+
218+
with pytest.raises(ValidationError) as exc_info:
219+
await tool_instance(message="hello", count="not-a-number")
220+
221+
assert (
222+
"1 validation error for sample_tool\ncount\n Input should be a valid integer, unable to parse string as an integer [type=int_parsing, input_value='not-a-number', input_type=str]\n For further information visit https://errors.pydantic.dev/2.11/v/int_parsing"
223+
in str(exc_info.value)
224+
)
225+
m.assert_not_called()

0 commit comments

Comments
 (0)