diff --git a/models/spring-ai-vertex-ai-embedding/src/test/java/org/springframework/ai/vertexai/embedding/text/VertexAiTextEmbeddingRetryTests.java b/models/spring-ai-vertex-ai-embedding/src/test/java/org/springframework/ai/vertexai/embedding/text/VertexAiTextEmbeddingRetryTests.java index 088c87bc75a..6d88ef6e958 100644 --- a/models/spring-ai-vertex-ai-embedding/src/test/java/org/springframework/ai/vertexai/embedding/text/VertexAiTextEmbeddingRetryTests.java +++ b/models/spring-ai-vertex-ai-embedding/src/test/java/org/springframework/ai/vertexai/embedding/text/VertexAiTextEmbeddingRetryTests.java @@ -143,6 +143,19 @@ public void vertexAiEmbeddingNonTransientError() { verify(this.mockPredictionServiceClient, times(1)).predict(any()); } + @Test + public void vertexAiEmbeddingWithEmptyTextList() { + PredictResponse emptyResponse = PredictResponse.newBuilder().build(); + given(this.mockPredictionServiceClient.predict(any())).willReturn(emptyResponse); + + EmbeddingOptions options = VertexAiTextEmbeddingOptions.builder().model("model").build(); + EmbeddingResponse result = this.embeddingModel.call(new EmbeddingRequest(List.of(), options)); + + assertThat(result).isNotNull(); + // Behavior depends on implementation - might be empty results or exception + verify(this.mockPredictionServiceClient, times(1)).predict(any()); + } + private static class TestRetryListener implements RetryListener { int onErrorRetryCount = 0; diff --git a/models/spring-ai-vertex-ai-gemini/src/test/java/org/springframework/ai/vertexai/gemini/VertexAiGeminiRetryTests.java b/models/spring-ai-vertex-ai-gemini/src/test/java/org/springframework/ai/vertexai/gemini/VertexAiGeminiRetryTests.java index d02efb6a011..79ac33982c3 100644 --- a/models/spring-ai-vertex-ai-gemini/src/test/java/org/springframework/ai/vertexai/gemini/VertexAiGeminiRetryTests.java +++ b/models/spring-ai-vertex-ai-gemini/src/test/java/org/springframework/ai/vertexai/gemini/VertexAiGeminiRetryTests.java @@ -157,6 +157,102 @@ public void vertexAiGeminiChatWithEmptyResponse() throws Exception { assertThat(this.retryListener.onErrorRetryCount).isEqualTo(1); } + @Test + public void vertexAiGeminiChatMaxRetriesExceeded() throws Exception { + // Test that after max retries, the exception is propagated + given(this.mockGenerativeModel.generateContent(any(List.class))) + .willThrow(new TransientAiException("Persistent Error")) + .willThrow(new TransientAiException("Persistent Error")) + .willThrow(new TransientAiException("Persistent Error")) + .willThrow(new TransientAiException("Persistent Error")); + + // Should throw the last TransientAiException after exhausting retries + assertThrows(TransientAiException.class, () -> this.chatModel.call(new Prompt("test prompt"))); + + // Verify retry attempts were made + assertThat(this.retryListener.onErrorRetryCount).isGreaterThan(0); + } + + @Test + public void vertexAiGeminiChatWithMultipleCandidatesResponse() throws Exception { + // Test response with multiple candidates + GenerateContentResponse multiCandidateResponse = GenerateContentResponse.newBuilder() + .addCandidates(Candidate.newBuilder() + .setContent(Content.newBuilder().addParts(Part.newBuilder().setText("First candidate").build()).build()) + .build()) + .addCandidates(Candidate.newBuilder() + .setContent( + Content.newBuilder().addParts(Part.newBuilder().setText("Second candidate").build()).build()) + .build()) + .build(); + + given(this.mockGenerativeModel.generateContent(any(List.class))) + .willThrow(new TransientAiException("Temporary failure")) + .willReturn(multiCandidateResponse); + + ChatResponse result = this.chatModel.call(new Prompt("test prompt")); + + assertThat(result).isNotNull(); + // Assuming the implementation uses the first candidate + assertThat(result.getResult().getOutput().getText()).isEqualTo("First candidate"); + assertThat(this.retryListener.onSuccessRetryCount).isEqualTo(1); + } + + @Test + public void vertexAiGeminiChatWithNullPrompt() throws Exception { + // Test handling of null prompt + Prompt prompt = null; + assertThrows(Exception.class, () -> this.chatModel.call(prompt)); + + // Should not trigger any retries for validation errors + assertThat(this.retryListener.onErrorRetryCount).isEqualTo(0); + assertThat(this.retryListener.onSuccessRetryCount).isEqualTo(0); + } + + @Test + public void vertexAiGeminiChatWithEmptyPrompt() throws Exception { + // Test handling of empty prompt + GenerateContentResponse mockedResponse = GenerateContentResponse.newBuilder() + .addCandidates(Candidate.newBuilder() + .setContent(Content.newBuilder() + .addParts(Part.newBuilder().setText("Response to empty prompt").build()) + .build()) + .build()) + .build(); + + given(this.mockGenerativeModel.generateContent(any(List.class))).willReturn(mockedResponse); + + ChatResponse result = this.chatModel.call(new Prompt("")); + + assertThat(result).isNotNull(); + assertThat(result.getResult().getOutput().getText()).isEqualTo("Response to empty prompt"); + assertThat(this.retryListener.onSuccessRetryCount).isEqualTo(0); + } + + @Test + public void vertexAiGeminiChatAlternatingErrorsAndSuccess() throws Exception { + // Test pattern of error -> success -> error -> success + GenerateContentResponse successResponse = GenerateContentResponse.newBuilder() + .addCandidates(Candidate.newBuilder() + .setContent(Content.newBuilder() + .addParts(Part.newBuilder().setText("Success after alternating errors").build()) + .build()) + .build()) + .build(); + + given(this.mockGenerativeModel.generateContent(any(List.class))) + .willThrow(new TransientAiException("First error")) + .willThrow(new TransientAiException("Second error")) + .willReturn(successResponse); + + ChatResponse result = this.chatModel.call(new Prompt("test prompt")); + + assertThat(result).isNotNull(); + assertThat(result.getResult().getOutput().getText()).isEqualTo("Success after alternating errors"); + assertThat(this.retryListener.onSuccessRetryCount).isEqualTo(2); + assertThat(this.retryListener.onErrorRetryCount).isEqualTo(2); + } + private static class TestRetryListener implements RetryListener { int onErrorRetryCount = 0; diff --git a/spring-ai-client-chat/src/test/java/org/springframework/ai/metadata/PromptMetadataTests.java b/spring-ai-client-chat/src/test/java/org/springframework/ai/metadata/PromptMetadataTests.java index 0e6353e828a..d93b82b5f62 100644 --- a/spring-ai-client-chat/src/test/java/org/springframework/ai/metadata/PromptMetadataTests.java +++ b/spring-ai-client-chat/src/test/java/org/springframework/ai/metadata/PromptMetadataTests.java @@ -106,4 +106,73 @@ void fromPromptIndexAndContentFilterMetadata() { assertThat(promptFilterMetadata.getContentFilterMetadata()).isEqualTo("{ content-sentiment: 'SAFE' }"); } + @Test + void promptMetadataWithEmptyFiltersArray() { + PromptMetadata promptMetadata = PromptMetadata.of(); + + assertThat(promptMetadata).isNotNull(); + assertThat(promptMetadata).isEmpty(); + } + + @Test + void promptMetadataWithMultipleFilters() { + PromptFilterMetadata filter1 = mockPromptFilterMetadata(0); + PromptFilterMetadata filter2 = mockPromptFilterMetadata(1); + PromptFilterMetadata filter3 = mockPromptFilterMetadata(2); + PromptFilterMetadata filter4 = mockPromptFilterMetadata(3); + + PromptMetadata promptMetadata = PromptMetadata.of(filter1, filter2, filter3, filter4); + + assertThat(promptMetadata).isNotNull(); + assertThat(promptMetadata).hasSize(4); + assertThat(promptMetadata).containsExactly(filter1, filter2, filter3, filter4); + } + + @Test + void promptMetadataWithDuplicateIndices() { + PromptFilterMetadata filter1 = mockPromptFilterMetadata(1); + PromptFilterMetadata filter2 = mockPromptFilterMetadata(1); + + PromptMetadata promptMetadata = PromptMetadata.of(filter1, filter2); + + assertThat(promptMetadata).isNotNull(); + assertThat(promptMetadata).hasSize(2); + + assertThat(promptMetadata.findByPromptIndex(1).orElse(null)).isEqualTo(filter1); + } + + @Test + void promptFilterMetadataWithEmptyContentFilter() { + PromptFilterMetadata promptFilterMetadata = PromptFilterMetadata.from(0, ""); + + assertThat(promptFilterMetadata).isNotNull(); + assertThat(promptFilterMetadata.getPromptIndex()).isZero(); + assertThat(promptFilterMetadata.getContentFilterMetadata()).isEmpty(); + } + + @Test + void promptMetadataSize() { + PromptFilterMetadata filter1 = mockPromptFilterMetadata(0); + PromptFilterMetadata filter2 = mockPromptFilterMetadata(1); + + PromptMetadata empty = PromptMetadata.empty(); + PromptMetadata single = PromptMetadata.of(filter1); + PromptMetadata multiple = PromptMetadata.of(filter1, filter2); + + assertThat(empty).hasSize(0); + assertThat(single).hasSize(1); + assertThat(multiple).hasSize(2); + } + + @Test + void promptMetadataImmutability() { + PromptFilterMetadata filter1 = mockPromptFilterMetadata(0); + PromptFilterMetadata filter2 = mockPromptFilterMetadata(1); + + PromptMetadata promptMetadata = PromptMetadata.of(filter1, filter2); + + assertThat(promptMetadata).isNotNull(); + assertThat(promptMetadata).hasSize(2); + } + }