@@ -127,3 +127,122 @@ async def test_bind_params(self):
127127 # unless we mock properly.
128128 assert new_tool ._core_tool == "new_core_tool"
129129 mock_core .bind_params .assert_called_with ({"a" : 1 })
130+ @pytest .mark .asyncio
131+ async def test_3lo_missing_client_secret (self ):
132+ # Test ValueError when client_id/secret missing
133+ core_tool = AsyncMock ()
134+ auth_config = CredentialConfig (type = CredentialType .USER_IDENTITY )
135+ # Missing client_id/secret
136+
137+ tool = ToolboxTool (core_tool , auth_config = auth_config )
138+ ctx = MagicMock () # Mock the context
139+
140+ with pytest .raises (ValueError , match = "USER_IDENTITY requires client_id and client_secret" ):
141+ await tool .run_async ({"arg" : "val" }, ctx )
142+
143+ @pytest .mark .asyncio
144+ async def test_3lo_request_credential_when_missing (self ):
145+ # Test that if creds are missing, request_credential is called and returns None
146+ core_tool = AsyncMock ()
147+ auth_config = CredentialConfig (
148+ type = CredentialType .USER_IDENTITY ,
149+ client_id = "cid" ,
150+ client_secret = "csec"
151+ )
152+
153+ tool = ToolboxTool (core_tool , auth_config = auth_config )
154+
155+ ctx = MagicMock ()
156+ # Mock get_auth_response returning None (no creds yet)
157+ ctx .get_auth_response .return_value = None
158+
159+ result = await tool .run_async ({}, ctx )
160+
161+ # Verify result is None (signal pause)
162+ assert result is None
163+ # Verify request_credential was called
164+ ctx .request_credential .assert_called_once ()
165+ # Verify core tool was NOT called
166+ core_tool .assert_not_called ()
167+
168+ @pytest .mark .asyncio
169+ async def test_3lo_uses_existing_credential (self ):
170+ # Test that if creds exist, they are used and injected
171+ core_tool = AsyncMock (return_value = "success" )
172+ auth_config = CredentialConfig (
173+ type = CredentialType .USER_IDENTITY ,
174+ client_id = "cid" ,
175+ client_secret = "csec"
176+ )
177+
178+ tool = ToolboxTool (core_tool , auth_config = auth_config )
179+
180+ ctx = MagicMock ()
181+ # Mock get_auth_response returning valid creds
182+ mock_creds = MagicMock ()
183+ mock_creds .oauth2 .access_token = "valid_token"
184+ ctx .get_auth_response .return_value = mock_creds
185+
186+ result = await tool .run_async ({}, ctx )
187+
188+ # Verify result is success
189+ assert result == "success"
190+ # Verify request_credential was NOT called
191+ ctx .request_credential .assert_not_called ()
192+ # Verify core tool WAS called
193+ core_tool .assert_called_once ()
194+
195+
196+ @pytest .mark .asyncio
197+ async def test_3lo_exception_reraise (self ):
198+ # Test that specific credential errors are re-raised
199+ core_tool = AsyncMock ()
200+ auth_config = CredentialConfig (
201+ type = CredentialType .USER_IDENTITY ,
202+ client_id = "cid" ,
203+ client_secret = "csec"
204+ )
205+ tool = ToolboxTool (core_tool , auth_config = auth_config )
206+ ctx = MagicMock ()
207+
208+ # Mock get_auth_response raising ValueError
209+ ctx .get_auth_response .side_effect = ValueError ("Invalid Credential" )
210+
211+ with pytest .raises (ValueError , match = "Invalid Credential" ):
212+ await tool .run_async ({}, ctx )
213+
214+ @pytest .mark .asyncio
215+ async def test_3lo_exception_fallback (self ):
216+ # Test that non-credential errors trigger fallback request
217+ core_tool = AsyncMock ()
218+ auth_config = CredentialConfig (
219+ type = CredentialType .USER_IDENTITY ,
220+ client_id = "cid" ,
221+ client_secret = "csec"
222+ )
223+ tool = ToolboxTool (core_tool , auth_config = auth_config )
224+ ctx = MagicMock ()
225+
226+ # Mock get_auth_response raising generic error
227+ ctx .get_auth_response .side_effect = RuntimeError ("Random failure" )
228+
229+ result = await tool .run_async ({}, ctx )
230+
231+ # Should catch RuntimeError, call request_credential, and return None
232+ assert result is None
233+ ctx .request_credential .assert_called_once ()
234+
235+ def test_init_defaults (self ):
236+ # Test initialization with minimal tool metadata
237+ class EmptyTool :
238+ pass
239+
240+ core_tool = EmptyTool ()
241+ args = {"core_tool" : core_tool }
242+
243+ # Directly instantiate or if strict typing prevents it, force it
244+ # ToolboxTool expects CoreToolboxTool which is a Protocol/Class.
245+ # But at runtime it just checks attributes.
246+ tool = ToolboxTool (core_tool )
247+ assert tool .name == "unknown_tool"
248+ assert tool .description == "No description provided."
0 commit comments