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

1299 lines
44 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.
import base64
import json
from unittest.mock import Mock
from unittest.mock import patch
from a2a import types as a2a_types
from google.adk.a2a import _compat
from google.adk.a2a.converters.part_converter import A2A_DATA_PART_END_TAG
from google.adk.a2a.converters.part_converter import A2A_DATA_PART_METADATA_TYPE_CODE_EXECUTION_RESULT
from google.adk.a2a.converters.part_converter import A2A_DATA_PART_METADATA_TYPE_EXECUTABLE_CODE
from google.adk.a2a.converters.part_converter import A2A_DATA_PART_METADATA_TYPE_FUNCTION_CALL
from google.adk.a2a.converters.part_converter import A2A_DATA_PART_METADATA_TYPE_FUNCTION_RESPONSE
from google.adk.a2a.converters.part_converter import A2A_DATA_PART_METADATA_TYPE_KEY
from google.adk.a2a.converters.part_converter import A2A_DATA_PART_START_TAG
from google.adk.a2a.converters.part_converter import A2A_DATA_PART_TEXT_MIME_TYPE
from google.adk.a2a.converters.part_converter import convert_a2a_part_to_genai_part
from google.adk.a2a.converters.part_converter import convert_genai_part_to_a2a_part
from google.adk.a2a.converters.utils import _get_adk_metadata_key
from google.adk.utils.variant_utils import GoogleLLMVariant
from google.genai import types as genai_types
import pytest
def _normalize_numbers(value):
"""Recursively coerce ints to floats so int-vs-float compares are tolerant.
On a2a-sdk 1.x, structured data round-trips through a protobuf Struct, which
stores every number as a float. This helper lets assertions compare data dicts
regardless of whether numbers come back as ``int`` (0.3.x) or ``float`` (1.x).
"""
if isinstance(value, bool):
return value
if isinstance(value, int):
return float(value)
if isinstance(value, dict):
return {k: _normalize_numbers(v) for k, v in value.items()}
if isinstance(value, list):
return [_normalize_numbers(v) for v in value]
return value
class TestConvertA2aPartToGenaiPart:
"""Test cases for convert_a2a_part_to_genai_part function."""
def test_convert_text_part(self):
"""Test conversion of A2A TextPart to GenAI Part."""
# Arrange
a2a_part = _compat.make_text_part("Hello, world!")
# Act
result = convert_a2a_part_to_genai_part(a2a_part)
# Assert
assert result is not None
assert isinstance(result, genai_types.Part)
assert result.text == "Hello, world!"
def test_convert_file_part_with_uri(self):
"""Test conversion of A2A FilePart with URI to GenAI Part."""
# Arrange
a2a_part = _compat.make_file_part_with_uri(
uri="gs://bucket/file.txt",
mime_type="text/plain",
name="my_file.txt",
)
# Act
result = convert_a2a_part_to_genai_part(a2a_part)
# Assert
assert result is not None
assert isinstance(result, genai_types.Part)
assert result.file_data is not None
assert result.file_data.file_uri == "gs://bucket/file.txt"
assert result.file_data.mime_type == "text/plain"
assert result.file_data.display_name == "my_file.txt"
def test_convert_file_part_with_bytes(self):
"""Test conversion of A2A FilePart with bytes to GenAI Part."""
# Arrange
test_bytes = b"test file content"
a2a_part = _compat.make_file_part_with_bytes(
data=test_bytes,
mime_type="text/plain",
name="my_bytes.txt",
)
# Act
result = convert_a2a_part_to_genai_part(a2a_part)
# Assert
assert result is not None
assert isinstance(result, genai_types.Part)
assert result.inline_data is not None
# The converter decodes base64 back to original bytes
assert result.inline_data.data == test_bytes
assert result.inline_data.mime_type == "text/plain"
assert result.inline_data.display_name == "my_bytes.txt"
def test_convert_data_part_function_call(self):
"""Test conversion of A2A DataPart with function call metadata."""
# Arrange
function_call_data = {
"name": "test_function",
"args": {"param1": "value1", "param2": 42},
}
a2a_part = _compat.make_data_part(
data=function_call_data,
metadata={
_get_adk_metadata_key(
A2A_DATA_PART_METADATA_TYPE_KEY
): A2A_DATA_PART_METADATA_TYPE_FUNCTION_CALL,
"adk_type": A2A_DATA_PART_METADATA_TYPE_FUNCTION_CALL,
},
)
# Act
result = convert_a2a_part_to_genai_part(a2a_part)
# Assert
assert result is not None
assert isinstance(result, genai_types.Part)
assert result.function_call is not None
assert result.function_call.name == "test_function"
assert result.function_call.args == {"param1": "value1", "param2": 42}
def test_convert_data_part_function_response(self):
"""Test conversion of A2A DataPart with function response metadata."""
# Arrange
function_response_data = {
"name": "test_function",
"response": {"result": "success", "data": [1, 2, 3]},
}
a2a_part = _compat.make_data_part(
data=function_response_data,
metadata={
_get_adk_metadata_key(
A2A_DATA_PART_METADATA_TYPE_KEY
): A2A_DATA_PART_METADATA_TYPE_FUNCTION_RESPONSE,
"adk_type": A2A_DATA_PART_METADATA_TYPE_FUNCTION_RESPONSE,
},
)
# Act
result = convert_a2a_part_to_genai_part(a2a_part)
# Assert
assert result is not None
assert isinstance(result, genai_types.Part)
assert result.function_response is not None
assert result.function_response.name == "test_function"
assert result.function_response.response == {
"result": "success",
"data": [1, 2, 3],
}
@pytest.mark.parametrize(
"test_name, data, metadata",
[
(
"without_special_metadata",
{"key": "value", "number": 123},
{"other": "metadata"},
),
(
"no_metadata",
{"key": "value", "array": [1, 2, 3]},
None,
),
(
"complex_data",
{
"nested": {
"array": [1, 2, {"inner": "value"}],
"boolean": True,
"null_value": None,
},
"unicode": "Hello 世界 🌍",
},
None,
),
(
"empty_metadata",
{"key": "value"},
{},
),
],
)
def test_convert_data_part_to_inline_data(self, test_name, data, metadata):
"""Test conversion of A2A DataPart to GenAI inline_data Part."""
# Arrange
a2a_part = _compat.make_data_part(data=data, metadata=metadata)
# Act
result = convert_a2a_part_to_genai_part(a2a_part)
# Assert
assert result is not None
assert isinstance(result, genai_types.Part)
assert result.inline_data is not None
assert result.inline_data.mime_type == A2A_DATA_PART_TEXT_MIME_TYPE
assert result.inline_data.data.startswith(A2A_DATA_PART_START_TAG)
assert result.inline_data.data.endswith(A2A_DATA_PART_END_TAG)
# The embedded payload is the serialized data part; parse it directly so
# the assertion is version-agnostic. The shapes differ between SDKs:
# 0.3.x: the whole DataPart is serialized -> {"data": ..., "metadata":
# ..., "kind": "data"}.
# 1.x: only the structured ``data`` dict is serialized; metadata is
# carried on the genai part (``part_metadata``) instead.
embedded = result.inline_data.data[
len(A2A_DATA_PART_START_TAG) : -len(A2A_DATA_PART_END_TAG)
]
converted_data_part = json.loads(embedded)
if _compat.IS_A2A_V1:
embedded_data = converted_data_part
else:
embedded_data = converted_data_part["data"]
# ``metadata`` may be omitted, None, or empty depending on the input;
# treat all empty forms as equivalent.
actual_metadata = converted_data_part.get("metadata") or None
expected_metadata = _normalize_numbers(metadata) if metadata else None
assert _normalize_numbers(actual_metadata) == expected_metadata
# On 1.x protobuf Struct stores all numbers as floats; normalize both
# sides so int-vs-float differences don't fail the comparison.
assert _normalize_numbers(embedded_data) == _normalize_numbers(data)
@pytest.mark.skipif(_compat.IS_A2A_V1, reason="0.3-only .root dispatch")
def test_convert_unsupported_file_type(self):
"""Test handling of unsupported file types."""
# Arrange - Create a mock unsupported file type
class UnsupportedFileType:
pass
# Create a part manually since FilePart validation might reject it
mock_file_part = Mock()
mock_file_part.file = UnsupportedFileType()
a2a_part = Mock()
a2a_part.root = mock_file_part
# Act
with patch(
"google.adk.a2a.converters.part_converter.logger"
) as mock_logger:
result = convert_a2a_part_to_genai_part(a2a_part)
# Assert
assert result is None
mock_logger.warning.assert_called_once()
@pytest.mark.skipif(_compat.IS_A2A_V1, reason="0.3-only .root dispatch")
def test_convert_unsupported_part_type(self):
"""Test handling of unsupported part types."""
# Arrange - Create a mock unsupported part type
class UnsupportedPartType:
pass
mock_part = Mock()
mock_part.root = UnsupportedPartType()
# Act
with patch(
"google.adk.a2a.converters.part_converter.logger"
) as mock_logger:
result = convert_a2a_part_to_genai_part(mock_part)
# Assert
assert result is None
mock_logger.warning.assert_called_once()
class TestConvertA2aPartToGenaiPartApiVariant:
"""Tests for part_metadata suppression based on api_variant (Vertex AI)."""
def _text_part_with_metadata(self):
part = _compat.make_text_part("hello")
_compat.set_part_metadata(
part,
{
_get_adk_metadata_key("thought"): True,
"custom": "value",
},
)
return part
def test_text_part_metadata_suppressed_in_vertex_mode(self):
"""In Vertex AI mode, part_metadata must be None to avoid SDK ValueError."""
a2a_part = self._text_part_with_metadata()
with patch(
"google.adk.a2a.converters.part_converter.get_google_llm_variant",
return_value=GoogleLLMVariant.VERTEX_AI,
):
result = convert_a2a_part_to_genai_part(a2a_part)
assert result is not None
assert result.part_metadata is None
# Native fields are still populated from the metadata.
assert result.text == "hello"
assert result.thought is True
def test_text_part_metadata_preserved_in_gemini_api_mode(self):
"""In Gemini Developer API mode, part_metadata is preserved."""
a2a_part = self._text_part_with_metadata()
with patch(
"google.adk.a2a.converters.part_converter.get_google_llm_variant",
return_value=GoogleLLMVariant.GEMINI_API,
):
result = convert_a2a_part_to_genai_part(a2a_part)
assert result is not None
assert result.part_metadata == {
_get_adk_metadata_key("thought"): True,
"custom": "value",
}
def test_function_call_metadata_suppressed_in_vertex_mode(self):
"""Function call data parts also suppress part_metadata in Vertex mode."""
a2a_part = _compat.make_data_part(
data={"name": "my_func", "args": {"x": 1}},
metadata={
_get_adk_metadata_key(
A2A_DATA_PART_METADATA_TYPE_KEY
): A2A_DATA_PART_METADATA_TYPE_FUNCTION_CALL,
"custom": "value",
},
)
with patch(
"google.adk.a2a.converters.part_converter.get_google_llm_variant",
return_value=GoogleLLMVariant.VERTEX_AI,
):
result = convert_a2a_part_to_genai_part(a2a_part)
assert result is not None
assert result.function_call is not None
assert result.part_metadata is None
def test_function_response_metadata_suppressed_in_vertex_mode(self):
"""Function response data parts suppress part_metadata in Vertex mode."""
a2a_part = _compat.make_data_part(
data={"name": "my_func", "response": {"ok": True}},
metadata={
_get_adk_metadata_key(
A2A_DATA_PART_METADATA_TYPE_KEY
): A2A_DATA_PART_METADATA_TYPE_FUNCTION_RESPONSE,
"custom": "value",
},
)
with patch(
"google.adk.a2a.converters.part_converter.get_google_llm_variant",
return_value=GoogleLLMVariant.VERTEX_AI,
):
result = convert_a2a_part_to_genai_part(a2a_part)
assert result is not None
assert result.function_response is not None
assert result.part_metadata is None
def test_file_with_uri_metadata_suppressed_in_vertex_mode(self):
"""File parts suppress part_metadata in Vertex mode."""
a2a_part = _compat.make_file_part_with_uri(
uri="gs://bucket/file.txt",
mime_type="text/plain",
name="my_file.txt",
)
_compat.set_part_metadata(a2a_part, {"custom": "value"})
with patch(
"google.adk.a2a.converters.part_converter.get_google_llm_variant",
return_value=GoogleLLMVariant.VERTEX_AI,
):
result = convert_a2a_part_to_genai_part(a2a_part)
assert result is not None
assert result.file_data is not None
assert result.part_metadata is None
def test_api_variant_resolved_from_env(self):
"""The api variant is resolved via get_google_llm_variant."""
a2a_part = self._text_part_with_metadata()
with patch(
"google.adk.a2a.converters.part_converter.get_google_llm_variant",
return_value=GoogleLLMVariant.VERTEX_AI,
) as mock_get_variant:
result = convert_a2a_part_to_genai_part(a2a_part)
mock_get_variant.assert_called_once()
assert result is not None
assert result.part_metadata is None
class TestConvertGenaiPartToA2aPart:
"""Test cases for convert_genai_part_to_a2a_part function."""
def test_convert_text_part(self):
"""Test conversion of GenAI text Part to A2A Part."""
# Arrange
genai_part = genai_types.Part(text="Hello, world!")
# Act
result = convert_genai_part_to_a2a_part(genai_part)
# Assert
assert result is not None
assert isinstance(result, a2a_types.Part)
assert _compat.is_text_part(result)
assert _compat.part_text(result) == "Hello, world!"
def test_convert_text_part_with_thought(self):
"""Test conversion of GenAI text Part with thought to A2A Part."""
# Arrange - thought is a boolean field in genai_types.Part
genai_part = genai_types.Part(text="Hello, world!", thought=True)
# Act
result = convert_genai_part_to_a2a_part(genai_part)
# Assert
assert result is not None
assert isinstance(result, a2a_types.Part)
assert _compat.is_text_part(result)
assert _compat.part_text(result) == "Hello, world!"
metadata = _compat.part_metadata(result)
assert metadata
assert metadata[_get_adk_metadata_key("thought")]
def test_convert_empty_text_part(self):
"""Test that Part(text='') is preserved, not dropped.
Regression test for #5341: empty-string text parts are valid and
must not fall through to the unsupported-part warning.
"""
# Arrange
genai_part = genai_types.Part(text="")
# Act
result = convert_genai_part_to_a2a_part(genai_part)
# Assert — should produce a valid TextPart, not None
assert result is not None
assert _compat.is_text_part(result)
assert _compat.part_text(result) == ""
def test_convert_file_data_part(self):
"""Test conversion of GenAI file_data Part to A2A Part."""
# Arrange
genai_part = genai_types.Part(
file_data=genai_types.FileData(
file_uri="gs://bucket/file.txt",
mime_type="text/plain",
display_name="my_file.txt",
)
)
# Act
result = convert_genai_part_to_a2a_part(genai_part)
# Assert
assert result is not None
assert isinstance(result, a2a_types.Part)
assert _compat.is_file_part(result)
assert _compat.file_part_uri(result) == "gs://bucket/file.txt"
assert _compat.file_part_mime_type(result) == "text/plain"
assert _compat.file_part_name(result) == "my_file.txt"
def test_convert_inline_data_part(self):
"""Test conversion of GenAI inline_data Part to A2A Part."""
# Arrange
test_bytes = b"test file content"
genai_part = genai_types.Part(
inline_data=genai_types.Blob(
data=test_bytes,
mime_type="text/plain",
display_name="my_bytes.txt",
)
)
# Act
result = convert_genai_part_to_a2a_part(genai_part)
# Assert
assert result is not None
assert isinstance(result, a2a_types.Part)
assert _compat.is_file_part(result)
# The version-agnostic reader returns the raw (decoded) bytes on both SDKs.
assert _compat.file_part_bytes(result) == test_bytes
assert _compat.file_part_mime_type(result) == "text/plain"
# Filename is preserved on both SDKs.
assert _compat.file_part_name(result) == "my_bytes.txt"
def test_convert_inline_data_part_empty_blob_is_skipped(self):
"""A degenerate inline_data Blob with no payload is unconvertible (None)."""
# Arrange: an empty Blob has ``data is None``.
genai_part = genai_types.Part(inline_data=genai_types.Blob())
# Act
result = convert_genai_part_to_a2a_part(genai_part)
# Assert
assert result is None
def test_convert_inline_data_part_empty_bytes_is_kept(self):
"""A Blob with present-but-empty bytes (b"") is still converted."""
# Arrange
genai_part = genai_types.Part(
inline_data=genai_types.Blob(data=b"", mime_type="text/plain")
)
# Act
result = convert_genai_part_to_a2a_part(genai_part)
# Assert
assert result is not None
assert _compat.is_file_part(result)
assert _compat.file_part_bytes(result) == b""
assert _compat.file_part_mime_type(result) == "text/plain"
def test_convert_inline_data_part_with_video_metadata(self):
"""Test conversion of GenAI inline_data Part with video metadata to A2A Part."""
# Arrange
test_bytes = b"test video content"
video_metadata = genai_types.VideoMetadata(fps=30.0)
genai_part = genai_types.Part(
inline_data=genai_types.Blob(data=test_bytes, mime_type="video/mp4"),
video_metadata=video_metadata,
)
# Act
result = convert_genai_part_to_a2a_part(genai_part)
# Assert
assert result is not None
assert isinstance(result, a2a_types.Part)
assert _compat.is_file_part(result)
metadata = _compat.part_metadata(result)
assert metadata
assert _get_adk_metadata_key("video_metadata") in metadata
def test_convert_inline_data_part_to_data_part(self):
"""Test conversion of GenAI inline_data Part to A2A DataPart."""
# Arrange
data = {"key": "value"}
metadata = {"meta": "data"}
# The embedded-payload shape and metadata channel differ between SDKs:
# 0.3.x: the embedded JSON is the full DataPart ({"data", "metadata"}).
# 1.x: the embedded JSON is just the structured ``data`` dict and the
# metadata travels on the genai part's ``part_metadata``.
if _compat.IS_A2A_V1:
json_data = json.dumps(data).encode("utf-8")
genai_part = genai_types.Part(
inline_data=genai_types.Blob(
data=A2A_DATA_PART_START_TAG + json_data + A2A_DATA_PART_END_TAG,
mime_type=A2A_DATA_PART_TEXT_MIME_TYPE,
),
part_metadata=metadata,
)
else:
json_data = json.dumps({"data": data, "metadata": metadata}).encode(
"utf-8"
)
genai_part = genai_types.Part(
inline_data=genai_types.Blob(
data=A2A_DATA_PART_START_TAG + json_data + A2A_DATA_PART_END_TAG,
mime_type=A2A_DATA_PART_TEXT_MIME_TYPE,
)
)
# Act
result = convert_genai_part_to_a2a_part(genai_part)
# Assert
assert result is not None
assert isinstance(result, a2a_types.Part)
assert _compat.is_data_part(result)
assert _compat.data_part_dict(result) == data
assert _compat.part_metadata(result) == metadata
def test_convert_function_call_part(self):
"""Test conversion of GenAI function_call Part to A2A Part."""
# Arrange
function_call = genai_types.FunctionCall(
name="test_function", args={"param1": "value1", "param2": 42}
)
genai_part = genai_types.Part(function_call=function_call)
# Act
result = convert_genai_part_to_a2a_part(genai_part)
# Assert
assert result is not None
assert isinstance(result, a2a_types.Part)
assert _compat.is_data_part(result)
expected_data = function_call.model_dump(by_alias=True, exclude_none=True)
assert _normalize_numbers(
_compat.data_part_dict(result)
) == _normalize_numbers(expected_data)
assert (
_compat.part_metadata(result)[
_get_adk_metadata_key(A2A_DATA_PART_METADATA_TYPE_KEY)
]
== A2A_DATA_PART_METADATA_TYPE_FUNCTION_CALL
)
def test_convert_function_response_part(self):
"""Test conversion of GenAI function_response Part to A2A Part."""
# Arrange
function_response = genai_types.FunctionResponse(
name="test_function", response={"result": "success", "data": [1, 2, 3]}
)
genai_part = genai_types.Part(function_response=function_response)
# Act
result = convert_genai_part_to_a2a_part(genai_part)
# Assert
assert result is not None
assert isinstance(result, a2a_types.Part)
assert _compat.is_data_part(result)
expected_data = function_response.model_dump(
by_alias=True, exclude_none=True
)
assert _normalize_numbers(
_compat.data_part_dict(result)
) == _normalize_numbers(expected_data)
assert (
_compat.part_metadata(result)[
_get_adk_metadata_key(A2A_DATA_PART_METADATA_TYPE_KEY)
]
== A2A_DATA_PART_METADATA_TYPE_FUNCTION_RESPONSE
)
def test_convert_code_execution_result_part(self):
"""Test conversion of GenAI code_execution_result Part to A2A Part."""
# Arrange
code_execution_result = genai_types.CodeExecutionResult(
outcome=genai_types.Outcome.OUTCOME_OK, output="Hello, World!"
)
genai_part = genai_types.Part(code_execution_result=code_execution_result)
# Act
result = convert_genai_part_to_a2a_part(genai_part)
# Assert
assert result is not None
assert isinstance(result, a2a_types.Part)
assert _compat.is_data_part(result)
expected_data = code_execution_result.model_dump(
by_alias=True, exclude_none=True
)
assert _normalize_numbers(
_compat.data_part_dict(result)
) == _normalize_numbers(expected_data)
assert (
_compat.part_metadata(result)[
_get_adk_metadata_key(A2A_DATA_PART_METADATA_TYPE_KEY)
]
== A2A_DATA_PART_METADATA_TYPE_CODE_EXECUTION_RESULT
)
def test_convert_executable_code_part(self):
"""Test conversion of GenAI executable_code Part to A2A Part."""
# Arrange
executable_code = genai_types.ExecutableCode(
language=genai_types.Language.PYTHON, code="print('Hello, World!')"
)
genai_part = genai_types.Part(executable_code=executable_code)
# Act
result = convert_genai_part_to_a2a_part(genai_part)
# Assert
assert result is not None
assert isinstance(result, a2a_types.Part)
assert _compat.is_data_part(result)
expected_data = executable_code.model_dump(by_alias=True, exclude_none=True)
assert _normalize_numbers(
_compat.data_part_dict(result)
) == _normalize_numbers(expected_data)
assert (
_compat.part_metadata(result)[
_get_adk_metadata_key(A2A_DATA_PART_METADATA_TYPE_KEY)
]
== A2A_DATA_PART_METADATA_TYPE_EXECUTABLE_CODE
)
def test_convert_unsupported_part(self):
"""Test handling of unsupported GenAI Part types."""
# Arrange - Create a GenAI Part with no recognized fields
genai_part = genai_types.Part()
# Act
with patch(
"google.adk.a2a.converters.part_converter.logger"
) as mock_logger:
result = convert_genai_part_to_a2a_part(genai_part)
# Assert
assert result is None
mock_logger.warning.assert_called_once()
class TestRoundTripConversions:
"""Test cases for round-trip conversions to ensure consistency."""
def test_text_part_round_trip(self):
"""Test round-trip conversion for text parts."""
# Arrange
original_text = "Hello, world!"
a2a_part = _compat.make_text_part(original_text)
# Act
genai_part = convert_a2a_part_to_genai_part(a2a_part)
result_a2a_part = convert_genai_part_to_a2a_part(genai_part)
# Assert
assert result_a2a_part is not None
assert isinstance(result_a2a_part, a2a_types.Part)
assert _compat.is_text_part(result_a2a_part)
assert _compat.part_text(result_a2a_part) == original_text
def test_text_part_with_thought_round_trip(self):
"""Test round-trip conversion for text parts with thought."""
# Arrange
original_text = "Thinking..."
genai_part = genai_types.Part(text=original_text, thought=True)
# Act
a2a_part = convert_genai_part_to_a2a_part(genai_part)
result_genai_part = convert_a2a_part_to_genai_part(a2a_part)
# Assert
assert result_genai_part is not None
assert isinstance(result_genai_part, genai_types.Part)
assert result_genai_part.text == original_text
assert result_genai_part.thought
def test_file_uri_round_trip(self):
"""Test round-trip conversion for file parts with URI."""
# Arrange
original_uri = "gs://bucket/file.txt"
original_mime_type = "text/plain"
a2a_part = _compat.make_file_part_with_uri(
uri=original_uri, mime_type=original_mime_type
)
# Act
genai_part = convert_a2a_part_to_genai_part(a2a_part)
result_a2a_part = convert_genai_part_to_a2a_part(genai_part)
# Assert
assert result_a2a_part is not None
assert isinstance(result_a2a_part, a2a_types.Part)
assert _compat.is_file_part(result_a2a_part)
assert _compat.file_part_uri(result_a2a_part) == original_uri
assert _compat.file_part_mime_type(result_a2a_part) == original_mime_type
def test_file_bytes_round_trip(self):
"""Test round-trip conversion for file parts with bytes."""
# Arrange
original_bytes = b"test file content for round trip"
original_mime_type = "application/octet-stream"
# Start with GenAI part (the more common starting point)
genai_part = genai_types.Part(
inline_data=genai_types.Blob(
data=original_bytes, mime_type=original_mime_type
)
)
# Act - Round trip: GenAI -> A2A -> GenAI
a2a_part = convert_genai_part_to_a2a_part(genai_part)
result_genai_part = convert_a2a_part_to_genai_part(a2a_part)
# Assert
assert result_genai_part is not None
assert isinstance(result_genai_part, genai_types.Part)
assert result_genai_part.inline_data is not None
assert result_genai_part.inline_data.data == original_bytes
assert result_genai_part.inline_data.mime_type == original_mime_type
def test_function_call_round_trip(self):
"""Test round-trip conversion for function call parts."""
# Arrange
function_call = genai_types.FunctionCall(
name="test_function", args={"param1": "value1", "param2": 42}
)
genai_part = genai_types.Part(function_call=function_call)
# Act - Round trip: GenAI -> A2A -> GenAI
a2a_part = convert_genai_part_to_a2a_part(genai_part)
result_genai_part = convert_a2a_part_to_genai_part(a2a_part)
# Assert
assert result_genai_part is not None
assert isinstance(result_genai_part, genai_types.Part)
assert result_genai_part.function_call is not None
assert result_genai_part.function_call.name == function_call.name
assert result_genai_part.function_call.args == function_call.args
def test_function_response_round_trip(self):
"""Test round-trip conversion for function response parts."""
# Arrange
function_response = genai_types.FunctionResponse(
name="test_function", response={"result": "success", "data": [1, 2, 3]}
)
genai_part = genai_types.Part(function_response=function_response)
# Act - Round trip: GenAI -> A2A -> GenAI
a2a_part = convert_genai_part_to_a2a_part(genai_part)
result_genai_part = convert_a2a_part_to_genai_part(a2a_part)
# Assert
assert result_genai_part is not None
assert isinstance(result_genai_part, genai_types.Part)
assert result_genai_part.function_response is not None
assert result_genai_part.function_response.name == function_response.name
assert (
result_genai_part.function_response.response
== function_response.response
)
def test_code_execution_result_round_trip(self):
"""Test round-trip conversion for code execution result parts."""
# Arrange
code_execution_result = genai_types.CodeExecutionResult(
outcome=genai_types.Outcome.OUTCOME_OK, output="Hello, World!"
)
genai_part = genai_types.Part(code_execution_result=code_execution_result)
# Act - Round trip: GenAI -> A2A -> GenAI
a2a_part = convert_genai_part_to_a2a_part(genai_part)
result_genai_part = convert_a2a_part_to_genai_part(a2a_part)
# Assert
assert result_genai_part is not None
assert isinstance(result_genai_part, genai_types.Part)
assert result_genai_part.code_execution_result is not None
assert (
result_genai_part.code_execution_result.outcome
== code_execution_result.outcome
)
assert (
result_genai_part.code_execution_result.output
== code_execution_result.output
)
def test_executable_code_round_trip(self):
"""Test round-trip conversion for executable code parts."""
# Arrange
executable_code = genai_types.ExecutableCode(
language=genai_types.Language.PYTHON, code="print('Hello, World!')"
)
genai_part = genai_types.Part(executable_code=executable_code)
# Act - Round trip: GenAI -> A2A -> GenAI
a2a_part = convert_genai_part_to_a2a_part(genai_part)
result_genai_part = convert_a2a_part_to_genai_part(a2a_part)
# Assert
assert result_genai_part is not None
assert isinstance(result_genai_part, genai_types.Part)
assert result_genai_part.executable_code is not None
assert (
result_genai_part.executable_code.language == executable_code.language
)
assert result_genai_part.executable_code.code == executable_code.code
def test_data_part_round_trip(self):
"""Test round-trip conversion for data parts."""
# Arrange
data = {"key": "value"}
metadata = {"meta": "data"}
a2a_part = _compat.make_data_part(data=data, metadata=metadata)
# Act
genai_part = convert_a2a_part_to_genai_part(a2a_part)
result_a2a_part = convert_genai_part_to_a2a_part(genai_part)
# Assert
assert result_a2a_part is not None
assert isinstance(result_a2a_part, a2a_types.Part)
assert _compat.is_data_part(result_a2a_part)
assert _normalize_numbers(
_compat.data_part_dict(result_a2a_part)
) == _normalize_numbers(data)
assert _compat.part_metadata(result_a2a_part) == metadata
def test_data_part_with_mime_type_metadata_round_trip(self):
"""Test round-trip conversion for data parts with 'mime_type' in metadata."""
# Arrange
data = {"content": "some data"}
metadata = {"meta": "data", "mime_type": "application/json"}
a2a_part = _compat.make_data_part(data=data, metadata=metadata)
# Act
genai_part = convert_a2a_part_to_genai_part(a2a_part)
result_a2a_part = convert_genai_part_to_a2a_part(genai_part)
# Assert
assert result_a2a_part is not None
assert isinstance(result_a2a_part, a2a_types.Part)
assert _compat.is_data_part(result_a2a_part)
assert _normalize_numbers(
_compat.data_part_dict(result_a2a_part)
) == _normalize_numbers(data)
# The 'mime_type' key in the metadata should be preserved as is
assert _compat.part_metadata(result_a2a_part) == metadata
def test_text_part_metadata_round_trip(self):
"""Test round-trip conversion for text parts with metadata."""
# Arrange
metadata = {"key1": "value1", "key2": "value2"}
a2a_part = _compat.make_text_part("some text")
_compat.set_part_metadata(a2a_part, metadata)
# Act
genai_part = convert_a2a_part_to_genai_part(a2a_part)
result_a2a_part = convert_genai_part_to_a2a_part(genai_part)
# Assert
assert result_a2a_part is not None
assert isinstance(result_a2a_part, a2a_types.Part)
assert _compat.is_text_part(result_a2a_part)
assert _compat.part_text(result_a2a_part) == "some text"
assert _compat.part_metadata(result_a2a_part) == metadata
def test_file_part_metadata_round_trip(self):
"""Test round-trip conversion for file parts with metadata."""
# Arrange
metadata = {"key1": "value1"}
a2a_part = _compat.make_file_part_with_uri(
uri="gs://bucket/file.txt",
mime_type="text/plain",
name="my_file.txt",
)
_compat.set_part_metadata(a2a_part, metadata)
# Act
genai_part = convert_a2a_part_to_genai_part(a2a_part)
result_a2a_part = convert_genai_part_to_a2a_part(genai_part)
# Assert
assert result_a2a_part is not None
assert isinstance(result_a2a_part, a2a_types.Part)
assert _compat.is_file_part(result_a2a_part)
assert _compat.file_part_uri(result_a2a_part) == "gs://bucket/file.txt"
assert _compat.part_metadata(result_a2a_part) == metadata
class TestEdgeCases:
"""Test cases for edge cases and error conditions."""
def test_empty_text_part(self):
"""Test conversion of empty text part."""
# Arrange
a2a_part = _compat.make_text_part("")
# Act
result = convert_a2a_part_to_genai_part(a2a_part)
# Assert
assert result is not None
assert result.text == ""
def test_genai_inline_data_with_mimetype_to_a2a(self):
"""Test conversion of GenAI inline_data with 'mimeType' in DataPart metadata to A2A.
This tests if 'mimeType' in metadata of a DataPart wrapped in inline_data
is correctly handled, ensuring the key casing is preserved.
"""
# Arrange
data = {"key": "value"}
metadata = {"adk_type": "some_type", "mimeType": "image/png"}
# The embedded-payload shape and metadata channel differ between SDKs.
if _compat.IS_A2A_V1:
json_data = json.dumps(data).encode("utf-8")
genai_part = genai_types.Part(
inline_data=genai_types.Blob(
data=A2A_DATA_PART_START_TAG + json_data + A2A_DATA_PART_END_TAG,
mime_type=A2A_DATA_PART_TEXT_MIME_TYPE,
),
part_metadata=metadata,
)
else:
json_data = json.dumps({"data": data, "metadata": metadata}).encode(
"utf-8"
)
genai_part = genai_types.Part(
inline_data=genai_types.Blob(
data=A2A_DATA_PART_START_TAG + json_data + A2A_DATA_PART_END_TAG,
mime_type=A2A_DATA_PART_TEXT_MIME_TYPE,
)
)
# Act
result = convert_genai_part_to_a2a_part(genai_part)
# Assert
assert result is not None
assert isinstance(result, a2a_types.Part)
assert _compat.is_data_part(result)
assert _compat.data_part_dict(result) == data
# The key casing should be preserved from the JSON
assert _compat.part_metadata(result) == metadata
def test_none_input_a2a_to_genai(self):
"""Test handling of None input for A2A to GenAI conversion."""
# This test depends on how the function handles None input
# If it should raise an exception, we test for that
with pytest.raises(AttributeError):
convert_a2a_part_to_genai_part(None)
def test_none_input_genai_to_a2a(self):
"""Test handling of None input for GenAI to A2A conversion."""
# This test depends on how the function handles None input
# If it should raise an exception, we test for that
with pytest.raises(AttributeError):
convert_genai_part_to_a2a_part(None)
class TestNewConstants:
"""Test cases for new constants and functionality."""
def test_new_constants_exist(self):
"""Test that new constants are defined."""
assert (
A2A_DATA_PART_METADATA_TYPE_CODE_EXECUTION_RESULT
== "code_execution_result"
)
assert A2A_DATA_PART_METADATA_TYPE_EXECUTABLE_CODE == "executable_code"
def test_convert_a2a_data_part_with_code_execution_result_metadata(self):
"""Test conversion of A2A DataPart with code execution result metadata."""
# Arrange
code_execution_result_data = {
"outcome": "OUTCOME_OK",
"output": "Hello, World!",
}
a2a_part = _compat.make_data_part(
data=code_execution_result_data,
metadata={
_get_adk_metadata_key(
A2A_DATA_PART_METADATA_TYPE_KEY
): A2A_DATA_PART_METADATA_TYPE_CODE_EXECUTION_RESULT,
},
)
# Act
result = convert_a2a_part_to_genai_part(a2a_part)
# Assert
assert result is not None
assert isinstance(result, genai_types.Part)
# Now it should convert back to a proper CodeExecutionResult
assert result.code_execution_result is not None
assert (
result.code_execution_result.outcome == genai_types.Outcome.OUTCOME_OK
)
assert result.code_execution_result.output == "Hello, World!"
def test_convert_a2a_data_part_with_executable_code_metadata(self):
"""Test conversion of A2A DataPart with executable code metadata."""
# Arrange
executable_code_data = {
"language": "PYTHON",
"code": "print('Hello, World!')",
}
a2a_part = _compat.make_data_part(
data=executable_code_data,
metadata={
_get_adk_metadata_key(
A2A_DATA_PART_METADATA_TYPE_KEY
): A2A_DATA_PART_METADATA_TYPE_EXECUTABLE_CODE,
},
)
# Act
result = convert_a2a_part_to_genai_part(a2a_part)
# Assert
assert result is not None
assert isinstance(result, genai_types.Part)
# Now it should convert back to a proper ExecutableCode
assert result.executable_code is not None
assert result.executable_code.language == genai_types.Language.PYTHON
assert result.executable_code.code == "print('Hello, World!')"
class TestThoughtSignaturePreservation:
"""Tests for thought_signature preservation in function call conversions."""
def test_genai_function_call_with_thought_signature_to_a2a(self):
"""Test that thought_signature is preserved when converting GenAI to A2A."""
# Arrange
function_call = genai_types.FunctionCall(
id="fc_gemini3",
name="my_tool",
args={"document": "test content"},
)
genai_part = genai_types.Part(
function_call=function_call,
thought_signature=b"gemini3_signature_bytes",
)
# Act
result = convert_genai_part_to_a2a_part(genai_part)
# Assert
assert result is not None
assert _compat.is_data_part(result)
metadata = _compat.part_metadata(result)
assert (
metadata[_get_adk_metadata_key(A2A_DATA_PART_METADATA_TYPE_KEY)]
== A2A_DATA_PART_METADATA_TYPE_FUNCTION_CALL
)
# thought_signature should be base64 encoded in metadata
thought_sig_key = _get_adk_metadata_key("thought_signature")
assert thought_sig_key in metadata
assert (
base64.b64decode(metadata[thought_sig_key])
== b"gemini3_signature_bytes"
)
def test_genai_function_call_without_thought_signature_to_a2a(self):
"""Test function call without thought_signature doesn't add metadata key."""
# Arrange
function_call = genai_types.FunctionCall(
id="fc_regular",
name="regular_tool",
args={},
)
genai_part = genai_types.Part(function_call=function_call)
# Act
result = convert_genai_part_to_a2a_part(genai_part)
# Assert
assert result is not None
assert _compat.is_data_part(result)
# thought_signature key should not be present
thought_sig_key = _get_adk_metadata_key("thought_signature")
assert thought_sig_key not in _compat.part_metadata(result)
def test_a2a_function_call_with_thought_signature_to_genai(self):
"""Test that thought_signature is restored when converting A2A to GenAI."""
# Arrange
a2a_part = _compat.make_data_part(
data={
"id": "fc_gemini3",
"name": "my_tool",
"args": {"document": "test content"},
},
metadata={
_get_adk_metadata_key(
A2A_DATA_PART_METADATA_TYPE_KEY
): A2A_DATA_PART_METADATA_TYPE_FUNCTION_CALL,
_get_adk_metadata_key("thought_signature"): (
base64.b64encode(b"restored_signature").decode("utf-8")
),
},
)
# Act
result = convert_a2a_part_to_genai_part(a2a_part)
# Assert
assert result is not None
assert result.function_call is not None
assert result.function_call.name == "my_tool"
# thought_signature should be decoded back to bytes
assert result.thought_signature == b"restored_signature"
def test_a2a_function_call_without_thought_signature_to_genai(self):
"""Test function call without thought_signature returns None for it."""
# Arrange
a2a_part = _compat.make_data_part(
data={
"id": "fc_regular",
"name": "regular_tool",
"args": {},
},
metadata={
_get_adk_metadata_key(
A2A_DATA_PART_METADATA_TYPE_KEY
): A2A_DATA_PART_METADATA_TYPE_FUNCTION_CALL,
},
)
# Act
result = convert_a2a_part_to_genai_part(a2a_part)
# Assert
assert result is not None
assert result.function_call is not None
assert result.function_call.name == "regular_tool"
# thought_signature should be None
assert result.thought_signature is None
def test_function_call_with_thought_signature_round_trip(self):
"""Test thought_signature is preserved in GenAI -> A2A -> GenAI round trip."""
# Arrange
original_signature = b"round_trip_signature_test"
function_call = genai_types.FunctionCall(
id="fc_round_trip",
name="round_trip_tool",
args={"key": "value"},
)
original_part = genai_types.Part(
function_call=function_call,
thought_signature=original_signature,
)
# Act - Convert GenAI -> A2A -> GenAI
a2a_part = convert_genai_part_to_a2a_part(original_part)
restored_part = convert_a2a_part_to_genai_part(a2a_part)
# Assert
assert restored_part is not None
assert restored_part.function_call is not None
assert restored_part.function_call.name == "round_trip_tool"
assert restored_part.thought_signature == original_signature
@pytest.mark.skipif(
_compat.IS_A2A_V1,
reason=(
"0.3-only: 1.x metadata is a proto Struct that cannot hold raw bytes"
),
)
def test_a2a_function_call_with_bytes_thought_signature_to_genai(self):
"""Test that bytes thought_signature is used directly without decoding."""
# Arrange - metadata contains raw bytes (not base64 encoded)
a2a_part = _compat.make_data_part(
data={
"id": "fc_bytes",
"name": "bytes_tool",
"args": {},
},
metadata={
_get_adk_metadata_key(
A2A_DATA_PART_METADATA_TYPE_KEY
): A2A_DATA_PART_METADATA_TYPE_FUNCTION_CALL,
_get_adk_metadata_key("thought_signature"): b"raw_bytes_signature",
},
)
# Act
result = convert_a2a_part_to_genai_part(a2a_part)
# Assert
assert result is not None
assert result.function_call is not None
# bytes should be used directly
assert result.thought_signature == b"raw_bytes_signature"
def test_a2a_function_call_with_invalid_base64_thought_signature(self):
"""Test that invalid base64 thought_signature logs warning and returns None."""
# Arrange - metadata contains invalid base64 string
a2a_part = _compat.make_data_part(
data={
"id": "fc_invalid",
"name": "invalid_sig_tool",
"args": {},
},
metadata={
_get_adk_metadata_key(
A2A_DATA_PART_METADATA_TYPE_KEY
): A2A_DATA_PART_METADATA_TYPE_FUNCTION_CALL,
_get_adk_metadata_key("thought_signature"): "not_valid_base64!!!",
},
)
# Act
result = convert_a2a_part_to_genai_part(a2a_part)
# Assert
assert result is not None
assert result.function_call is not None
assert result.function_call.name == "invalid_sig_tool"
# thought_signature should be None due to decode failure
assert result.thought_signature is None