1616
1717from typing import Optional
1818from unittest .mock import AsyncMock
19- from unittest .mock import MagicMock
2019from unittest .mock import Mock
2120from unittest .mock import patch
2221
2827from google .adk .auth .auth_credential import AuthCredential
2928from google .adk .auth .auth_credential import AuthCredentialTypes
3029from google .adk .auth .auth_credential import OAuth2Auth
31- from google .adk .auth .auth_preprocessor import TOOLSET_AUTH_CREDENTIAL_ID_PREFIX
3230from google .adk .auth .auth_tool import AuthConfig
33- from google .adk .auth .auth_tool import AuthToolArguments
3431from google .adk .flows .llm_flows .base_llm_flow import _resolve_toolset_auth
35- from google .adk .flows .llm_flows .base_llm_flow import BaseLlmFlow
36- from google .adk .flows .llm_flows .base_llm_flow import TOOLSET_AUTH_CREDENTIAL_ID_PREFIX as FLOW_PREFIX
3732from google .adk .flows .llm_flows .functions import build_auth_request_event
3833from google .adk .flows .llm_flows .functions import REQUEST_EUC_FUNCTION_CALL_NAME
3934from google .adk .tools .base_tool import BaseTool
@@ -85,15 +80,6 @@ def create_oauth2_auth_config() -> AuthConfig:
8580 )
8681
8782
88- class TestToolsetAuthPrefixConstant :
89- """Test that prefix constants are consistent."""
90-
91- def test_prefix_constants_match (self ):
92- """Ensure auth_preprocessor and base_llm_flow use the same prefix."""
93- assert TOOLSET_AUTH_CREDENTIAL_ID_PREFIX == FLOW_PREFIX
94- assert TOOLSET_AUTH_CREDENTIAL_ID_PREFIX == "_adk_toolset_auth_"
95-
96-
9783class TestResolveToolsetAuth :
9884 """Tests for _resolve_toolset_auth method in BaseLlmFlow."""
9985
@@ -121,19 +107,12 @@ def mock_agent(self):
121107 return agent
122108
123109 @pytest .mark .asyncio
124- async def test_no_tools_returns_no_events (
125- self , mock_invocation_context , mock_agent
126- ):
127- """Test that no events are yielded when agent has no tools."""
110+ async def test_no_tools_completes (self , mock_invocation_context , mock_agent ):
111+ """Test that resolve completes without side effects when agent has no tools."""
128112 mock_agent .tools = []
129113
130- events = []
131- async for event in _resolve_toolset_auth (
132- mock_invocation_context , mock_agent
133- ):
134- events .append (event )
114+ await _resolve_toolset_auth (mock_invocation_context , mock_agent )
135115
136- assert len (events ) == 0
137116 assert mock_invocation_context .end_invocation is False
138117
139118 @pytest .mark .asyncio
@@ -144,13 +123,8 @@ async def test_toolset_without_auth_config_skipped(
144123 toolset = MockToolset (auth_config = None )
145124 mock_agent .tools = [toolset ]
146125
147- events = []
148- async for event in _resolve_toolset_auth (
149- mock_invocation_context , mock_agent
150- ):
151- events .append (event )
126+ await _resolve_toolset_auth (mock_invocation_context , mock_agent )
152127
153- assert len (events ) == 0
154128 assert mock_invocation_context .end_invocation is False
155129
156130 @pytest .mark .asyncio
@@ -162,7 +136,6 @@ async def test_toolset_with_credential_available_populates_config(
162136 toolset = MockToolset (auth_config = auth_config )
163137 mock_agent .tools = [toolset ]
164138
165- # Mock CredentialManager to return a credential
166139 mock_credential = AuthCredential (
167140 auth_type = AuthCredentialTypes .OAUTH2 ,
168141 oauth2 = OAuth2Auth (access_token = "test-token" ),
@@ -175,23 +148,21 @@ async def test_toolset_with_credential_available_populates_config(
175148 mock_manager .get_auth_credential = AsyncMock (return_value = mock_credential )
176149 MockCredentialManager .return_value = mock_manager
177150
178- events = []
179- async for event in _resolve_toolset_auth (
180- mock_invocation_context , mock_agent
181- ):
182- events .append (event )
151+ await _resolve_toolset_auth (mock_invocation_context , mock_agent )
183152
184- # No auth request events - credential was available
185- assert len (events ) == 0
186153 assert mock_invocation_context .end_invocation is False
187- # Credential should be populated in auth_config
188154 assert auth_config .exchanged_auth_credential == mock_credential
189155
190156 @pytest .mark .asyncio
191- async def test_toolset_without_credential_yields_auth_event (
157+ async def test_toolset_without_credential_defers_auth (
192158 self , mock_invocation_context , mock_agent
193159 ):
194- """Test that auth request event is yielded when credential not available."""
160+ """Test that auth is deferred when credential is not available.
161+
162+ When no credential is found, _resolve_toolset_auth should not interrupt
163+ the invocation. Auth will be handled on demand by ToolAuthHandler when
164+ a tool is actually invoked.
165+ """
195166 auth_config = create_oauth2_auth_config ()
196167 toolset = MockToolset (auth_config = auth_config )
197168 mock_agent .tools = [toolset ]
@@ -203,37 +174,16 @@ async def test_toolset_without_credential_yields_auth_event(
203174 mock_manager .get_auth_credential = AsyncMock (return_value = None )
204175 MockCredentialManager .return_value = mock_manager
205176
206- events = []
207- async for event in _resolve_toolset_auth (
208- mock_invocation_context , mock_agent
209- ):
210- events .append (event )
211-
212- # Should yield one auth request event
213- assert len (events ) == 1
214- assert mock_invocation_context .end_invocation is True
177+ await _resolve_toolset_auth (mock_invocation_context , mock_agent )
215178
216- # Check event structure
217- event = events [0 ]
218- assert event .invocation_id == "test-invocation-id"
219- assert event .author == "test-agent"
220- assert event .content is not None
221- assert len (event .content .parts ) == 1
222-
223- # Check function call
224- fc = event .content .parts [0 ].function_call
225- assert fc .name == REQUEST_EUC_FUNCTION_CALL_NAME
226- # The args use camelCase aliases from the pydantic model
227- assert fc .args ["functionCallId" ].startswith (
228- TOOLSET_AUTH_CREDENTIAL_ID_PREFIX
229- )
230- assert "MockToolset" in fc .args ["functionCallId" ]
179+ assert mock_invocation_context .end_invocation is False
180+ assert auth_config .exchanged_auth_credential is None
231181
232182 @pytest .mark .asyncio
233- async def test_multiple_toolsets_needing_auth (
183+ async def test_multiple_toolsets_without_credentials_defers_auth (
234184 self , mock_invocation_context , mock_agent
235185 ):
236- """Test that multiple toolsets needing auth yield multiple function calls ."""
186+ """Test that multiple toolsets without credentials do not interrupt ."""
237187 auth_config1 = create_oauth2_auth_config ()
238188 auth_config2 = create_oauth2_auth_config ()
239189 toolset1 = MockToolset (auth_config = auth_config1 )
@@ -247,17 +197,51 @@ async def test_multiple_toolsets_needing_auth(
247197 mock_manager .get_auth_credential = AsyncMock (return_value = None )
248198 MockCredentialManager .return_value = mock_manager
249199
250- events = []
251- async for event in _resolve_toolset_auth (
252- mock_invocation_context , mock_agent
253- ):
254- events .append (event )
255-
256- # Should yield one event with multiple function calls
257- # But since both toolsets have same class name, they'll have same ID
258- # and only one will be in pending_auth_requests (dict overwrites)
259- assert len (events ) == 1
260- assert mock_invocation_context .end_invocation is True
200+ await _resolve_toolset_auth (mock_invocation_context , mock_agent )
201+
202+ assert mock_invocation_context .end_invocation is False
203+
204+ @pytest .mark .asyncio
205+ async def test_mixed_toolsets_populates_available_credentials (
206+ self , mock_invocation_context , mock_agent
207+ ):
208+ """Test that credentials are populated when available, without interrupt.
209+
210+ When one toolset has credentials and another does not, the available
211+ credential should be populated while the missing one is deferred.
212+ """
213+ auth_config_with_cred = create_oauth2_auth_config ()
214+ auth_config_without_cred = create_oauth2_auth_config ()
215+ toolset_with_cred = MockToolset (auth_config = auth_config_with_cred )
216+ toolset_without_cred = MockToolset (auth_config = auth_config_without_cred )
217+ mock_agent .tools = [toolset_with_cred , toolset_without_cred ]
218+
219+ mock_credential = AuthCredential (
220+ auth_type = AuthCredentialTypes .OAUTH2 ,
221+ oauth2 = OAuth2Auth (access_token = "test-token" ),
222+ )
223+
224+ call_count = 0
225+
226+ async def side_effect (* args , ** kwargs ):
227+ nonlocal call_count
228+ call_count += 1
229+ if call_count == 1 :
230+ return mock_credential
231+ return None
232+
233+ with patch (
234+ "google.adk.flows.llm_flows.base_llm_flow.CredentialManager"
235+ ) as MockCredentialManager :
236+ mock_manager = AsyncMock ()
237+ mock_manager .get_auth_credential = AsyncMock (side_effect = side_effect )
238+ MockCredentialManager .return_value = mock_manager
239+
240+ await _resolve_toolset_auth (mock_invocation_context , mock_agent )
241+
242+ assert mock_invocation_context .end_invocation is False
243+ assert auth_config_with_cred .exchanged_auth_credential == mock_credential
244+ assert auth_config_without_cred .exchanged_auth_credential is None
261245
262246
263247class TestAuthPreprocessorToolsetAuthSkip :
0 commit comments