Files
wehub-resource-sync ec2b666284
Continuous Integration / Pre-commit Linter (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.10) (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.11) (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.12) (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.13) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.10) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.11) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.12) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.13) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.14) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.10) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.11) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.12) (push) Has been cancelled
Copybara PR Handler / close-imported-pr (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.13) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.14) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:25:13 +08:00

448 lines
13 KiB
Python

# 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