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
151 lines
5.5 KiB
Python
151 lines
5.5 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 EventActions serialization and its fallback helper."""
|
|
|
|
import datetime
|
|
import logging
|
|
|
|
from google.adk.events.event_actions import _make_json_serializable
|
|
from google.adk.events.event_actions import EventActions
|
|
from pydantic import BaseModel
|
|
|
|
|
|
class _Sample(BaseModel):
|
|
x: int = 5
|
|
label: str = 'hi'
|
|
|
|
|
|
class TestMakeJsonSerializable:
|
|
"""Tests for the `_make_json_serializable` fallback helper."""
|
|
|
|
def test_plain_values_are_unchanged(self):
|
|
value = {'a': 1, 'b': [1, 2], 'c': {'d': 'e'}, 'f': None, 'g': True}
|
|
assert _make_json_serializable(value) == value
|
|
|
|
def test_datetime_is_preserved_not_discarded(self):
|
|
dt = datetime.datetime(2024, 1, 2, 3, 4, 5, tzinfo=datetime.timezone.utc)
|
|
assert _make_json_serializable(dt) == '2024-01-02T03:04:05Z'
|
|
|
|
def test_pydantic_model_is_serialized_to_dict(self):
|
|
assert _make_json_serializable(_Sample()) == {'x': 5, 'label': 'hi'}
|
|
|
|
def test_nested_rich_types_are_serialized(self):
|
|
dt = datetime.datetime(2024, 5, 6, tzinfo=datetime.timezone.utc)
|
|
result = _make_json_serializable({'when': dt, 'model': _Sample(), 'n': [1]})
|
|
assert result == {
|
|
'when': '2024-05-06T00:00:00Z',
|
|
'model': {'x': 5, 'label': 'hi'},
|
|
'n': [1],
|
|
}
|
|
|
|
def test_unserializable_value_is_replaced_with_repr(self):
|
|
result = _make_json_serializable(lambda: 1)
|
|
assert isinstance(result, str)
|
|
assert 'function' in result
|
|
|
|
def test_unserializable_value_nested(self):
|
|
result = _make_json_serializable({'cb': lambda: 1, 'ok': 2})
|
|
assert result['ok'] == 2
|
|
assert isinstance(result['cb'], str)
|
|
|
|
|
|
class TestStateDeltaSerialization:
|
|
"""Tests for the `state_delta` wrap serializer."""
|
|
|
|
def test_serializable_state_delta_round_trips(self):
|
|
actions = EventActions(state_delta={'a': 1, 'b': [1, 2], 'c': {'d': 'e'}})
|
|
dumped = actions.model_dump(mode='json')
|
|
assert dumped['state_delta'] == {'a': 1, 'b': [1, 2], 'c': {'d': 'e'}}
|
|
|
|
def test_non_serializable_state_delta_does_not_raise(self):
|
|
actions = EventActions(state_delta={'cb': lambda: 1, 'ok': 2})
|
|
dumped = actions.model_dump(mode='json')
|
|
assert dumped['state_delta']['ok'] == 2
|
|
assert isinstance(dumped['state_delta']['cb'], str)
|
|
|
|
def test_non_serializable_state_delta_logs_warning(self, caplog):
|
|
actions = EventActions(state_delta={'cb': lambda: 1})
|
|
with caplog.at_level(logging.WARNING):
|
|
actions.model_dump(mode='json')
|
|
assert any(
|
|
'Failed to serialize `state_delta`' in record.message
|
|
for record in caplog.records
|
|
)
|
|
|
|
def test_datetime_preserved_when_fallback_triggered(self):
|
|
# A callable forces the fallback path; the datetime must still serialize
|
|
# faithfully rather than being discarded.
|
|
dt = datetime.datetime(2024, 1, 2, 3, 4, 5, tzinfo=datetime.timezone.utc)
|
|
actions = EventActions(state_delta={'when': dt, 'cb': lambda: 1})
|
|
dumped = actions.model_dump(mode='json')
|
|
assert dumped['state_delta']['when'] == '2024-01-02T03:04:05Z'
|
|
|
|
def test_exclude_is_respected_for_serializable_state(self):
|
|
actions = EventActions(
|
|
state_delta={'_adk_replay_config': {'dir': '/x'}, 'foo': 1}
|
|
)
|
|
dumped = actions.model_dump(
|
|
mode='json', exclude={'state_delta': {'_adk_replay_config': True}}
|
|
)
|
|
assert dumped['state_delta'] == {'foo': 1}
|
|
|
|
def test_exclude_is_respected_in_fallback_path(self):
|
|
# Even when sanitization is required (callable present), caller `exclude`
|
|
# directives must still be applied to the fallback output.
|
|
actions = EventActions(
|
|
state_delta={
|
|
'_adk_replay_config': {'dir': '/x'},
|
|
'cb': lambda: 1,
|
|
'ok': 2,
|
|
}
|
|
)
|
|
dumped = actions.model_dump(
|
|
mode='json', exclude={'state_delta': {'_adk_replay_config': True}}
|
|
)
|
|
assert '_adk_replay_config' not in dumped['state_delta']
|
|
assert dumped['state_delta']['ok'] == 2
|
|
assert isinstance(dumped['state_delta']['cb'], str)
|
|
|
|
|
|
class TestAgentStateSerialization:
|
|
"""Tests for the `agent_state` wrap serializer."""
|
|
|
|
def test_none_agent_state_serializes_to_none(self):
|
|
assert EventActions().model_dump(mode='json')['agent_state'] is None
|
|
|
|
def test_serializable_agent_state_round_trips(self):
|
|
actions = EventActions(agent_state={'a': 1, 'b': 'two'})
|
|
assert actions.model_dump(mode='json')['agent_state'] == {
|
|
'a': 1,
|
|
'b': 'two',
|
|
}
|
|
|
|
def test_non_serializable_agent_state_does_not_raise(self):
|
|
actions = EventActions(agent_state={'cb': lambda: 1, 'n': 3})
|
|
dumped = actions.model_dump(mode='json')
|
|
assert dumped['agent_state']['n'] == 3
|
|
assert isinstance(dumped['agent_state']['cb'], str)
|
|
|
|
def test_non_serializable_agent_state_logs_warning(self, caplog):
|
|
actions = EventActions(agent_state={'cb': lambda: 1})
|
|
with caplog.at_level(logging.WARNING):
|
|
actions.model_dump(mode='json')
|
|
assert any(
|
|
'Failed to serialize `agent_state`' in record.message
|
|
for record in caplog.records
|
|
)
|