# 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