Skip to content

Commit ac585d9

Browse files
committed
test(Gemini API): add unit tests for handling null and empty inputs in system instructions and options
Signed-off-by: liuhy <liuhongyu@apache.org>
1 parent 36cbb8b commit ac585d9

6 files changed

Lines changed: 402 additions & 0 deletions

File tree

agentscope-core/src/test/java/io/agentscope/core/formatter/gemini/GeminiChatFormatterTest.java

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,12 @@
2626
import io.agentscope.core.message.Msg;
2727
import io.agentscope.core.message.MsgRole;
2828
import io.agentscope.core.message.TextBlock;
29+
import io.agentscope.core.message.ThinkingBlock;
2930
import io.agentscope.core.model.ChatResponse;
3031
import io.agentscope.core.model.GenerateOptions;
3132
import io.agentscope.core.model.ToolChoice;
3233
import io.agentscope.core.model.ToolSchema;
34+
import java.lang.reflect.Method;
3335
import java.time.Instant;
3436
import java.util.ArrayList;
3537
import java.util.HashMap;
@@ -266,4 +268,70 @@ void testApplyToolsWithEmptyListDoesNothing() {
266268
formatter.applyTools(request, new ArrayList<>());
267269
assertNull(request.getTools());
268270
}
271+
272+
@Test
273+
void testApplyOptionsWithEmptyOptionsObjectDoesNotCreateGenerationConfig() {
274+
GeminiRequest request = new GeminiRequest();
275+
276+
formatter.applyOptions(request, GenerateOptions.builder().build(), null);
277+
278+
assertNull(request.getGenerationConfig());
279+
}
280+
281+
@Test
282+
void testApplyOptionsWithEmptyDefaultOptionsObjectDoesNotCreateGenerationConfig() {
283+
GeminiRequest request = new GeminiRequest();
284+
285+
formatter.applyOptions(request, null, GenerateOptions.builder().build());
286+
287+
assertNull(request.getGenerationConfig());
288+
}
289+
290+
@Test
291+
void testApplySystemInstructionWithNullAndEmptyMessages() {
292+
GeminiRequest requestWithNull = new GeminiRequest();
293+
formatter.applySystemInstruction(requestWithNull, null);
294+
assertNull(requestWithNull.getSystemInstruction());
295+
296+
GeminiRequest requestWithEmpty = new GeminiRequest();
297+
formatter.applySystemInstruction(requestWithEmpty, List.of());
298+
assertNull(requestWithEmpty.getSystemInstruction());
299+
}
300+
301+
@Test
302+
void testApplySystemInstructionWithNonConvertibleSystemMessage() {
303+
Msg systemThinkingOnly =
304+
Msg.builder()
305+
.role(MsgRole.SYSTEM)
306+
.content(List.of(ThinkingBlock.builder().thinking("internal").build()))
307+
.build();
308+
309+
GeminiRequest request = new GeminiRequest();
310+
formatter.applySystemInstruction(request, List.of(systemThinkingOnly));
311+
312+
assertNull(request.getSystemInstruction());
313+
}
314+
315+
@Test
316+
void testComputeStartIndexHelperBranches() throws Exception {
317+
Method method =
318+
GeminiChatFormatter.class.getDeclaredMethod("computeStartIndex", List.class);
319+
method.setAccessible(true);
320+
321+
Msg system =
322+
Msg.builder()
323+
.role(MsgRole.SYSTEM)
324+
.content(List.of(TextBlock.builder().text("sys").build()))
325+
.build();
326+
Msg user =
327+
Msg.builder()
328+
.role(MsgRole.USER)
329+
.content(List.of(TextBlock.builder().text("user").build()))
330+
.build();
331+
332+
assertEquals(0, method.invoke(formatter, new Object[] {null}));
333+
assertEquals(0, method.invoke(formatter, List.of()));
334+
assertEquals(1, method.invoke(formatter, List.of(system)));
335+
assertEquals(0, method.invoke(formatter, List.of(user)));
336+
}
269337
}

agentscope-core/src/test/java/io/agentscope/core/formatter/gemini/GeminiMessageConverterTest.java

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,22 +19,30 @@
1919
import static org.junit.jupiter.api.Assertions.assertFalse;
2020
import static org.junit.jupiter.api.Assertions.assertNotNull;
2121
import static org.junit.jupiter.api.Assertions.assertNull;
22+
import static org.junit.jupiter.api.Assertions.assertThrows;
2223
import static org.junit.jupiter.api.Assertions.assertTrue;
24+
import static org.mockito.Mockito.mock;
25+
import static org.mockito.Mockito.when;
2326

2427
import io.agentscope.core.formatter.gemini.dto.GeminiContent;
2528
import io.agentscope.core.formatter.gemini.dto.GeminiPart;
2629
import io.agentscope.core.message.AudioBlock;
2730
import io.agentscope.core.message.Base64Source;
31+
import io.agentscope.core.message.ContentBlock;
2832
import io.agentscope.core.message.ImageBlock;
2933
import io.agentscope.core.message.Msg;
3034
import io.agentscope.core.message.MsgRole;
35+
import io.agentscope.core.message.Source;
3136
import io.agentscope.core.message.TextBlock;
3237
import io.agentscope.core.message.ThinkingBlock;
3338
import io.agentscope.core.message.ToolResultBlock;
3439
import io.agentscope.core.message.ToolUseBlock;
3540
import io.agentscope.core.message.URLSource;
3641
import io.agentscope.core.message.VideoBlock;
42+
import java.lang.reflect.InvocationTargetException;
43+
import java.lang.reflect.Method;
3744
import java.util.ArrayList;
45+
import java.util.Arrays;
3846
import java.util.Base64;
3947
import java.util.HashMap;
4048
import java.util.List;
@@ -64,6 +72,8 @@ class GeminiMessageConverterTest {
6472

6573
private GeminiMessageConverter converter;
6674

75+
private static class DummySource extends Source {}
76+
6777
@BeforeEach
6878
void setUp() {
6979
converter = new GeminiMessageConverter();
@@ -1082,4 +1092,96 @@ void testSameRoleNotMergedWhenFunctionExists() {
10821092
assertNotNull(result.get(0).getParts().get(0).getFunctionCall());
10831093
assertEquals("after tool", result.get(1).getParts().get(0).getText());
10841094
}
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+
}
10851187
}

agentscope-core/src/test/java/io/agentscope/core/formatter/gemini/GeminiMultiAgentFormatterTest.java

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,12 @@
2828
import io.agentscope.core.message.Msg;
2929
import io.agentscope.core.message.MsgRole;
3030
import io.agentscope.core.message.TextBlock;
31+
import io.agentscope.core.message.ThinkingBlock;
3132
import io.agentscope.core.model.ChatResponse;
3233
import io.agentscope.core.model.GenerateOptions;
3334
import io.agentscope.core.model.ToolChoice;
3435
import io.agentscope.core.model.ToolSchema;
36+
import java.lang.reflect.Method;
3537
import java.time.Instant;
3638
import java.util.List;
3739
import java.util.Map;
@@ -239,4 +241,52 @@ void testApplySystemInstructionIsStateless() {
239241
formatter.applySystemInstruction(requestWithoutSystem, List.of(user));
240242
assertNull(requestWithoutSystem.getSystemInstruction());
241243
}
244+
245+
@Test
246+
void testApplySystemInstructionWithNullAndEmptyMessages() {
247+
GeminiRequest requestWithNull = new GeminiRequest();
248+
formatter.applySystemInstruction(requestWithNull, null);
249+
assertNull(requestWithNull.getSystemInstruction());
250+
251+
GeminiRequest requestWithEmpty = new GeminiRequest();
252+
formatter.applySystemInstruction(requestWithEmpty, List.of());
253+
assertNull(requestWithEmpty.getSystemInstruction());
254+
}
255+
256+
@Test
257+
void testApplySystemInstructionWithNonConvertibleSystemMessage() {
258+
Msg systemThinkingOnly =
259+
Msg.builder()
260+
.role(MsgRole.SYSTEM)
261+
.content(List.of(ThinkingBlock.builder().thinking("internal").build()))
262+
.build();
263+
264+
GeminiRequest request = new GeminiRequest();
265+
formatter.applySystemInstruction(request, List.of(systemThinkingOnly));
266+
267+
assertNull(request.getSystemInstruction());
268+
}
269+
270+
@Test
271+
void testComputeStartIndexHelperBranches() throws Exception {
272+
Method method =
273+
GeminiMultiAgentFormatter.class.getDeclaredMethod("computeStartIndex", List.class);
274+
method.setAccessible(true);
275+
276+
Msg system =
277+
Msg.builder()
278+
.role(MsgRole.SYSTEM)
279+
.content(List.of(TextBlock.builder().text("sys").build()))
280+
.build();
281+
Msg user =
282+
Msg.builder()
283+
.role(MsgRole.USER)
284+
.content(List.of(TextBlock.builder().text("user").build()))
285+
.build();
286+
287+
assertEquals(0, method.invoke(formatter, new Object[] {null}));
288+
assertEquals(0, method.invoke(formatter, List.of()));
289+
assertEquals(1, method.invoke(formatter, List.of(system)));
290+
assertEquals(0, method.invoke(formatter, List.of(user)));
291+
}
242292
}

agentscope-core/src/test/java/io/agentscope/core/formatter/gemini/GeminiResponseParserTest.java

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,12 @@
1818
import static org.junit.jupiter.api.Assertions.assertEquals;
1919
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
2020
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;
2123
import static org.junit.jupiter.api.Assertions.assertTrue;
2224

2325
import io.agentscope.core.agent.StructuredOutputCapableAgent;
26+
import io.agentscope.core.formatter.FormatterException;
2427
import io.agentscope.core.formatter.gemini.dto.GeminiContent;
2528
import io.agentscope.core.formatter.gemini.dto.GeminiPart;
2629
import io.agentscope.core.formatter.gemini.dto.GeminiPart.GeminiFunctionCall;
@@ -536,4 +539,98 @@ void testNoCandidatesIncludesPromptFeedbackText() {
536539
assertTrue(text.contains("promptFeedback"));
537540
assertTrue(text.contains("SAFETY"));
538541
}
542+
543+
@Test
544+
void testParseResponseThrowsFormatterExceptionWhenPartsContainNull() {
545+
List<GeminiPart> parts = new ArrayList<>();
546+
parts.add(null);
547+
548+
GeminiCandidate candidate = new GeminiCandidate();
549+
candidate.setContent(new GeminiContent("model", parts));
550+
551+
GeminiResponse response = new GeminiResponse();
552+
response.setCandidates(List.of(candidate));
553+
554+
assertThrows(FormatterException.class, () -> parser.parseResponse(response, startTime));
555+
}
556+
557+
@Test
558+
void testParseUsageMetadataDefaultsPromptTokensToZeroWhenNull() {
559+
GeminiPart textPart = new GeminiPart();
560+
textPart.setText("ok");
561+
562+
GeminiCandidate candidate = new GeminiCandidate();
563+
candidate.setContent(new GeminiContent("model", List.of(textPart)));
564+
565+
GeminiUsageMetadata usageMetadata = new GeminiUsageMetadata();
566+
usageMetadata.setPromptTokenCount(null);
567+
usageMetadata.setCandidatesTokenCount(5);
568+
569+
GeminiResponse response = new GeminiResponse();
570+
response.setCandidates(List.of(candidate));
571+
response.setUsageMetadata(usageMetadata);
572+
573+
ChatResponse chatResponse = parser.parseResponse(response, startTime);
574+
assertNotNull(chatResponse.getUsage());
575+
assertEquals(0, chatResponse.getUsage().getInputTokens());
576+
assertEquals(5, chatResponse.getUsage().getOutputTokens());
577+
}
578+
579+
@Test
580+
void testSkipFunctionCallWhenNameIsNull() {
581+
GeminiFunctionCall functionCall = new GeminiFunctionCall();
582+
functionCall.setId("call-null-name");
583+
functionCall.setName(null);
584+
functionCall.setArgs(Map.of("k", "v"));
585+
586+
GeminiPart functionCallPart = new GeminiPart();
587+
functionCallPart.setFunctionCall(functionCall);
588+
589+
GeminiPart textPart = new GeminiPart();
590+
textPart.setText("text after null-name call");
591+
592+
GeminiCandidate candidate = new GeminiCandidate();
593+
candidate.setContent(new GeminiContent("model", List.of(functionCallPart, textPart)));
594+
595+
GeminiResponse response = new GeminiResponse();
596+
response.setCandidates(List.of(candidate));
597+
598+
ChatResponse chatResponse = parser.parseResponse(response, startTime);
599+
assertEquals(1, chatResponse.getContent().size());
600+
assertInstanceOf(TextBlock.class, chatResponse.getContent().get(0));
601+
assertEquals(
602+
"text after null-name call",
603+
((TextBlock) chatResponse.getContent().get(0)).getText());
604+
}
605+
606+
@Test
607+
void testParseToolCallSerializationFailureKeepsToolBlock() {
608+
Map<String, Object> cyclic = new HashMap<>();
609+
cyclic.put("self", cyclic);
610+
611+
GeminiFunctionCall functionCall = new GeminiFunctionCall();
612+
functionCall.setId("call-cyclic");
613+
functionCall.setName("cyclic_tool");
614+
functionCall.setArgs(cyclic);
615+
616+
List<ContentBlock> blocks = new ArrayList<>();
617+
parser.parseToolCall(functionCall, "sig-cyclic", blocks);
618+
619+
assertEquals(1, blocks.size());
620+
ToolUseBlock toolUse = (ToolUseBlock) blocks.get(0);
621+
assertEquals("cyclic_tool", toolUse.getName());
622+
assertNull(toolUse.getContent());
623+
assertEquals(
624+
"sig-cyclic", toolUse.getMetadata().get(ToolUseBlock.METADATA_THOUGHT_SIGNATURE));
625+
}
626+
627+
@Test
628+
void testParseToolCallSwallowsExceptionWhenBlocksListIsNull() {
629+
GeminiFunctionCall functionCall = new GeminiFunctionCall();
630+
functionCall.setId("call-null-blocks");
631+
functionCall.setName("tool");
632+
functionCall.setArgs(Map.of("x", 1));
633+
634+
parser.parseToolCall(functionCall, null, null);
635+
}
539636
}

0 commit comments

Comments
 (0)