# Copyright 2026 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from unittest.mock import Mock from google.adk.agents.base_agent import BaseAgent from google.adk.agents.base_agent import BaseAgentState from google.adk.agents.invocation_context import InvocationContext from google.adk.apps import ResumabilityConfig from google.adk.events.event import Event from google.adk.events.event_actions import EventActions from google.adk.sessions.base_session_service import BaseSessionService from google.adk.sessions.session import Session from google.genai.types import Content from google.genai.types import FunctionCall from google.genai.types import Part import pytest from .. import testing_utils class TestInvocationContext: """Test suite for InvocationContext.""" @pytest.fixture def mock_events(self): """Create mock events for testing.""" event1 = Mock(spec=Event) event1.invocation_id = 'inv_1' event1.branch = 'agent_1' event2 = Mock(spec=Event) event2.invocation_id = 'inv_1' event2.branch = 'agent_2' event3 = Mock(spec=Event) event3.invocation_id = 'inv_2' event3.branch = 'agent_1' event4 = Mock(spec=Event) event4.invocation_id = 'inv_2' event4.branch = 'agent_2' return [event1, event2, event3, event4] @pytest.fixture def mock_invocation_context(self, mock_events): """Create a mock invocation context for testing.""" ctx = InvocationContext( session_service=Mock(spec=BaseSessionService), agent=Mock(spec=BaseAgent), invocation_id='inv_1', branch='agent_1', session=Mock(spec=Session, events=mock_events), ) return ctx def test_get_events_returns_all_events_by_default( self, mock_invocation_context, mock_events ): """Tests that get_events returns all events when no filters are applied.""" events = mock_invocation_context._get_events() assert events == mock_events def test_get_events_filters_by_current_invocation( self, mock_invocation_context, mock_events ): """Tests that get_events correctly filters by the current invocation.""" event1, event2, _, _ = mock_events events = mock_invocation_context._get_events(current_invocation=True) assert events == [event1, event2] def test_get_events_filters_by_current_branch( self, mock_invocation_context, mock_events ): """Tests that get_events correctly filters by the current branch.""" event1, _, event3, _ = mock_events events = mock_invocation_context._get_events(current_branch=True) assert events == [event1, event3] def test_get_events_filters_by_invocation_and_branch( self, mock_invocation_context, mock_events ): """Tests that get_events filters by invocation and branch.""" event1, _, _, _ = mock_events events = mock_invocation_context._get_events( current_invocation=True, current_branch=True, ) assert events == [event1] def test_get_events_with_no_events_in_session(self, mock_invocation_context): """Tests get_events when the session has no events.""" mock_invocation_context.session.events = [] events = mock_invocation_context._get_events() assert not events def test_get_events_with_no_matching_events(self, mock_invocation_context): """Tests get_events when no events match the filters.""" mock_invocation_context.invocation_id = 'inv_3' mock_invocation_context.branch = 'branch_C' # Filter by invocation events = mock_invocation_context._get_events(current_invocation=True) assert not events # Filter by branch events = mock_invocation_context._get_events(current_branch=True) assert not events # Filter by both events = mock_invocation_context._get_events( current_invocation=True, current_branch=True, ) assert not events class TestInvocationContextWithAppResumablity: """Test suite for InvocationContext regarding app resumability.""" @pytest.fixture def long_running_function_call(self) -> FunctionCall: """A long running function call.""" return FunctionCall( id='tool_call_id_1', name='long_running_function_call', args={}, ) @pytest.fixture def event_to_pause(self, long_running_function_call) -> Event: """An event with a long running function call.""" return Event( invocation_id='inv_1', author='agent', content=testing_utils.ModelContent( [Part(function_call=long_running_function_call)] ), long_running_tool_ids=[long_running_function_call.id], ) def _create_test_invocation_context( self, resumability_config: ResumabilityConfig | None = None ) -> InvocationContext: """Create a mock invocation context for testing.""" ctx = InvocationContext( session_service=Mock(spec=BaseSessionService), agent=Mock(spec=BaseAgent), invocation_id='inv_1', session=Mock(spec=Session, events=[]), resumability_config=resumability_config, ) return ctx def test_should_pause_invocation_with_resumable_app(self, event_to_pause): """Tests should_pause_invocation with a resumable app.""" mock_invocation_context = self._create_test_invocation_context( ResumabilityConfig(is_resumable=True) ) assert mock_invocation_context.should_pause_invocation(event_to_pause) def test_should_pause_invocation_with_non_resumable_app(self, event_to_pause): """Tests should_pause_invocation pauses even without resumability.""" invocation_context = self._create_test_invocation_context( ResumabilityConfig(is_resumable=False) ) assert invocation_context.should_pause_invocation(event_to_pause) def test_should_not_pause_invocation_with_no_long_running_tool_ids( self, event_to_pause ): """Tests should_pause_invocation with no long running tools.""" invocation_context = self._create_test_invocation_context( ResumabilityConfig(is_resumable=True) ) nonpausable_event = event_to_pause.model_copy( update={'long_running_tool_ids': []} ) assert not invocation_context.should_pause_invocation(nonpausable_event) def test_should_not_pause_invocation_with_no_function_calls( self, event_to_pause ): """Tests should_pause_invocation with a non-model event.""" mock_invocation_context = self._create_test_invocation_context( ResumabilityConfig(is_resumable=True) ) nonpausable_event = event_to_pause.model_copy( update={'content': testing_utils.UserContent('test text part')} ) assert not mock_invocation_context.should_pause_invocation( nonpausable_event ) def test_should_not_pause_when_user_resumes_in_sub_branch( self, event_to_pause, long_running_function_call ): """We do not pause the invocation if a subsequent user event belongs to a sub-branch.""" # Arrange mock_invocation_context = self._create_test_invocation_context() user_event = Event( invocation_id='inv_1', author='user', branch=f'agent@{long_running_function_call.id}.child', ) mock_invocation_context.session.events = [event_to_pause, user_event] # Act should_pause = mock_invocation_context.should_pause_invocation( event_to_pause ) # Assert assert not should_pause def test_should_not_pause_when_user_resumes_in_deeply_nested_sub_branch( self, event_to_pause, long_running_function_call ): """We do not pause if the user resumes in a deeply nested sub-branch containing the tool call.""" # Arrange mock_invocation_context = self._create_test_invocation_context() user_event = Event( invocation_id='inv_1', author='user', branch=f'parent@other.child@{long_running_function_call.id}.grandchild', ) mock_invocation_context.session.events = [event_to_pause, user_event] # Act should_pause = mock_invocation_context.should_pause_invocation( event_to_pause ) # Assert assert not should_pause def test_should_pause_when_user_resumes_in_different_branch( self, event_to_pause ): """We still pause the invocation if the subsequent user event belongs to a different branch.""" # Arrange mock_invocation_context = self._create_test_invocation_context() user_event = Event( invocation_id='inv_1', author='user', branch='parent@different_id.child', ) mock_invocation_context.session.events = [event_to_pause, user_event] # Act should_pause = mock_invocation_context.should_pause_invocation( event_to_pause ) # Assert assert should_pause def test_is_resumable_true(self): """Tests that is_resumable is True when resumability is enabled.""" invocation_context = self._create_test_invocation_context( ResumabilityConfig(is_resumable=True) ) assert invocation_context.is_resumable def test_is_resumable_false(self): """Tests that is_resumable is False when resumability is disabled.""" invocation_context = self._create_test_invocation_context( ResumabilityConfig(is_resumable=False) ) assert not invocation_context.is_resumable def test_is_resumable_no_config(self): """Tests that is_resumable is False when no resumability config is set.""" invocation_context = self._create_test_invocation_context(None) assert not invocation_context.is_resumable def test_populate_invocation_agent_states_not_resumable(self): """Tests that populate_invocation_agent_states does nothing if not resumable.""" invocation_context = self._create_test_invocation_context( ResumabilityConfig(is_resumable=False) ) event = Event( invocation_id='inv_1', author='agent1', actions=EventActions(end_of_agent=True, agent_state=None), ) invocation_context.session.events = [event] invocation_context.populate_invocation_agent_states() assert not invocation_context.agent_states assert not invocation_context.end_of_agents def test_populate_invocation_agent_states_end_of_agent(self): """Tests that populate_invocation_agent_states handles end_of_agent.""" invocation_context = self._create_test_invocation_context( ResumabilityConfig(is_resumable=True) ) event = Event( invocation_id='inv_1', author='agent1', actions=EventActions(end_of_agent=True, agent_state=None), ) invocation_context.session.events = [event] invocation_context.populate_invocation_agent_states() assert not invocation_context.agent_states assert invocation_context.end_of_agents == {'agent1': True} def test_populate_invocation_agent_states_with_agent_state(self): """Tests that populate_invocation_agent_states handles agent_state.""" invocation_context = self._create_test_invocation_context( ResumabilityConfig(is_resumable=True) ) event = Event( invocation_id='inv_1', author='agent1', actions=EventActions( end_of_agent=False, agent_state=BaseAgentState().model_dump(mode='json'), ), ) invocation_context.session.events = [event] invocation_context.populate_invocation_agent_states() assert invocation_context.agent_states == {'agent1': {}} assert invocation_context.end_of_agents == {'agent1': False} def test_populate_invocation_agent_states_with_agent_state_and_end_of_agent( self, ): """Tests that populate_invocation_agent_states handles agent_state and end_of_agent.""" invocation_context = self._create_test_invocation_context( ResumabilityConfig(is_resumable=True) ) event = Event( invocation_id='inv_1', author='agent1', actions=EventActions( end_of_agent=True, agent_state=BaseAgentState().model_dump(mode='json'), ), ) invocation_context.session.events = [event] invocation_context.populate_invocation_agent_states() # When both agent_state and end_of_agent are set, agent_state should be # cleared, as end_of_agent is of a higher priority. assert not invocation_context.agent_states assert invocation_context.end_of_agents == {'agent1': True} def test_populate_invocation_agent_states_with_content_no_state(self): """Tests that populate_invocation_agent_states creates default state.""" invocation_context = self._create_test_invocation_context( ResumabilityConfig(is_resumable=True) ) event = Event( invocation_id='inv_1', author='agent1', actions=EventActions(end_of_agent=False, agent_state=None), content=Content(role='model', parts=[Part(text='hi')]), ) invocation_context.session.events = [event] invocation_context.populate_invocation_agent_states() assert invocation_context.agent_states == { 'agent1': BaseAgentState().model_dump(mode='json') } assert invocation_context.end_of_agents == {'agent1': False} def test_populate_invocation_agent_states_user_message_event(self): """Tests that populate_invocation_agent_states ignores user message events for default state.""" invocation_context = self._create_test_invocation_context( ResumabilityConfig(is_resumable=True) ) event = Event( invocation_id='inv_1', author='user', actions=EventActions(end_of_agent=False, agent_state=None), content=Content(role='user', parts=[Part(text='hi')]), ) invocation_context.session.events = [event] invocation_context.populate_invocation_agent_states() assert not invocation_context.agent_states assert not invocation_context.end_of_agents def test_populate_invocation_agent_states_no_content(self): """Tests that populate_invocation_agent_states ignores events with no content if no state.""" invocation_context = self._create_test_invocation_context( ResumabilityConfig(is_resumable=True) ) event = Event( invocation_id='inv_1', author='agent1', actions=EventActions(end_of_agent=None, agent_state=None), content=None, ) invocation_context.session.events = [event] invocation_context.populate_invocation_agent_states() assert not invocation_context.agent_states assert not invocation_context.end_of_agents def test_set_agent_state_with_end_of_agent_true(self): """Tests that set_agent_state clears agent_state and sets end_of_agent to True.""" invocation_context = self._create_test_invocation_context( ResumabilityConfig(is_resumable=True) ) invocation_context.agent_states['agent1'] = {} invocation_context.end_of_agents['agent1'] = False # Set state with end_of_agent=True, which should clear the existing # agent_state. invocation_context.set_agent_state('agent1', end_of_agent=True) assert 'agent1' not in invocation_context.agent_states assert invocation_context.end_of_agents['agent1'] def test_set_agent_state_with_agent_state(self): """Tests that set_agent_state sets agent_state and sets end_of_agent to False.""" agent_state = BaseAgentState() invocation_context = self._create_test_invocation_context( ResumabilityConfig(is_resumable=True) ) invocation_context.end_of_agents['agent1'] = True # Set state with agent_state=agent_state, which should set the agent_state # and reset the end_of_agent flag to False. invocation_context.set_agent_state('agent1', agent_state=agent_state) assert invocation_context.agent_states['agent1'] == agent_state.model_dump( mode='json' ) assert invocation_context.end_of_agents['agent1'] is False def test_reset_agent_state(self): """Tests that set_agent_state clears agent_state and end_of_agent.""" invocation_context = self._create_test_invocation_context( ResumabilityConfig(is_resumable=True) ) invocation_context.agent_states['agent1'] = {} invocation_context.end_of_agents['agent1'] = True # Reset state, which should clear the agent_state and end_of_agent flag. invocation_context.set_agent_state('agent1') assert 'agent1' not in invocation_context.agent_states assert 'agent1' not in invocation_context.end_of_agents def test_reset_sub_agent_states(self): """Tests that reset_sub_agent_states resets sub-agent states.""" sub_sub_agent_1 = BaseAgent(name='sub_sub_agent_1') sub_agent_1 = BaseAgent(name='sub_agent_1', sub_agents=[sub_sub_agent_1]) sub_agent_2 = BaseAgent(name='sub_agent_2') root_agent = BaseAgent( name='root_agent', sub_agents=[sub_agent_1, sub_agent_2] ) invocation_context = self._create_test_invocation_context( ResumabilityConfig(is_resumable=True) ) invocation_context.agent = root_agent invocation_context.set_agent_state( 'sub_agent_1', agent_state=BaseAgentState() ) invocation_context.set_agent_state('sub_agent_2', end_of_agent=True) invocation_context.set_agent_state( 'sub_sub_agent_1', agent_state=BaseAgentState() ) assert 'sub_agent_1' in invocation_context.agent_states assert 'sub_agent_2' in invocation_context.end_of_agents assert 'sub_sub_agent_1' in invocation_context.agent_states invocation_context.reset_sub_agent_states('root_agent') assert 'sub_agent_1' not in invocation_context.agent_states assert 'sub_agent_1' not in invocation_context.end_of_agents assert 'sub_agent_2' not in invocation_context.agent_states assert 'sub_agent_2' not in invocation_context.end_of_agents assert 'sub_sub_agent_1' not in invocation_context.agent_states assert 'sub_sub_agent_1' not in invocation_context.end_of_agents class TestFindMatchingFunctionCall: """Test suite for find_matching_function_call.""" @pytest.fixture def test_invocation_context(self): """Create a mock invocation context for testing.""" def _create_invocation_context(events): return InvocationContext( session_service=Mock(spec=BaseSessionService), agent=Mock(spec=BaseAgent, name='agent'), invocation_id='inv_1', session=Mock(spec=Session, events=events), ) return _create_invocation_context def test_find_matching_function_call_found(self, test_invocation_context): """Tests that a matching function call is found.""" fc = Part.from_function_call(name='some_tool', args={}) fc.function_call.id = 'test_function_call_id' fc_event = Event( invocation_id='inv_1', author='agent', content=testing_utils.ModelContent([fc]), ) fr = Part.from_function_response( name='some_tool', response={'result': 'ok'} ) fr.function_response.id = 'test_function_call_id' fr_event = Event( invocation_id='inv_1', author='agent', content=Content(role='user', parts=[fr]), ) invocation_context = test_invocation_context([fc_event, fr_event]) matching_fc_event = invocation_context._find_matching_function_call( fr_event ) assert testing_utils.simplify_content( matching_fc_event.content ) == testing_utils.simplify_content(fc_event.content) def test_find_matching_function_call_not_found(self, test_invocation_context): """Tests that no matching function call is returned if id doesn't match.""" fc = Part.from_function_call(name='some_tool', args={}) fc.function_call.id = 'another_function_call_id' fc_event = Event( invocation_id='inv_1', author='agent', content=testing_utils.ModelContent([fc]), ) fr = Part.from_function_response( name='some_tool', response={'result': 'ok'} ) fr.function_response.id = 'test_function_call_id' fr_event = Event( invocation_id='inv_1', author='agent', content=Content(role='user', parts=[fr]), ) invocation_context = test_invocation_context([fc_event, fr_event]) match = invocation_context._find_matching_function_call(fr_event) assert match is None def test_find_matching_function_call_no_call_events( self, test_invocation_context ): """Tests that no matching function call is returned if there are no call events.""" fr = Part.from_function_response( name='some_tool', response={'result': 'ok'} ) fr.function_response.id = 'test_function_call_id' fr_event = Event( invocation_id='inv_1', author='agent', content=Content(role='user', parts=[fr]), ) invocation_context = test_invocation_context([fr_event]) match = invocation_context._find_matching_function_call(fr_event) assert match is None def test_find_matching_function_call_no_response_in_event( self, test_invocation_context ): """Tests result is None if function_response_event has no function response.""" fr_event_no_fr = Event( author='agent', content=Content(role='user', parts=[Part(text='user message')]), ) fc = Part.from_function_call(name='some_tool', args={}) fc.function_call.id = 'test_function_call_id' fc_event = Event( invocation_id='inv_1', author='agent', content=testing_utils.ModelContent([fc]), ) fr = Part.from_function_response( name='some_tool', response={'result': 'ok'} ) fr.function_response.id = 'test_function_call_id' fr_event = Event( invocation_id='inv_1', author='agent', content=Content(role='user', parts=[Part(text='user message')]), ) invocation_context = test_invocation_context([fc_event, fr_event]) match = invocation_context._find_matching_function_call(fr_event_no_fr) assert match is None def test_stamp_event_branch_context_preserves_isolation_scope( self, test_invocation_context ): """Tests stamp_event_branch_context does not overwrite existing isolation_scope with None.""" fc = Part.from_function_call(name='some_tool', args={}) fc.function_call.id = 'test_function_call_id' fc_event = Event( invocation_id='inv_1', author='agent', branch='root@1', isolation_scope=None, # Coordinator FC has None scope content=testing_utils.ModelContent([fc]), ) fr = Part.from_function_response( name='some_tool', response={'result': 'ok'} ) fr.function_response.id = 'test_function_call_id' fr_event = Event( invocation_id='inv_1', author='agent', isolation_scope='task_123', # Pre-populated active task scope content=Content(role='user', parts=[fr]), ) invocation_context = test_invocation_context([fc_event, fr_event]) invocation_context.stamp_event_branch_context(fr_event) assert fr_event.branch == 'root@1' assert fr_event.isolation_scope == 'task_123' def test_stamp_event_branch_context_does_not_overwrite_existing_scope( self, test_invocation_context ): """Tests stamp_event_branch_context does not overwrite existing isolation_scope if set.""" fc = Part.from_function_call(name='some_tool', args={}) fc.function_call.id = 'test_function_call_id' fc_event = Event( invocation_id='inv_1', author='agent', branch='root@1', isolation_scope='task_456', # Function call has isolation scope content=testing_utils.ModelContent([fc]), ) fr = Part.from_function_response( name='some_tool', response={'result': 'ok'} ) fr.function_response.id = 'test_function_call_id' fr_event = Event( invocation_id='inv_1', author='agent', isolation_scope='task_123', # Pre-populated active task scope content=Content(role='user', parts=[fr]), ) invocation_context = test_invocation_context([fc_event, fr_event]) invocation_context.stamp_event_branch_context(fr_event) assert fr_event.branch == 'root@1' assert fr_event.isolation_scope == 'task_123' def test_find_matching_function_call_when_response_is_not_last_event( self, test_invocation_context ): """Tests that matching function call is found even when response is not the last event in history.""" fc = Part.from_function_call(name='some_tool', args={}) fc.function_call.id = 'test_function_call_id' fc_event = Event( invocation_id='inv_1', author='agent', content=testing_utils.ModelContent([fc]), ) fr = Part.from_function_response( name='some_tool', response={'result': 'ok'} ) fr.function_response.id = 'test_function_call_id' fr_event = Event( invocation_id='inv_1', author='agent', content=Content(role='user', parts=[fr]), ) # Add a subsequent event to the history so that fr_event is NOT the last one subsequent_event = Event( invocation_id='inv_1', author='user', content=Content(role='user', parts=[Part(text='next user message')]), ) invocation_context = test_invocation_context( [fc_event, fr_event, subsequent_event] ) matching_fc_event = invocation_context._find_matching_function_call( fr_event ) assert testing_utils.simplify_content( matching_fc_event.content ) == testing_utils.simplify_content(fc_event.content)