|
12 | 12 | # See the License for the specific language governing permissions and |
13 | 13 | # limitations under the License. |
14 | 14 |
|
| 15 | +import base64 |
15 | 16 | import json |
16 | 17 | from unittest.mock import Mock |
17 | 18 | from unittest.mock import patch |
@@ -74,7 +75,6 @@ def test_convert_file_part_with_bytes(self): |
74 | 75 | # Arrange |
75 | 76 | test_bytes = b"test file content" |
76 | 77 | # A2A FileWithBytes expects base64-encoded string |
77 | | - import base64 |
78 | 78 |
|
79 | 79 | base64_encoded = base64.b64encode(test_bytes).decode("utf-8") |
80 | 80 | a2a_part = a2a_types.Part( |
@@ -328,7 +328,6 @@ def test_convert_inline_data_part(self): |
328 | 328 | assert isinstance(result.root, a2a_types.FilePart) |
329 | 329 | assert isinstance(result.root.file, a2a_types.FileWithBytes) |
330 | 330 | # A2A FileWithBytes now stores base64-encoded bytes to ensure round-trip compatibility |
331 | | - import base64 |
332 | 331 |
|
333 | 332 | expected_base64 = base64.b64encode(test_bytes).decode("utf-8") |
334 | 333 | assert result.root.file.bytes == expected_base64 |
@@ -841,3 +840,204 @@ def test_convert_a2a_data_part_with_executable_code_metadata(self): |
841 | 840 | assert result.executable_code is not None |
842 | 841 | assert result.executable_code.language == genai_types.Language.PYTHON |
843 | 842 | assert result.executable_code.code == "print('Hello, World!')" |
| 843 | + |
| 844 | + |
| 845 | +class TestThoughtSignaturePreservation: |
| 846 | + """Tests for thought_signature preservation in function call conversions.""" |
| 847 | + |
| 848 | + def test_genai_function_call_with_thought_signature_to_a2a(self): |
| 849 | + """Test that thought_signature is preserved when converting GenAI to A2A.""" |
| 850 | + # Arrange |
| 851 | + function_call = genai_types.FunctionCall( |
| 852 | + id="fc_gemini3", |
| 853 | + name="my_tool", |
| 854 | + args={"document": "test content"}, |
| 855 | + ) |
| 856 | + genai_part = genai_types.Part( |
| 857 | + function_call=function_call, |
| 858 | + thought_signature=b"gemini3_signature_bytes", |
| 859 | + ) |
| 860 | + |
| 861 | + # Act |
| 862 | + result = convert_genai_part_to_a2a_part(genai_part) |
| 863 | + |
| 864 | + # Assert |
| 865 | + assert result is not None |
| 866 | + assert isinstance(result.root, a2a_types.DataPart) |
| 867 | + assert ( |
| 868 | + result.root.metadata[ |
| 869 | + _get_adk_metadata_key(A2A_DATA_PART_METADATA_TYPE_KEY) |
| 870 | + ] |
| 871 | + == A2A_DATA_PART_METADATA_TYPE_FUNCTION_CALL |
| 872 | + ) |
| 873 | + # thought_signature should be base64 encoded in metadata |
| 874 | + thought_sig_key = _get_adk_metadata_key("thought_signature") |
| 875 | + assert thought_sig_key in result.root.metadata |
| 876 | + assert ( |
| 877 | + base64.b64decode(result.root.metadata[thought_sig_key]) |
| 878 | + == b"gemini3_signature_bytes" |
| 879 | + ) |
| 880 | + |
| 881 | + def test_genai_function_call_without_thought_signature_to_a2a(self): |
| 882 | + """Test function call without thought_signature doesn't add metadata key.""" |
| 883 | + # Arrange |
| 884 | + function_call = genai_types.FunctionCall( |
| 885 | + id="fc_regular", |
| 886 | + name="regular_tool", |
| 887 | + args={}, |
| 888 | + ) |
| 889 | + genai_part = genai_types.Part(function_call=function_call) |
| 890 | + |
| 891 | + # Act |
| 892 | + result = convert_genai_part_to_a2a_part(genai_part) |
| 893 | + |
| 894 | + # Assert |
| 895 | + assert result is not None |
| 896 | + assert isinstance(result.root, a2a_types.DataPart) |
| 897 | + # thought_signature key should not be present |
| 898 | + thought_sig_key = _get_adk_metadata_key("thought_signature") |
| 899 | + assert thought_sig_key not in result.root.metadata |
| 900 | + |
| 901 | + def test_a2a_function_call_with_thought_signature_to_genai(self): |
| 902 | + """Test that thought_signature is restored when converting A2A to GenAI.""" |
| 903 | + # Arrange |
| 904 | + a2a_part = a2a_types.Part( |
| 905 | + root=a2a_types.DataPart( |
| 906 | + data={ |
| 907 | + "id": "fc_gemini3", |
| 908 | + "name": "my_tool", |
| 909 | + "args": {"document": "test content"}, |
| 910 | + }, |
| 911 | + metadata={ |
| 912 | + _get_adk_metadata_key( |
| 913 | + A2A_DATA_PART_METADATA_TYPE_KEY |
| 914 | + ): A2A_DATA_PART_METADATA_TYPE_FUNCTION_CALL, |
| 915 | + _get_adk_metadata_key("thought_signature"): ( |
| 916 | + base64.b64encode(b"restored_signature").decode("utf-8") |
| 917 | + ), |
| 918 | + }, |
| 919 | + ) |
| 920 | + ) |
| 921 | + |
| 922 | + # Act |
| 923 | + result = convert_a2a_part_to_genai_part(a2a_part) |
| 924 | + |
| 925 | + # Assert |
| 926 | + assert result is not None |
| 927 | + assert result.function_call is not None |
| 928 | + assert result.function_call.name == "my_tool" |
| 929 | + # thought_signature should be decoded back to bytes |
| 930 | + assert result.thought_signature == b"restored_signature" |
| 931 | + |
| 932 | + def test_a2a_function_call_without_thought_signature_to_genai(self): |
| 933 | + """Test function call without thought_signature returns None for it.""" |
| 934 | + # Arrange |
| 935 | + a2a_part = a2a_types.Part( |
| 936 | + root=a2a_types.DataPart( |
| 937 | + data={ |
| 938 | + "id": "fc_regular", |
| 939 | + "name": "regular_tool", |
| 940 | + "args": {}, |
| 941 | + }, |
| 942 | + metadata={ |
| 943 | + _get_adk_metadata_key( |
| 944 | + A2A_DATA_PART_METADATA_TYPE_KEY |
| 945 | + ): A2A_DATA_PART_METADATA_TYPE_FUNCTION_CALL, |
| 946 | + }, |
| 947 | + ) |
| 948 | + ) |
| 949 | + |
| 950 | + # Act |
| 951 | + result = convert_a2a_part_to_genai_part(a2a_part) |
| 952 | + |
| 953 | + # Assert |
| 954 | + assert result is not None |
| 955 | + assert result.function_call is not None |
| 956 | + assert result.function_call.name == "regular_tool" |
| 957 | + # thought_signature should be None |
| 958 | + assert result.thought_signature is None |
| 959 | + |
| 960 | + def test_function_call_with_thought_signature_round_trip(self): |
| 961 | + """Test thought_signature is preserved in GenAI -> A2A -> GenAI round trip.""" |
| 962 | + # Arrange |
| 963 | + original_signature = b"round_trip_signature_test" |
| 964 | + function_call = genai_types.FunctionCall( |
| 965 | + id="fc_round_trip", |
| 966 | + name="round_trip_tool", |
| 967 | + args={"key": "value"}, |
| 968 | + ) |
| 969 | + original_part = genai_types.Part( |
| 970 | + function_call=function_call, |
| 971 | + thought_signature=original_signature, |
| 972 | + ) |
| 973 | + |
| 974 | + # Act - Convert GenAI -> A2A -> GenAI |
| 975 | + a2a_part = convert_genai_part_to_a2a_part(original_part) |
| 976 | + restored_part = convert_a2a_part_to_genai_part(a2a_part) |
| 977 | + |
| 978 | + # Assert |
| 979 | + assert restored_part is not None |
| 980 | + assert restored_part.function_call is not None |
| 981 | + assert restored_part.function_call.name == "round_trip_tool" |
| 982 | + assert restored_part.thought_signature == original_signature |
| 983 | + |
| 984 | + def test_a2a_function_call_with_bytes_thought_signature_to_genai(self): |
| 985 | + """Test that bytes thought_signature is used directly without decoding.""" |
| 986 | + # Arrange - metadata contains raw bytes (not base64 encoded) |
| 987 | + a2a_part = a2a_types.Part( |
| 988 | + root=a2a_types.DataPart( |
| 989 | + data={ |
| 990 | + "id": "fc_bytes", |
| 991 | + "name": "bytes_tool", |
| 992 | + "args": {}, |
| 993 | + }, |
| 994 | + metadata={ |
| 995 | + _get_adk_metadata_key( |
| 996 | + A2A_DATA_PART_METADATA_TYPE_KEY |
| 997 | + ): A2A_DATA_PART_METADATA_TYPE_FUNCTION_CALL, |
| 998 | + _get_adk_metadata_key( |
| 999 | + "thought_signature" |
| 1000 | + ): b"raw_bytes_signature", |
| 1001 | + }, |
| 1002 | + ) |
| 1003 | + ) |
| 1004 | + |
| 1005 | + # Act |
| 1006 | + result = convert_a2a_part_to_genai_part(a2a_part) |
| 1007 | + |
| 1008 | + # Assert |
| 1009 | + assert result is not None |
| 1010 | + assert result.function_call is not None |
| 1011 | + # bytes should be used directly |
| 1012 | + assert result.thought_signature == b"raw_bytes_signature" |
| 1013 | + |
| 1014 | + def test_a2a_function_call_with_invalid_base64_thought_signature(self): |
| 1015 | + """Test that invalid base64 thought_signature logs warning and returns None.""" |
| 1016 | + # Arrange - metadata contains invalid base64 string |
| 1017 | + a2a_part = a2a_types.Part( |
| 1018 | + root=a2a_types.DataPart( |
| 1019 | + data={ |
| 1020 | + "id": "fc_invalid", |
| 1021 | + "name": "invalid_sig_tool", |
| 1022 | + "args": {}, |
| 1023 | + }, |
| 1024 | + metadata={ |
| 1025 | + _get_adk_metadata_key( |
| 1026 | + A2A_DATA_PART_METADATA_TYPE_KEY |
| 1027 | + ): A2A_DATA_PART_METADATA_TYPE_FUNCTION_CALL, |
| 1028 | + _get_adk_metadata_key( |
| 1029 | + "thought_signature" |
| 1030 | + ): "not_valid_base64!!!", |
| 1031 | + }, |
| 1032 | + ) |
| 1033 | + ) |
| 1034 | + |
| 1035 | + # Act |
| 1036 | + result = convert_a2a_part_to_genai_part(a2a_part) |
| 1037 | + |
| 1038 | + # Assert |
| 1039 | + assert result is not None |
| 1040 | + assert result.function_call is not None |
| 1041 | + assert result.function_call.name == "invalid_sig_tool" |
| 1042 | + # thought_signature should be None due to decode failure |
| 1043 | + assert result.thought_signature is None |
0 commit comments