# 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 __future__ import annotations """Unit tests for the helper methods on the Event class.""" import copy from google.adk.events.event import Event from google.adk.events.event import NodeInfo from google.adk.events.event_actions import EventActions from google.adk.events.request_input import RequestInput from google.genai import types import pytest def _text_part(text: str = 'hello') -> types.Part: return types.Part(text=text) def _function_call_part(name: str = 'my_func') -> types.Part: return types.Part(function_call=types.FunctionCall(name=name, args={'x': 1})) def _function_response_part(name: str = 'my_func') -> types.Part: return types.Part( function_response=types.FunctionResponse(name=name, response={'y': 2}) ) def _code_execution_result_part(output: str = '42') -> types.Part: return types.Part( code_execution_result=types.CodeExecutionResult( outcome=types.Outcome.OUTCOME_OK, output=output ) ) def _event(parts: list[types.Part] | None = None, **kwargs) -> Event: content = ( types.Content(role='model', parts=parts) if parts is not None else None ) return Event(author='agent', content=content, **kwargs) # --- is_final_response ------------------------------------------------------- def test_is_final_response_plain_text_event_is_final(): event = _event(parts=[_text_part()]) assert event.is_final_response() is True def test_is_final_response_empty_event_is_final(): event = _event() assert event.is_final_response() is True def test_is_final_response_with_function_call_is_not_final(): event = _event(parts=[_text_part(), _function_call_part()]) assert event.is_final_response() is False def test_is_final_response_with_function_response_is_not_final(): event = _event(parts=[_function_response_part()]) assert event.is_final_response() is False def test_is_final_response_partial_event_is_not_final(): event = _event(parts=[_text_part()], partial=True) assert event.is_final_response() is False def test_is_final_response_with_trailing_code_result_is_not_final(): event = _event(parts=[_text_part(), _code_execution_result_part()]) assert event.is_final_response() is False def test_is_final_response_skip_summarization_overrides_function_response(): event = _event( parts=[_function_response_part()], actions=EventActions(skip_summarization=True), ) assert event.is_final_response() is True def test_is_final_response_long_running_tool_ids_overrides_function_call(): event = _event( parts=[_function_call_part()], long_running_tool_ids={'tool-1'} ) assert event.is_final_response() is True # --- get_function_calls ------------------------------------------------------ def test_get_function_calls_returns_calls_in_order(): event = _event( parts=[ _text_part(), _function_call_part('first'), _function_response_part(), _function_call_part('second'), ] ) assert [call.name for call in event.get_function_calls()] == [ 'first', 'second', ] def test_get_function_calls_no_content_returns_empty(): assert _event().get_function_calls() == [] def test_get_function_calls_empty_parts_returns_empty(): assert _event(parts=[]).get_function_calls() == [] def test_get_function_calls_text_only_returns_empty(): assert _event(parts=[_text_part()]).get_function_calls() == [] # --- get_function_responses -------------------------------------------------- def test_get_function_responses_returns_responses_in_order(): event = _event( parts=[ _function_response_part('first'), _text_part(), _function_call_part(), _function_response_part('second'), ] ) assert [resp.name for resp in event.get_function_responses()] == [ 'first', 'second', ] def test_get_function_responses_no_content_returns_empty(): assert _event().get_function_responses() == [] def test_get_function_responses_empty_parts_returns_empty(): assert _event(parts=[]).get_function_responses() == [] # --- has_trailing_code_execution_result -------------------------------------- def test_has_trailing_code_execution_result_true_when_last(): event = _event(parts=[_text_part(), _code_execution_result_part()]) assert event.has_trailing_code_execution_result() is True def test_has_trailing_code_execution_result_false_when_not_last(): event = _event(parts=[_code_execution_result_part(), _text_part()]) assert event.has_trailing_code_execution_result() is False def test_has_trailing_code_execution_result_false_no_content(): assert _event().has_trailing_code_execution_result() is False def test_has_trailing_code_execution_result_false_empty_parts(): assert _event(parts=[]).has_trailing_code_execution_result() is False # --- id generation (model_post_init) ----------------------------------------- def test_event_id_auto_assigned_when_missing(): assert _event().id != '' def test_event_ids_are_unique(): assert _event().id != _event().id def test_event_id_preserved_when_provided(): assert _event(id='fixed-id').id == 'fixed-id' # --- state initialization ---------------------------------------------------- def test_event_constructor_with_state(): """Tests that the event constructor handles the state argument.""" my_event = Event(state={'key': 'value'}) assert my_event.actions is not None assert my_event.actions.state_delta == {'key': 'value'} def test_event_constructor_without_state(): """Tests that the event constructor works without the state argument.""" my_event = Event() assert my_event.actions is not None assert my_event.actions.state_delta == {} # --- isolation scope --------------------------------------------------------- def test_event_isolation_scope(): """Tests Event.isolation_scope default value and serialization.""" ev = Event() assert ev.isolation_scope is None ev2 = Event(isolation_scope='task:fc-123') dumped = ev2.model_dump(mode='json', by_alias=True, exclude_none=True) assert dumped['isolationScope'] == 'task:fc-123' # --- serialization ----------------------------------------------------------- def test_event_serialization_always_camel_case(): """Tests that Event serialization produces camelCase keys.""" request_input = RequestInput(interrupt_id='fc-1', message='test') # Create an event with fields that would produce snake_case if not dumped by alias event = Event( invocation_id='i-1', node_info=NodeInfo( path='a/b', output_for=['c'], message_as_output=True, ), output=request_input, ) dumped = event.model_dump(by_alias=True) def check_no_snake_case_keys(data): if isinstance(data, dict): for key, value in data.items(): assert '_' not in key, f'Found snake_case key: {key} in {data}' check_no_snake_case_keys(value) elif isinstance(data, list): for item in data: check_no_snake_case_keys(item) check_no_snake_case_keys(dumped) # Also verify that expected keys are indeed camelCased assert 'invocationId' in dumped assert 'nodeInfo' in dumped assert 'outputFor' in dumped['nodeInfo'] assert 'messageAsOutput' in dumped['nodeInfo'] # Verify RequestInput fields are camelCased assert 'output' in dumped assert 'interruptId' in dumped['output'] # --- message alias for content ----------------------------------------------- class TestMessageConstructor: """Tests for Event(message=...) constructor parameter.""" def test_message_str_sets_content(self): event = Event(message='Hello!') assert event.content is not None assert event.content.parts[0].text == 'Hello!' def test_message_content_passes_through(self): content = types.Content( parts=[types.Part(text='from Content')], role='model' ) event = Event(message=content) assert event.content is content def test_message_part_converts_to_content(self): part = types.Part(text='from Part') event = Event(message=part) assert event.content is not None assert event.content.parts[0].text == 'from Part' def test_message_list_of_parts(self): parts = [types.Part(text='part1'), types.Part(text='part2')] event = Event(message=parts) assert event.content is not None assert len(event.content.parts) == 2 assert event.content.parts[0].text == 'part1' assert event.content.parts[1].text == 'part2' def test_message_and_content_raises(self): with pytest.raises(ValueError, match='mutually exclusive'): Event( message='hello', content=types.Content(parts=[types.Part(text='world')]), ) def test_content_still_works(self): content = types.Content( parts=[types.Part(text='via content')], role='model' ) event = Event(content=content) assert event.content is content assert event.content.parts[0].text == 'via content' def test_neither_message_nor_content(self): event = Event() assert event.content is None class TestMessageProperty: """Tests for Event.message property getter and setter.""" def test_message_getter_aliases_content(self): content = types.Content(parts=[types.Part(text='hello')], role='model') event = Event(content=content) assert event.message is event.content def test_message_getter_none_when_no_content(self): event = Event() assert event.message is None def test_message_setter_updates_content(self): event = Event() new_content = types.Content( parts=[types.Part(text='updated')], role='model' ) event.message = new_content assert event.content is new_content def test_message_setter_accepts_str(self): event = Event() event.message = 'updated via setter' assert event.content is not None assert event.content.parts[0].text == 'updated via setter' def test_message_setter_none_clears_content(self): event = Event(message='hello') event.message = None assert event.content is None def test_message_from_constructor_readable_via_property(self): event = Event(message='Hello!') assert event.message is not None assert event.message.parts[0].text == 'Hello!' class TestMessageSerialization: """Tests that serialization uses 'content', not 'message'.""" def test_serialized_uses_content_field(self): event = Event(message='Hello!') data = event.model_dump(exclude_none=True) assert 'content' in data assert 'message' not in data def test_round_trip_via_content(self): event = Event(message='Hello!') data = event.model_dump() restored = Event.model_validate(data) assert restored.content is not None assert restored.content.parts[0].text == 'Hello!' assert restored.message is not None assert restored.message.parts[0].text == 'Hello!' def test_model_validate_does_not_mutate_input_dict(self): data = { 'message': 'Hello!', 'state': {'key': 'value'}, 'route': 'next', 'node_path': 'root.node', } original = copy.deepcopy(data) event = Event.model_validate(data) assert data == original assert event.content is not None assert event.content.parts[0].text == 'Hello!' assert event.actions.state_delta == {'key': 'value'} assert event.actions.route == 'next' assert event.node_info.path == 'root.node' class TestMessageWithOtherKwargs: """Tests message combined with other convenience kwargs.""" def test_message_with_state(self): event = Event(message='hello', state={'key': 'val'}) assert event.content is not None assert event.content.parts[0].text == 'hello' assert event.actions.state_delta == {'key': 'val'} def test_message_with_route(self): event = Event(message='hello', route='next') assert event.content is not None assert event.actions.route == 'next' class TestMessageSubclassField: """Tests that a subclass declaring `message` as a real field is honored. `_accept_convenience_kwargs` already routes construction kwargs to such a field; the `message` property/setter must defer to it too instead of aliasing `content`. """ def test_subclass_field_readable_via_property(self): class _Sub(Event): message: str = '' event = _Sub(message='hello', author='a') assert event.message == 'hello' def test_subclass_field_serializes_and_round_trips(self): class _Sub(Event): message: str = '' event = _Sub(message='hello', author='a') data = event.model_dump() assert data['message'] == 'hello' assert _Sub.model_validate(data).message == 'hello' def test_subclass_field_setter_updates_field_not_content(self): class _Sub(Event): message: str = '' event = _Sub(message='hello', author='a') event.message = 'updated' assert event.message == 'updated' assert event.content is None def test_base_event_message_still_aliases_content(self): content = types.Content(parts=[types.Part(text='hi')], role='model') event = Event(content=content) assert event.message is event.content