diff --git a/.roost/runreport.json b/.roost/runreport.json new file mode 100644 index 0000000000..42086aa862 --- /dev/null +++ b/.roost/runreport.json @@ -0,0 +1,131 @@ +{ + "run_id": "66bf2165", + "started_at": "2026-04-29T08:53:45.621073Z", + "ended_at": "2026-04-29T08:56:25.723592Z", + "status": "ok", + "config": { + "repo": "/private/var/tmp/Roost/RoostGPT/unit-adk-python/1777452750/source/adk-python", + "out": "/private/var/tmp/Roost/RoostGPT/unit-adk-python/1777452750/source/adk-python", + "workers": 5, + "scenarios_from": "planner", + "provider": "env:AI_TYPE", + "model": "gemini/gemini-3-flash-preview", + "jedi": true + }, + "summary": { + "targets_total": 1, + "targets_passed": 1, + "targets_failed": 0, + "targets_partial": 0, + "targets_skipped": 0, + "scenarios_total": 7, + "scenarios_passed": 7, + "scenarios_failed": 0, + "coverage_weighted": null, + "llm_cost_usd": 0.1565, + "llm_input_tokens": 549347, + "llm_output_tokens": 17765, + "llm_cache_read_input_tokens": 380971, + "llm_cache_creation_input_tokens": 0, + "llm_api_calls": 18, + "elapsed_ms": 0 + }, + "targets": [ + { + "target_symbol_id": "src.google.adk.a2a.converters.event_converter.convert_a2a_task_to_event", + "symbol_kind": "function", + "source_file": "src/google/adk/a2a/converters/event_converter.py", + "source_range": [ + 201, + 265 + ], + "test_file": "/private/var/tmp/Roost/RoostGPT/unit-adk-python/1777452750/source/adk-python/adk/a2a/converters/event_converter/test_convert_a2a_task_to_event.py", + "test_file_hash": "sha256:158d67202970f17d0a2764c6b77c2cc4fed47a1041ea91d5808d8d25ef8ec0b1", + "test_status": "passed", + "iterations": 18, + "pivots": 0, + "scenarios": [ + { + "scenario_id": "src.google.adk.a2a.converters.event_converter.convert_a2a_task_to_event#0", + "scenario_name": "happy_path_artifacts", + "passed": true, + "duration_ms": null, + "failure_reason": null, + "stderr_excerpt": null + }, + { + "scenario_id": "src.google.adk.a2a.converters.event_converter.convert_a2a_task_to_event#1", + "scenario_name": "happy_path_status_message", + "passed": true, + "duration_ms": null, + "failure_reason": null, + "stderr_excerpt": null + }, + { + "scenario_id": "src.google.adk.a2a.converters.event_converter.convert_a2a_task_to_event#2", + "scenario_name": "happy_path_history", + "passed": true, + "duration_ms": null, + "failure_reason": null, + "stderr_excerpt": null + }, + { + "scenario_id": "src.google.adk.a2a.converters.event_converter.convert_a2a_task_to_event#3", + "scenario_name": "edge_case_no_content", + "passed": true, + "duration_ms": null, + "failure_reason": null, + "stderr_excerpt": null + }, + { + "scenario_id": "src.google.adk.a2a.converters.event_converter.convert_a2a_task_to_event#4", + "scenario_name": "with_invocation_context", + "passed": true, + "duration_ms": null, + "failure_reason": null, + "stderr_excerpt": null + }, + { + "scenario_id": "src.google.adk.a2a.converters.event_converter.convert_a2a_task_to_event#5", + "scenario_name": "error_none_task", + "passed": true, + "duration_ms": null, + "failure_reason": null, + "stderr_excerpt": null + }, + { + "scenario_id": "src.google.adk.a2a.converters.event_converter.convert_a2a_task_to_event#6", + "scenario_name": "error_conversion_failure", + "passed": true, + "duration_ms": null, + "failure_reason": null, + "stderr_excerpt": null + } + ], + "coverage_pct": null, + "writer_cost_usd": 0.15653155, + "writer_input_tokens": 549347, + "writer_output_tokens": 17765, + "writer_cache_read_input_tokens": 380971, + "writer_cache_creation_input_tokens": 0, + "diagnostics": [] + } + ], + "files_written": [ + { + "path": "adk/a2a/converters/event_converter/test_convert_a2a_task_to_event.py", + "hash": "sha256:158d67202970f17d0a2764c6b77c2cc4fed47a1041ea91d5808d8d25ef8ec0b1", + "status": "created" + } + ], + "pr_ready": { + "branch_suggestion": "roost/python-tests-20260429-0856", + "commit_message": "roost-pytest: add 1 test file(s)", + "pr_title": "Auto-generated Python unit tests (1 file(s))", + "pr_body_markdown": "## Summary\n- 1 targets with fully green tests\n\n## Generated test files\n- `adk/a2a/converters/event_converter/test_convert_a2a_task_to_event.py`", + "changed_files": [ + "adk/a2a/converters/event_converter/test_convert_a2a_task_to_event.py" + ] + }, + "diagnostics": [] +} \ No newline at end of file diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/adk/__init__.py b/adk/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/adk/a2a/__init__.py b/adk/a2a/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/adk/a2a/converters/__init__.py b/adk/a2a/converters/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/adk/a2a/converters/event_converter/__init__.py b/adk/a2a/converters/event_converter/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/adk/a2a/converters/event_converter/test_convert_a2a_task_to_event.py b/adk/a2a/converters/event_converter/test_convert_a2a_task_to_event.py new file mode 100644 index 0000000000..a338fe5cc3 --- /dev/null +++ b/adk/a2a/converters/event_converter/test_convert_a2a_task_to_event.py @@ -0,0 +1,166 @@ +# ROOST_METHOD_HASH=convert_a2a_task_to_event_40cb3ce258 +# ROOST_METHOD_SIG_HASH=convert_a2a_task_to_event_124ed678ea + +"""Tests for `src.google.adk.a2a.converters.event_converter.convert_a2a_task_to_event`. + +Auto-generated by roost-pytest. Re-generate when the source +changes; see the run report for generation details. +""" +# Source: src/google/adk/a2a/converters/event_converter.py:201-265 +# Scenarios: +# - [happy_path] happy_path_artifacts: Verifies that when a task has artifacts, the last artifact's parts are used to create a message for conversion. +# - [happy_path] happy_path_status_message: Verifies that when a task has no artifacts but has a status message, that message is used for conversion. +# - [happy_path] happy_path_history: Verifies that when a task has no artifacts or status message but has history, the last message in history is used. +# - [edge_case] edge_case_no_content: Verifies that a minimal Event is returned when no message content is found in the task. +# - [happy_path] with_invocation_context: Verifies that invocation_id and branch are correctly propagated from the InvocationContext. +# - [error_path] error_none_task: Verifies that passing None as a2a_task raises a ValueError. +# - [error_path] error_conversion_failure: Verifies that a RuntimeError is raised if the underlying message conversion fails. + +import pytest +from unittest.mock import MagicMock, patch +from google.adk.a2a.converters.event_converter import convert_a2a_task_to_event + +@pytest.mark.generated +@pytest.mark.happy_path +def test_happy_path_artifacts(): + """Verifies that when a task has artifacts, the last artifact's parts are used to create a message for conversion.""" + mock_task = MagicMock() + mock_task.artifacts = [MagicMock(parts=['part1', 'part2'])] + mock_task.status = None + mock_task.history = [] + + mock_event = MagicMock() + + with patch('google.adk.a2a.converters.event_converter.Message') as mock_message_cls, \ + patch('google.adk.a2a.converters.event_converter.Role') as mock_role_cls, \ + patch('google.adk.a2a.converters.event_converter.convert_a2a_message_to_event') as mock_convert: + + mock_convert.return_value = mock_event + mock_message_instance = MagicMock() + mock_message_cls.return_value = mock_message_instance + + result = convert_a2a_task_to_event(mock_task) + + mock_message_cls.assert_called_once_with( + message_id="", role=mock_role_cls.agent, parts=['part1', 'part2'] + ) + mock_convert.assert_called_once() + args, kwargs = mock_convert.call_args + assert args[0] == mock_message_instance + assert result == mock_event + +@pytest.mark.generated +@pytest.mark.happy_path +def test_happy_path_status_message(): + """Verifies that when a task has no artifacts but has a status message, that message is used for conversion.""" + mock_task = MagicMock() + mock_task.artifacts = [] + mock_task.status.message.parts = ['status_part'] + mock_task.status.message.role = 'agent' + mock_task.history = [] + + mock_event = MagicMock() + + with patch('google.adk.a2a.converters.event_converter.convert_a2a_message_to_event') as mock_convert: + mock_convert.return_value = mock_event + + result = convert_a2a_task_to_event(mock_task) + + mock_convert.assert_called_once() + args, kwargs = mock_convert.call_args + assert args[0] == mock_task.status.message + assert result == mock_event + +@pytest.mark.generated +@pytest.mark.happy_path +def test_happy_path_history(): + """Verifies that when a task has no artifacts or status message but has history, the last message in history is used.""" + mock_task = MagicMock() + mock_task.artifacts = [] + mock_task.status = None + mock_task.history = [MagicMock(parts=['hist1']), MagicMock(parts=['hist2'])] + + mock_event = MagicMock() + + with patch('google.adk.a2a.converters.event_converter.convert_a2a_message_to_event') as mock_convert: + mock_convert.return_value = mock_event + + result = convert_a2a_task_to_event(mock_task) + + mock_convert.assert_called_once() + args, kwargs = mock_convert.call_args + assert args[0] == mock_task.history[-1] + assert result == mock_event + +@pytest.mark.generated +@pytest.mark.edge_case +def test_edge_case_no_content(): + """Verifies that a minimal Event is returned when no message content is found in the task.""" + mock_task = MagicMock() + mock_task.artifacts = [] + mock_task.status = None + mock_task.history = [] + + with patch('google.adk.a2a.converters.event_converter.Event') as mock_event_cls, \ + patch('google.adk.a2a.converters.event_converter.uuid.uuid4') as mock_uuid: + + mock_uuid.return_value = 'generated-id' + mock_event_instance = MagicMock() + mock_event_cls.return_value = mock_event_instance + + result = convert_a2a_task_to_event(mock_task) + + mock_event_cls.assert_called_once_with( + invocation_id='generated-id', + author='a2a agent', + branch=None + ) + assert result == mock_event_instance + +@pytest.mark.generated +@pytest.mark.happy_path +def test_with_invocation_context(): + """Verifies that invocation_id and branch are correctly propagated from the InvocationContext.""" + mock_task = MagicMock() + mock_task.artifacts = [] + mock_task.status = None + mock_task.history = [] + + mock_context = MagicMock() + mock_context.invocation_id = 'ctx-id' + mock_context.branch = 'ctx-branch' + + with patch('google.adk.a2a.converters.event_converter.Event') as mock_event_cls: + mock_event_instance = MagicMock() + mock_event_cls.return_value = mock_event_instance + + result = convert_a2a_task_to_event(mock_task, invocation_context=mock_context) + + mock_event_cls.assert_called_once_with( + invocation_id='ctx-id', + author='a2a agent', + branch='ctx-branch' + ) + assert result == mock_event_instance + +@pytest.mark.generated +@pytest.mark.error_path +def test_error_none_task(): + """Verifies that passing None as a2a_task raises a ValueError.""" + with pytest.raises(ValueError, match="A2A task cannot be None"): + convert_a2a_task_to_event(None) + +@pytest.mark.generated +@pytest.mark.error_path +def test_error_conversion_failure(): + """Verifies that a RuntimeError is raised if the underlying message conversion fails.""" + mock_task = MagicMock() + mock_task.artifacts = [MagicMock(parts=['p1'])] + mock_task.status = None + mock_task.history = [] + + with patch('google.adk.a2a.converters.event_converter.Message'), \ + patch('google.adk.a2a.converters.event_converter.Role'), \ + patch('google.adk.a2a.converters.event_converter.convert_a2a_message_to_event', side_effect=Exception('inner error')): + with pytest.raises(RuntimeError, match="Failed to convert task message: inner error"): + convert_a2a_task_to_event(mock_task) diff --git a/conftest.py b/conftest.py new file mode 100644 index 0000000000..5964a6943d --- /dev/null +++ b/conftest.py @@ -0,0 +1,27 @@ +"""roost-pytest test-suite configuration. + +This file is auto-generated. It registers the custom markers used by +generated tests so pytest doesn't warn ``PytestUnknownMarkWarning``. +Add your own fixtures/hooks below the marker block as you would in +any pytest project. +""" + +from __future__ import annotations + +import pytest + + +_GENERATED_MARKERS = { + 'generated': 'test was auto-generated by roost-pytest', + 'happy_path': 'exercises the primary success path', + 'edge_case': 'exercises an edge / boundary condition', + 'error_path': 'exercises an exception or failure path', + 'security': 'exercises a security-sensitive behaviour', + 'property': 'property / invariant test (often parametrised)', + 'regression': 'guards against a previously fixed bug', +} + + +def pytest_configure(config: pytest.Config) -> None: + for name, description in _GENERATED_MARKERS.items(): + config.addinivalue_line("markers", f"{name}: {description}") diff --git a/pyproject.toml b/pyproject.toml index 69ba9984e6..449533a0cd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -136,6 +136,7 @@ test = [ "rouge-score>=0.1.2", "tabulate>=0.9.0", # go/keep-sorted end + "google" ] docs = [