1
+ # Copyright 2025 Google LLC
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import pytest
16
+ from unittest .mock import AsyncMock , MagicMock
17
+ from inspect import Parameter , Signature
18
+ from typing import Any , Optional , Callable
19
+
20
+ from toolbox_core .tool import ToolboxTool
21
+
22
+ class TestToolboxTool :
23
+ @pytest .fixture
24
+ def mock_session (self ) -> MagicMock : # Added self
25
+ session = MagicMock ()
26
+ session .post = MagicMock ()
27
+ return session
28
+
29
+ @pytest .fixture
30
+ def tool_details (self ) -> dict :
31
+ base_url = "http://fake-toolbox.com"
32
+ tool_name = "test_tool"
33
+ params = [
34
+ Parameter ("arg1" , Parameter .POSITIONAL_OR_KEYWORD , annotation = str ),
35
+ Parameter ("opt_arg" , Parameter .POSITIONAL_OR_KEYWORD , default = 123 , annotation = Optional [int ]),
36
+ ]
37
+ return {
38
+ "base_url" : base_url ,
39
+ "name" : tool_name ,
40
+ "desc" : "A tool for testing." ,
41
+ "params" : params ,
42
+ "signature" : Signature (parameters = params , return_annotation = str ),
43
+ "expected_url" : f"{ base_url } /api/tool/{ tool_name } /invoke" ,
44
+ "annotations" : {"arg1" : str , "opt_arg" : Optional [int ]},
45
+ }
46
+
47
+ @pytest .fixture
48
+ def tool (self , mock_session : MagicMock , tool_details : dict ) -> ToolboxTool :
49
+ return ToolboxTool (
50
+ session = mock_session ,
51
+ base_url = tool_details ["base_url" ],
52
+ name = tool_details ["name" ],
53
+ desc = tool_details ["desc" ],
54
+ params = tool_details ["params" ],
55
+ )
56
+
57
+ @pytest .fixture
58
+ def configure_mock_response (self , mock_session : MagicMock ) -> Callable :
59
+ def _configure (json_data : Any , status : int = 200 ):
60
+ mock_resp = MagicMock ()
61
+ mock_resp .status = status
62
+ mock_resp .json = AsyncMock (return_value = json_data )
63
+ mock_resp .__aenter__ .return_value = mock_resp
64
+ mock_resp .__aexit__ .return_value = None
65
+ mock_session .post .return_value = mock_resp
66
+ return _configure
67
+
68
+ @pytest .mark .asyncio
69
+ async def test_initialization_and_introspection (self , tool : ToolboxTool , tool_details : dict ):
70
+ """Verify attributes are set correctly during initialization."""
71
+ assert tool .__name__ == tool_details ["name" ]
72
+ assert tool .__doc__ == tool_details ["desc" ]
73
+ assert tool ._ToolboxTool__url == tool_details ["expected_url" ]
74
+ assert tool ._ToolboxTool__session is tool ._ToolboxTool__session
75
+ assert tool .__signature__ == tool_details ["signature" ]
76
+ assert tool .__annotations__ == tool_details ["annotations" ]
77
+ # assert hasattr(tool, "__qualname__")
78
+
79
+ @pytest .mark .asyncio
80
+ async def test_call_success (
81
+ self ,
82
+ tool : ToolboxTool ,
83
+ mock_session : MagicMock ,
84
+ tool_details : dict ,
85
+ configure_mock_response : Callable
86
+ ):
87
+ expected_result = "Operation successful!"
88
+ configure_mock_response ({"result" : expected_result })
89
+
90
+ arg1_val = "test_value"
91
+ opt_arg_val = 456
92
+ result = await tool (arg1_val , opt_arg = opt_arg_val )
93
+
94
+ assert result == expected_result
95
+ mock_session .post .assert_called_once_with (
96
+ tool_details ["expected_url" ],
97
+ json = {"arg1" : arg1_val , "opt_arg" : opt_arg_val },
98
+ )
99
+ mock_session .post .return_value .__aenter__ .return_value .json .assert_awaited_once ()
100
+
101
+ @pytest .mark .asyncio
102
+ async def test_call_success_with_defaults (
103
+ self ,
104
+ tool : ToolboxTool ,
105
+ mock_session : MagicMock ,
106
+ tool_details : dict ,
107
+ configure_mock_response : Callable
108
+ ):
109
+ expected_result = "Default success!"
110
+ configure_mock_response ({"result" : expected_result })
111
+
112
+ arg1_val = "another_test"
113
+ default_opt_val = tool_details ["params" ][1 ].default
114
+ result = await tool (arg1_val )
115
+
116
+ assert result == expected_result
117
+ mock_session .post .assert_called_once_with (
118
+ tool_details ["expected_url" ],
119
+ json = {"arg1" : arg1_val , "opt_arg" : default_opt_val },
120
+ )
121
+ mock_session .post .return_value .__aenter__ .return_value .json .assert_awaited_once ()
122
+
123
+ @pytest .mark .asyncio
124
+ async def test_call_api_error (
125
+ self ,
126
+ tool : ToolboxTool ,
127
+ mock_session : MagicMock ,
128
+ tool_details : dict ,
129
+ configure_mock_response : Callable
130
+ ):
131
+ error_message = "Tool execution failed on server"
132
+ configure_mock_response ({"error" : error_message })
133
+ default_opt_val = tool_details ["params" ][1 ].default
134
+
135
+ with pytest .raises (Exception ) as exc_info :
136
+ await tool ("some_arg" )
137
+
138
+ assert str (exc_info .value ) == error_message
139
+ mock_session .post .assert_called_once_with (
140
+ tool_details ["expected_url" ],
141
+ json = {"arg1" : "some_arg" , "opt_arg" : default_opt_val },
142
+ )
143
+ mock_session .post .return_value .__aenter__ .return_value .json .assert_awaited_once ()
144
+
145
+ @pytest .mark .asyncio
146
+ async def test_call_missing_result_key (
147
+ self ,
148
+ tool : ToolboxTool ,
149
+ mock_session : MagicMock ,
150
+ tool_details : dict ,
151
+ configure_mock_response : Callable
152
+ ):
153
+ fallback_response = {"status" : "completed" , "details" : "some info" }
154
+ configure_mock_response (fallback_response )
155
+ default_opt_val = tool_details ["params" ][1 ].default
156
+
157
+ result = await tool ("value_for_arg1" )
158
+
159
+ assert result == fallback_response
160
+ mock_session .post .assert_called_once_with (
161
+ tool_details ["expected_url" ],
162
+ json = {"arg1" : "value_for_arg1" , "opt_arg" : default_opt_val },
163
+ )
164
+ mock_session .post .return_value .__aenter__ .return_value .json .assert_awaited_once ()
165
+
166
+ @pytest .mark .asyncio
167
+ async def test_call_invalid_arguments_type_error (
168
+ self ,
169
+ tool : ToolboxTool ,
170
+ mock_session : MagicMock
171
+ ):
172
+ with pytest .raises (TypeError ):
173
+ await tool ("val1" , 2 , 3 )
174
+
175
+ with pytest .raises (TypeError ):
176
+ await tool ("val1" , non_existent_arg = "bad" )
177
+
178
+ with pytest .raises (TypeError ):
179
+ await tool (opt_arg = 500 )
180
+
181
+ mock_session .post .assert_not_called ()
0 commit comments