diff --git a/.roost/runreport.json b/.roost/runreport.json new file mode 100644 index 0000000000..b84069eb90 --- /dev/null +++ b/.roost/runreport.json @@ -0,0 +1,131 @@ +{ + "run_id": "9133e0e9", + "started_at": "2026-04-29T05:46:20.901176Z", + "ended_at": "2026-04-29T05:48:25.658533Z", + "status": "ok", + "config": { + "repo": "/private/var/tmp/Roost/RoostGPT/unit-adk-python/1777441516/source/adk-python", + "out": "/private/var/tmp/Roost/RoostGPT/unit-adk-python/1777441516/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.1452, + "llm_input_tokens": 541206, + "llm_output_tokens": 12324, + "llm_cache_read_input_tokens": 360814, + "llm_cache_creation_input_tokens": 0, + "llm_api_calls": 19, + "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/1777441516/source/adk-python/adk/a2a/converters/event_converter/test_convert_a2a_task_to_event.py", + "test_file_hash": "sha256:46cc961cb6ad23a88ebbd58e126a82963a00fb97267044dc6507ed8377a93c60", + "test_status": "passed", + "iterations": 19, + "pivots": 0, + "scenarios": [ + { + "scenario_id": "src.google.adk.a2a.converters.event_converter.convert_a2a_task_to_event#0", + "scenario_name": "happy_path_with_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_with_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_with_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": "happy_path_minimal_event", + "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": "edge_case_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_path_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_path_conversion_failure", + "passed": true, + "duration_ms": null, + "failure_reason": null, + "stderr_excerpt": null + } + ], + "coverage_pct": null, + "writer_cost_usd": 0.14520869999999997, + "writer_input_tokens": 541206, + "writer_output_tokens": 12324, + "writer_cache_read_input_tokens": 360814, + "writer_cache_creation_input_tokens": 0, + "diagnostics": [] + } + ], + "files_written": [ + { + "path": "adk/a2a/converters/event_converter/test_convert_a2a_task_to_event.py", + "hash": "sha256:46cc961cb6ad23a88ebbd58e126a82963a00fb97267044dc6507ed8377a93c60", + "status": "created" + } + ], + "pr_ready": { + "branch_suggestion": "roost/python-tests-20260429-0548", + "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..151d65d857 --- /dev/null +++ b/adk/a2a/converters/event_converter/test_convert_a2a_task_to_event.py @@ -0,0 +1,145 @@ +# 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_with_artifacts: Verify that the function correctly extracts the message from task artifacts when they are present. +# - [happy_path] happy_path_with_status_message: Verify that the function correctly extracts the message from task status when artifacts are missing. +# - [happy_path] happy_path_with_history: Verify that the function correctly extracts the message from task history when artifacts and status message are missing. +# - [happy_path] happy_path_minimal_event: Verify that the function returns a minimal Event when no message source is available in the task. +# - [edge_case] edge_case_with_invocation_context: Verify that the function correctly uses the provided invocation context for the resulting Event. +# - [error_path] error_path_none_task: Verify that the function raises ValueError when the input task is None. +# - [error_path] error_path_conversion_failure: Verify that the function raises RuntimeError when the underlying message conversion fails. + +import pytest +from unittest.mock import MagicMock, patch, ANY +import uuid + +from a2a.types import Message, Role, Task, Part as A2APart, TextPart +from google.adk.agents.invocation_context import InvocationContext +from google.adk.events.event import Event +from google.adk.a2a.converters.event_converter import convert_a2a_task_to_event + +@pytest.mark.generated +@pytest.mark.happy_path +def test_happy_path_with_artifacts(): + """Verify that the function correctly extracts the message from task artifacts when they are present.""" + a2a_task = MagicMock(spec=Task) + # Use real Part and TextPart to avoid Pydantic validation errors in Message constructor + mock_part = A2APart(root=TextPart(text="test artifact")) + a2a_task.artifacts = [MagicMock(parts=[mock_part])] + a2a_task.status = None + a2a_task.history = [] + + mock_event = MagicMock(spec=Event) + + with patch("google.adk.a2a.converters.event_converter.convert_a2a_message_to_event", return_value=mock_event) as mock_convert: + result = convert_a2a_task_to_event(a2a_task) + + assert result == mock_event + mock_convert.assert_called_once() + call_args = mock_convert.call_args[0] + passed_message = call_args[0] + assert isinstance(passed_message, Message) + assert passed_message.message_id == "" + assert passed_message.role == Role.agent + assert passed_message.parts == [mock_part] + +@pytest.mark.generated +@pytest.mark.happy_path +def test_happy_path_with_status_message(): + """Verify that the function correctly extracts the message from task status when artifacts are missing.""" + a2a_task = MagicMock(spec=Task) + a2a_task.artifacts = [] + mock_message = MagicMock(spec=Message) + mock_message.parts = [MagicMock()] + a2a_task.status = MagicMock(message=mock_message) + a2a_task.history = [] + + mock_event = MagicMock(spec=Event) + + with patch("google.adk.a2a.converters.event_converter.convert_a2a_message_to_event", return_value=mock_event) as mock_convert: + result = convert_a2a_task_to_event(a2a_task) + + assert result == mock_event + mock_convert.assert_called_once_with(mock_message, None, None, part_converter=ANY) + +@pytest.mark.generated +@pytest.mark.happy_path +def test_happy_path_with_history(): + """Verify that the function correctly extracts the message from task history when artifacts and status message are missing.""" + a2a_task = MagicMock(spec=Task) + a2a_task.artifacts = [] + a2a_task.status = None + mock_message = MagicMock(spec=Message) + a2a_task.history = [mock_message] + + mock_event = MagicMock(spec=Event) + + with patch("google.adk.a2a.converters.event_converter.convert_a2a_message_to_event", return_value=mock_event) as mock_convert: + result = convert_a2a_task_to_event(a2a_task) + + assert result == mock_event + mock_convert.assert_called_once_with(mock_message, None, None, part_converter=ANY) + +@pytest.mark.generated +@pytest.mark.happy_path +def test_happy_path_minimal_event(): + """Verify that the function returns a minimal Event when no message source is available in the task.""" + a2a_task = MagicMock(spec=Task) + a2a_task.artifacts = [] + a2a_task.status = None + a2a_task.history = [] + + fixed_uuid = "12345678-1234-5678-1234-567812345678" + with patch("uuid.uuid4", return_value=uuid.UUID(fixed_uuid)): + result = convert_a2a_task_to_event(a2a_task) + + assert isinstance(result, Event) + assert result.author == "a2a agent" + assert result.invocation_id == fixed_uuid + assert result.branch is None + +@pytest.mark.generated +@pytest.mark.edge_case +def test_edge_case_with_invocation_context(): + """Verify that the function correctly uses the provided invocation context for the resulting Event.""" + a2a_task = MagicMock(spec=Task) + a2a_task.artifacts = [] + a2a_task.status = None + a2a_task.history = [] + + invocation_context = MagicMock(spec=InvocationContext) + invocation_context.invocation_id = 'test-id' + invocation_context.branch = 'test-branch' + + result = convert_a2a_task_to_event(a2a_task, invocation_context=invocation_context) + + assert isinstance(result, Event) + assert result.invocation_id == 'test-id' + assert result.branch == 'test-branch' + +@pytest.mark.generated +@pytest.mark.error_path +def test_error_path_none_task(): + """Verify that the function raises ValueError when the input task is None.""" + 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_path_conversion_failure(): + """Verify that the function raises RuntimeError when the underlying message conversion fails.""" + a2a_task = MagicMock(spec=Task) + a2a_task.artifacts = [] + a2a_task.status = None + a2a_task.history = [MagicMock(spec=Message)] + + with patch("google.adk.a2a.converters.event_converter.convert_a2a_message_to_event", side_effect=Exception("fail")): + with pytest.raises(RuntimeError, match="Failed to convert task message: fail"): + convert_a2a_task_to_event(a2a_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..3ac9692460 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -136,6 +136,8 @@ test = [ "rouge-score>=0.1.2", "tabulate>=0.9.0", # go/keep-sorted end + "a2a", + "google" ] docs = [