|
16 | 16 | package io.agentscope.core.formatter.gemini; |
17 | 17 |
|
18 | 18 | import static org.junit.jupiter.api.Assertions.assertEquals; |
19 | | -import static org.junit.jupiter.api.Assertions.assertFalse; |
20 | 19 | import static org.junit.jupiter.api.Assertions.assertNotNull; |
21 | | -import static org.junit.jupiter.api.Assertions.assertNull; |
22 | | -import static org.junit.jupiter.api.Assertions.assertThrows; |
23 | 20 | import static org.junit.jupiter.api.Assertions.assertTrue; |
24 | | -import static org.mockito.Mockito.mock; |
25 | | -import static org.mockito.Mockito.when; |
26 | 21 |
|
27 | 22 | import io.agentscope.core.formatter.gemini.dto.GeminiContent; |
28 | 23 | import io.agentscope.core.formatter.gemini.dto.GeminiPart; |
29 | 24 | import io.agentscope.core.message.AudioBlock; |
30 | 25 | import io.agentscope.core.message.Base64Source; |
31 | | -import io.agentscope.core.message.ContentBlock; |
32 | 26 | import io.agentscope.core.message.ImageBlock; |
33 | 27 | import io.agentscope.core.message.Msg; |
34 | 28 | import io.agentscope.core.message.MsgRole; |
35 | | -import io.agentscope.core.message.Source; |
36 | 29 | import io.agentscope.core.message.TextBlock; |
37 | 30 | import io.agentscope.core.message.ThinkingBlock; |
38 | 31 | import io.agentscope.core.message.ToolResultBlock; |
39 | 32 | import io.agentscope.core.message.ToolUseBlock; |
40 | 33 | import io.agentscope.core.message.URLSource; |
41 | 34 | import io.agentscope.core.message.VideoBlock; |
42 | | -import java.lang.reflect.InvocationTargetException; |
43 | | -import java.lang.reflect.Method; |
44 | 35 | import java.util.ArrayList; |
45 | | -import java.util.Arrays; |
46 | 36 | import java.util.Base64; |
47 | 37 | import java.util.HashMap; |
48 | 38 | import java.util.List; |
@@ -72,8 +62,6 @@ class GeminiMessageConverterTest { |
72 | 62 |
|
73 | 63 | private GeminiMessageConverter converter; |
74 | 64 |
|
75 | | - private static class DummySource extends Source {} |
76 | | - |
77 | 65 | @BeforeEach |
78 | 66 | void setUp() { |
79 | 67 | converter = new GeminiMessageConverter(); |
@@ -953,235 +941,4 @@ void testToolCallFallbackToInputMapWhenContentInvalidJson() { |
953 | 941 | assertEquals("Tokyo", args.get("city")); |
954 | 942 | assertEquals("celsius", args.get("unit")); |
955 | 943 | } |
956 | | - |
957 | | - @Test |
958 | | - @DisplayName("Should convert synthetic tool call to text part") |
959 | | - void testConvertSyntheticToolUseBlockToText() { |
960 | | - ToolUseBlock syntheticToolCall = |
961 | | - ToolUseBlock.builder() |
962 | | - .id("call_syn") |
963 | | - .name("generate_response") |
964 | | - .input(Map.of("k", "v")) |
965 | | - .metadata(Map.of("synthetic", true)) |
966 | | - .build(); |
967 | | - |
968 | | - Msg msg = |
969 | | - Msg.builder() |
970 | | - .name("assistant") |
971 | | - .role(MsgRole.ASSISTANT) |
972 | | - .content(List.of(syntheticToolCall)) |
973 | | - .build(); |
974 | | - |
975 | | - List<GeminiContent> result = converter.convertMessages(List.of(msg)); |
976 | | - |
977 | | - assertEquals(1, result.size()); |
978 | | - GeminiPart part = result.get(0).getParts().get(0); |
979 | | - assertNull(part.getFunctionCall()); |
980 | | - assertEquals("[Synthetic tool call: generate_response]", part.getText()); |
981 | | - } |
982 | | - |
983 | | - @Test |
984 | | - @DisplayName("Should carry thinking signature from ThinkingBlock to next tool call") |
985 | | - void testToolCallUsesPrecedingThinkingSignature() { |
986 | | - ThinkingBlock thinking = |
987 | | - ThinkingBlock.builder().metadata(Map.of("thoughtSignature", "sig_pre")).build(); |
988 | | - ToolUseBlock toolUse = |
989 | | - ToolUseBlock.builder() |
990 | | - .id("call_pre_sig") |
991 | | - .name("search") |
992 | | - .input(Map.of("q", "hello")) |
993 | | - .build(); |
994 | | - |
995 | | - Msg msg = |
996 | | - Msg.builder() |
997 | | - .name("assistant") |
998 | | - .role(MsgRole.ASSISTANT) |
999 | | - .content(List.of(thinking, toolUse)) |
1000 | | - .build(); |
1001 | | - |
1002 | | - List<GeminiContent> result = converter.convertMessages(List.of(msg)); |
1003 | | - |
1004 | | - assertEquals(1, result.size()); |
1005 | | - GeminiPart part = result.get(0).getParts().get(0); |
1006 | | - assertNotNull(part.getFunctionCall()); |
1007 | | - assertEquals("sig_pre", part.getThoughtSignature()); |
1008 | | - } |
1009 | | - |
1010 | | - @Test |
1011 | | - @DisplayName("Should prefer metadata thought signature over preceding thinking signature") |
1012 | | - void testToolCallMetadataSignatureOverridesThinkingSignature() { |
1013 | | - ThinkingBlock thinking = |
1014 | | - ThinkingBlock.builder() |
1015 | | - .metadata(Map.of("thoughtSignature", "sig_from_thinking")) |
1016 | | - .build(); |
1017 | | - ToolUseBlock toolUse = |
1018 | | - ToolUseBlock.builder() |
1019 | | - .id("call_meta_sig") |
1020 | | - .name("search") |
1021 | | - .input(Map.of("q", "hello")) |
1022 | | - .metadata(Map.of(ToolUseBlock.METADATA_THOUGHT_SIGNATURE, "sig_from_tool")) |
1023 | | - .build(); |
1024 | | - |
1025 | | - Msg msg = |
1026 | | - Msg.builder() |
1027 | | - .name("assistant") |
1028 | | - .role(MsgRole.ASSISTANT) |
1029 | | - .content(List.of(thinking, toolUse)) |
1030 | | - .build(); |
1031 | | - |
1032 | | - List<GeminiContent> result = converter.convertMessages(List.of(msg)); |
1033 | | - |
1034 | | - GeminiPart part = result.get(0).getParts().get(0); |
1035 | | - assertEquals("sig_from_tool", part.getThoughtSignature()); |
1036 | | - } |
1037 | | - |
1038 | | - @Test |
1039 | | - @DisplayName("Should convert byte array thought signature metadata to base64") |
1040 | | - void testToolCallByteArrayThoughtSignature() { |
1041 | | - byte[] signatureBytes = new byte[] {1, 2, 3, 4}; |
1042 | | - ToolUseBlock toolUse = |
1043 | | - ToolUseBlock.builder() |
1044 | | - .id("call_byte_sig") |
1045 | | - .name("search") |
1046 | | - .input(Map.of("q", "hello")) |
1047 | | - .metadata(Map.of(ToolUseBlock.METADATA_THOUGHT_SIGNATURE, signatureBytes)) |
1048 | | - .build(); |
1049 | | - |
1050 | | - Msg msg = |
1051 | | - Msg.builder() |
1052 | | - .name("assistant") |
1053 | | - .role(MsgRole.ASSISTANT) |
1054 | | - .content(List.of(toolUse)) |
1055 | | - .build(); |
1056 | | - |
1057 | | - List<GeminiContent> result = converter.convertMessages(List.of(msg)); |
1058 | | - GeminiPart part = result.get(0).getParts().get(0); |
1059 | | - |
1060 | | - assertEquals( |
1061 | | - Base64.getEncoder().encodeToString(signatureBytes), part.getThoughtSignature()); |
1062 | | - } |
1063 | | - |
1064 | | - @Test |
1065 | | - @DisplayName("Should not merge same-role contents when function call is present") |
1066 | | - void testSameRoleNotMergedWhenFunctionExists() { |
1067 | | - ToolUseBlock toolUse = |
1068 | | - ToolUseBlock.builder() |
1069 | | - .id("call_merge_guard") |
1070 | | - .name("search") |
1071 | | - .input(Map.of("q", "hello")) |
1072 | | - .build(); |
1073 | | - |
1074 | | - Msg assistantToolMsg = |
1075 | | - Msg.builder() |
1076 | | - .name("assistant") |
1077 | | - .role(MsgRole.ASSISTANT) |
1078 | | - .content(List.of(toolUse)) |
1079 | | - .build(); |
1080 | | - Msg assistantTextMsg = |
1081 | | - Msg.builder() |
1082 | | - .name("assistant") |
1083 | | - .role(MsgRole.ASSISTANT) |
1084 | | - .content(List.of(TextBlock.builder().text("after tool").build())) |
1085 | | - .build(); |
1086 | | - |
1087 | | - List<GeminiContent> result = |
1088 | | - converter.convertMessages(List.of(assistantToolMsg, assistantTextMsg)); |
1089 | | - |
1090 | | - assertEquals(2, result.size()); |
1091 | | - assertFalse(result.get(0).getParts().isEmpty()); |
1092 | | - assertNotNull(result.get(0).getParts().get(0).getFunctionCall()); |
1093 | | - assertEquals("after tool", result.get(1).getParts().get(0).getText()); |
1094 | | - } |
1095 | | - |
1096 | | - @Test |
1097 | | - @DisplayName("Should fallback to input map when content parses to null") |
1098 | | - void testToolCallFallbackToInputMapWhenContentParsesToNull() { |
1099 | | - Map<String, Object> inputMap = new HashMap<>(); |
1100 | | - inputMap.put("city", "Shenzhen"); |
1101 | | - inputMap.put("unit", "celsius"); |
1102 | | - |
1103 | | - ToolUseBlock toolBlock = |
1104 | | - ToolUseBlock.builder() |
1105 | | - .id("call_null_json_test") |
1106 | | - .name("get_weather") |
1107 | | - .input(inputMap) |
1108 | | - .content("null") |
1109 | | - .build(); |
1110 | | - |
1111 | | - Msg msg = |
1112 | | - Msg.builder() |
1113 | | - .name("assistant") |
1114 | | - .content(List.of(toolBlock)) |
1115 | | - .role(MsgRole.ASSISTANT) |
1116 | | - .build(); |
1117 | | - |
1118 | | - List<GeminiContent> result = converter.convertMessages(List.of(msg)); |
1119 | | - GeminiPart part = result.get(0).getParts().get(0); |
1120 | | - assertNotNull(part.getFunctionCall()); |
1121 | | - assertEquals("Shenzhen", part.getFunctionCall().getArgs().get("city")); |
1122 | | - assertEquals("celsius", part.getFunctionCall().getArgs().get("unit")); |
1123 | | - } |
1124 | | - |
1125 | | - @Test |
1126 | | - @DisplayName("Should return unsupported source placeholder for unknown source type") |
1127 | | - void testToolResultWithUnsupportedSourceType() { |
1128 | | - ImageBlock imageBlock = ImageBlock.builder().source(new DummySource()).build(); |
1129 | | - |
1130 | | - String output = converter.convertToolResultToString(List.of(imageBlock)); |
1131 | | - |
1132 | | - assertEquals("[image - unsupported source type]", output); |
1133 | | - } |
1134 | | - |
1135 | | - @Test |
1136 | | - @DisplayName("Should support base64 media type without slash extension") |
1137 | | - void testToolResultWithBase64MediaTypeWithoutSlash() { |
1138 | | - String base64Data = Base64.getEncoder().encodeToString("fake image".getBytes()); |
1139 | | - ImageBlock imageBlock = |
1140 | | - ImageBlock.builder() |
1141 | | - .source(Base64Source.builder().mediaType("png").data(base64Data).build()) |
1142 | | - .build(); |
1143 | | - |
1144 | | - String output = converter.convertToolResultToString(List.of(imageBlock)); |
1145 | | - |
1146 | | - assertTrue(output.contains(".png")); |
1147 | | - } |
1148 | | - |
1149 | | - @Test |
1150 | | - @DisplayName("Should return false for hasFunction helper with null values") |
1151 | | - void testHasFunctionHelperWithNullValues() throws Exception { |
1152 | | - Method method = |
1153 | | - GeminiMessageConverter.class.getDeclaredMethod("hasFunction", GeminiContent.class); |
1154 | | - method.setAccessible(true); |
1155 | | - |
1156 | | - assertEquals(false, method.invoke(converter, new Object[] {null})); |
1157 | | - assertEquals(false, method.invoke(converter, new GeminiContent("user", null))); |
1158 | | - } |
1159 | | - |
1160 | | - @Test |
1161 | | - @DisplayName("Should throw for extractSourceFromBlock with unsupported block type") |
1162 | | - void testExtractSourceFromBlockUnsupportedType() throws Exception { |
1163 | | - Method method = |
1164 | | - GeminiMessageConverter.class.getDeclaredMethod( |
1165 | | - "extractSourceFromBlock", ContentBlock.class); |
1166 | | - method.setAccessible(true); |
1167 | | - |
1168 | | - try { |
1169 | | - method.invoke(converter, TextBlock.builder().text("not media").build()); |
1170 | | - } catch (InvocationTargetException e) { |
1171 | | - assertTrue(e.getCause() instanceof IllegalArgumentException); |
1172 | | - assertTrue(e.getCause().getMessage().contains("Unsupported block type")); |
1173 | | - return; |
1174 | | - } |
1175 | | - throw new AssertionError("Expected IllegalArgumentException"); |
1176 | | - } |
1177 | | - |
1178 | | - @Test |
1179 | | - @DisplayName("Should throw when mocked message returns null content block") |
1180 | | - void testConvertMessageWithMockedNullContentBlockThrows() { |
1181 | | - Msg mocked = mock(Msg.class); |
1182 | | - when(mocked.getRole()).thenReturn(MsgRole.USER); |
1183 | | - when(mocked.getContent()).thenReturn(Arrays.asList((ContentBlock) null)); |
1184 | | - |
1185 | | - assertThrows(NullPointerException.class, () -> converter.convertMessages(List.of(mocked))); |
1186 | | - } |
1187 | 944 | } |
0 commit comments