Skip to content

Commit 77aeadf

Browse files
committed
refactor(tests): Consolidate event tests into test_event.py
Merge test_event_message.py and test_workflow_events.py into test_event.py to simplify the test suite and keep all Event class behavior tests in a single place. Change-Id: I99223f44e54fce8d37c5d632358e4343a8dc526d
1 parent bae5b1a commit 77aeadf

3 files changed

Lines changed: 238 additions & 259 deletions

File tree

tests/unittests/events/test_event.py

Lines changed: 238 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,14 @@
1414

1515
from __future__ import annotations
1616

17-
"""Unit tests for the helper methods on the Event class."""
17+
"""Unit tests for the Event class."""
1818

1919
from google.adk.events.event import Event
20+
from google.adk.events.event import NodeInfo
2021
from google.adk.events.event_actions import EventActions
22+
from google.adk.events.request_input import RequestInput
2123
from google.genai import types
24+
import pytest
2225

2326

2427
def _text_part(text: str = 'hello') -> types.Part:
@@ -188,3 +191,237 @@ def test_event_ids_are_unique():
188191

189192
def test_event_id_preserved_when_provided():
190193
assert _event(id='fixed-id').id == 'fixed-id'
194+
195+
196+
# --- state initialization ----------------------------------------------------
197+
198+
199+
def test_event_constructor_with_state():
200+
"""Tests that the event constructor handles the state argument."""
201+
my_event = Event(state={'key': 'value'})
202+
assert my_event.actions is not None
203+
assert my_event.actions.state_delta == {'key': 'value'}
204+
205+
206+
def test_event_constructor_without_state():
207+
"""Tests that the event constructor works without the state argument."""
208+
my_event = Event()
209+
assert my_event.actions is not None
210+
assert my_event.actions.state_delta == {}
211+
212+
213+
# --- isolation scope ---------------------------------------------------------
214+
215+
216+
def test_event_isolation_scope():
217+
"""Tests Event.isolation_scope default value and serialization."""
218+
ev = Event()
219+
assert ev.isolation_scope is None
220+
221+
ev2 = Event(isolation_scope='task:fc-123')
222+
dumped = ev2.model_dump(mode='json', by_alias=True, exclude_none=True)
223+
assert dumped['isolationScope'] == 'task:fc-123'
224+
225+
226+
# --- serialization -----------------------------------------------------------
227+
228+
229+
def test_event_serialization_always_camel_case():
230+
"""Tests that Event serialization produces camelCase keys."""
231+
request_input = RequestInput(interrupt_id='fc-1', message='test')
232+
233+
# Create an event with fields that would produce snake_case if not dumped by alias
234+
event = Event(
235+
invocation_id='i-1',
236+
node_info=NodeInfo(
237+
path='a/b',
238+
output_for=['c'],
239+
message_as_output=True,
240+
),
241+
output=request_input,
242+
)
243+
244+
dumped = event.model_dump(by_alias=True)
245+
246+
def check_no_snake_case_keys(data):
247+
if isinstance(data, dict):
248+
for key, value in data.items():
249+
assert '_' not in key, f'Found snake_case key: {key} in {data}'
250+
check_no_snake_case_keys(value)
251+
elif isinstance(data, list):
252+
for item in data:
253+
check_no_snake_case_keys(item)
254+
255+
check_no_snake_case_keys(dumped)
256+
257+
# Also verify that expected keys are indeed camelCased
258+
assert 'invocationId' in dumped
259+
assert 'nodeInfo' in dumped
260+
assert 'outputFor' in dumped['nodeInfo']
261+
assert 'messageAsOutput' in dumped['nodeInfo']
262+
263+
# Verify RequestInput fields are camelCased
264+
assert 'output' in dumped
265+
assert 'interruptId' in dumped['output']
266+
267+
268+
# --- message alias for content -----------------------------------------------
269+
270+
271+
class TestMessageConstructor:
272+
"""Tests for Event(message=...) constructor parameter."""
273+
274+
def test_message_str_sets_content(self):
275+
event = Event(message='Hello!')
276+
assert event.content is not None
277+
assert event.content.parts[0].text == 'Hello!'
278+
279+
def test_message_content_passes_through(self):
280+
content = types.Content(
281+
parts=[types.Part(text='from Content')], role='model'
282+
)
283+
event = Event(message=content)
284+
assert event.content is content
285+
286+
def test_message_part_converts_to_content(self):
287+
part = types.Part(text='from Part')
288+
event = Event(message=part)
289+
assert event.content is not None
290+
assert event.content.parts[0].text == 'from Part'
291+
292+
def test_message_list_of_parts(self):
293+
parts = [types.Part(text='part1'), types.Part(text='part2')]
294+
event = Event(message=parts)
295+
assert event.content is not None
296+
assert len(event.content.parts) == 2
297+
assert event.content.parts[0].text == 'part1'
298+
assert event.content.parts[1].text == 'part2'
299+
300+
def test_message_and_content_raises(self):
301+
with pytest.raises(ValueError, match='mutually exclusive'):
302+
Event(
303+
message='hello',
304+
content=types.Content(parts=[types.Part(text='world')]),
305+
)
306+
307+
def test_content_still_works(self):
308+
content = types.Content(
309+
parts=[types.Part(text='via content')], role='model'
310+
)
311+
event = Event(content=content)
312+
assert event.content is content
313+
assert event.content.parts[0].text == 'via content'
314+
315+
def test_neither_message_nor_content(self):
316+
event = Event()
317+
assert event.content is None
318+
319+
320+
class TestMessageProperty:
321+
"""Tests for Event.message property getter and setter."""
322+
323+
def test_message_getter_aliases_content(self):
324+
content = types.Content(parts=[types.Part(text='hello')], role='model')
325+
event = Event(content=content)
326+
assert event.message is event.content
327+
328+
def test_message_getter_none_when_no_content(self):
329+
event = Event()
330+
assert event.message is None
331+
332+
def test_message_setter_updates_content(self):
333+
event = Event()
334+
new_content = types.Content(
335+
parts=[types.Part(text='updated')], role='model'
336+
)
337+
event.message = new_content
338+
assert event.content is new_content
339+
340+
def test_message_setter_accepts_str(self):
341+
event = Event()
342+
event.message = 'updated via setter'
343+
assert event.content is not None
344+
assert event.content.parts[0].text == 'updated via setter'
345+
346+
def test_message_setter_none_clears_content(self):
347+
event = Event(message='hello')
348+
event.message = None
349+
assert event.content is None
350+
351+
def test_message_from_constructor_readable_via_property(self):
352+
event = Event(message='Hello!')
353+
assert event.message is not None
354+
assert event.message.parts[0].text == 'Hello!'
355+
356+
357+
class TestMessageSerialization:
358+
"""Tests that serialization uses 'content', not 'message'."""
359+
360+
def test_serialized_uses_content_field(self):
361+
event = Event(message='Hello!')
362+
data = event.model_dump(exclude_none=True)
363+
assert 'content' in data
364+
assert 'message' not in data
365+
366+
def test_round_trip_via_content(self):
367+
event = Event(message='Hello!')
368+
data = event.model_dump()
369+
restored = Event.model_validate(data)
370+
assert restored.content is not None
371+
assert restored.content.parts[0].text == 'Hello!'
372+
assert restored.message is not None
373+
assert restored.message.parts[0].text == 'Hello!'
374+
375+
376+
class TestMessageWithOtherKwargs:
377+
"""Tests message combined with other convenience kwargs."""
378+
379+
def test_message_with_state(self):
380+
event = Event(message='hello', state={'key': 'val'})
381+
assert event.content is not None
382+
assert event.content.parts[0].text == 'hello'
383+
assert event.actions.state_delta == {'key': 'val'}
384+
385+
def test_message_with_route(self):
386+
event = Event(message='hello', route='next')
387+
assert event.content is not None
388+
assert event.actions.route == 'next'
389+
390+
391+
class TestMessageSubclassField:
392+
"""Tests that a subclass declaring `message` as a real field is honored.
393+
394+
`_accept_convenience_kwargs` already routes construction kwargs to such a
395+
field; the `message` property/setter must defer to it too instead of
396+
aliasing `content`.
397+
"""
398+
399+
def test_subclass_field_readable_via_property(self):
400+
class _Sub(Event):
401+
message: str = ''
402+
403+
event = _Sub(message='hello', author='a')
404+
assert event.message == 'hello'
405+
406+
def test_subclass_field_serializes_and_round_trips(self):
407+
class _Sub(Event):
408+
message: str = ''
409+
410+
event = _Sub(message='hello', author='a')
411+
data = event.model_dump()
412+
assert data['message'] == 'hello'
413+
assert _Sub.model_validate(data).message == 'hello'
414+
415+
def test_subclass_field_setter_updates_field_not_content(self):
416+
class _Sub(Event):
417+
message: str = ''
418+
419+
event = _Sub(message='hello', author='a')
420+
event.message = 'updated'
421+
assert event.message == 'updated'
422+
assert event.content is None
423+
424+
def test_base_event_message_still_aliases_content(self):
425+
content = types.Content(parts=[types.Part(text='hi')], role='model')
426+
event = Event(content=content)
427+
assert event.message is event.content

0 commit comments

Comments
 (0)