13
13
# limitations under the License.
14
14
15
15
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 \n Args:\n " " config_id (str): The ID of the configuration."
120
+ )
121
+
122
+ assert result_docstring == expected_docstring
123
+ assert result_docstring .startswith ("\n \n Args:" )
124
+ assert "config_id (str): The ID of the configuration." in result_docstring
17
125
18
126
19
127
def test_create_docstring_no_params ():
@@ -27,3 +135,91 @@ def test_create_docstring_no_params():
27
135
28
136
assert result_docstring == description
29
137
assert "\n \n Args:" 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\n count\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