diff --git a/spring-ai-model/src/test/java/org/springframework/ai/image/observation/DefaultImageModelObservationConventionTests.java b/spring-ai-model/src/test/java/org/springframework/ai/image/observation/DefaultImageModelObservationConventionTests.java index f75fb4961f5..a0c2c4a8305 100644 --- a/spring-ai-model/src/test/java/org/springframework/ai/image/observation/DefaultImageModelObservationConventionTests.java +++ b/spring-ai-model/src/test/java/org/springframework/ai/image/observation/DefaultImageModelObservationConventionTests.java @@ -120,6 +120,83 @@ void shouldNotHaveKeyValuesWhenEmptyValues() { HighCardinalityKeyNames.REQUEST_IMAGE_STYLE.asString()); } + @Test + void shouldHandleNullModel() { + ImageModelObservationContext observationContext = ImageModelObservationContext.builder() + .imagePrompt(new ImagePrompt("test prompt")) + .provider("test-provider") + .build(); + + assertThat(this.observationConvention.getContextualName(observationContext)).isEqualTo("image"); + assertThat(this.observationConvention.getLowCardinalityKeyValues(observationContext)) + .contains(KeyValue.of(AiObservationAttributes.REQUEST_MODEL.value(), KeyValue.NONE_VALUE)); + } + + @Test + void shouldHandleEmptyModel() { + ImageModelObservationContext observationContext = ImageModelObservationContext.builder() + .imagePrompt(generateImagePrompt(ImageOptionsBuilder.builder().model("").build())) + .provider("test-provider") + .build(); + + assertThat(this.observationConvention.getContextualName(observationContext)).isEqualTo("image"); + } + + @Test + void shouldHandleBlankModel() { + ImageModelObservationContext observationContext = ImageModelObservationContext.builder() + .imagePrompt(generateImagePrompt(ImageOptionsBuilder.builder().model(" ").build())) + .provider("test-provider") + .build(); + + assertThat(this.observationConvention.getContextualName(observationContext)).isEqualTo("image"); + } + + @Test + void shouldHandleEmptyStyle() { + var imageOptions = ImageOptionsBuilder.builder().model("test-model").style("").build(); + + ImageModelObservationContext observationContext = ImageModelObservationContext.builder() + .imagePrompt(generateImagePrompt(imageOptions)) + .provider("test-provider") + .build(); + + // Empty style should not be included + assertThat(this.observationConvention.getHighCardinalityKeyValues(observationContext) + .stream() + .map(KeyValue::getKey) + .toList()).doesNotContain(HighCardinalityKeyNames.REQUEST_IMAGE_STYLE.asString()); + } + + @Test + void shouldHandleEmptyResponseFormat() { + var imageOptions = ImageOptionsBuilder.builder().model("test-model").responseFormat("").build(); + + ImageModelObservationContext observationContext = ImageModelObservationContext.builder() + .imagePrompt(generateImagePrompt(imageOptions)) + .provider("test-provider") + .build(); + + // Empty response format should not be included + assertThat(this.observationConvention.getHighCardinalityKeyValues(observationContext) + .stream() + .map(KeyValue::getKey) + .toList()).doesNotContain(HighCardinalityKeyNames.REQUEST_IMAGE_RESPONSE_FORMAT.asString()); + } + + @Test + void shouldHandleImagePromptWithoutOptions() { + ImageModelObservationContext observationContext = ImageModelObservationContext.builder() + .imagePrompt(new ImagePrompt("simple prompt")) + .provider("simple-provider") + .build(); + + assertThat(this.observationConvention.getContextualName(observationContext)).isEqualTo("image"); + assertThat(this.observationConvention.getLowCardinalityKeyValues(observationContext)) + .contains(KeyValue.of(AiObservationAttributes.REQUEST_MODEL.value(), KeyValue.NONE_VALUE)); + assertThat(this.observationConvention.getHighCardinalityKeyValues(observationContext)).isEmpty(); + } + private ImagePrompt generateImagePrompt(ImageOptions imageOptions) { return new ImagePrompt("here comes the sun", imageOptions); } diff --git a/spring-ai-model/src/test/java/org/springframework/ai/image/observation/ImageModelObservationContextTests.java b/spring-ai-model/src/test/java/org/springframework/ai/image/observation/ImageModelObservationContextTests.java index 36b06a1f52c..cfda9898b93 100644 --- a/spring-ai-model/src/test/java/org/springframework/ai/image/observation/ImageModelObservationContextTests.java +++ b/spring-ai-model/src/test/java/org/springframework/ai/image/observation/ImageModelObservationContextTests.java @@ -23,6 +23,7 @@ import org.springframework.ai.image.ImagePrompt; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; /** * Unit tests for {@link ImageModelObservationContext}. @@ -41,6 +42,75 @@ void whenMandatoryRequestOptionsThenReturn() { assertThat(observationContext).isNotNull(); } + @Test + void shouldBuildContextWithImageOptions() { + var imageOptions = ImageOptionsBuilder.builder().model("test-model").build(); + var imagePrompt = new ImagePrompt("test prompt", imageOptions); + + var observationContext = ImageModelObservationContext.builder() + .imagePrompt(imagePrompt) + .provider("test-provider") + .build(); + + assertThat(observationContext).isNotNull(); + } + + @Test + void shouldThrowExceptionWhenImagePromptIsNull() { + assertThatThrownBy( + () -> ImageModelObservationContext.builder().imagePrompt(null).provider("test-provider").build()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("request cannot be null"); + } + + @Test + void shouldThrowExceptionWhenProviderIsNull() { + var imagePrompt = new ImagePrompt("test prompt"); + + assertThatThrownBy(() -> ImageModelObservationContext.builder().imagePrompt(imagePrompt).provider(null).build()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("provider cannot be null or empty"); + } + + @Test + void shouldThrowExceptionWhenProviderIsEmpty() { + var imagePrompt = new ImagePrompt("test prompt"); + + assertThatThrownBy(() -> ImageModelObservationContext.builder().imagePrompt(imagePrompt).provider("").build()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("provider cannot be null or empty"); + } + + @Test + void shouldThrowExceptionWhenProviderIsBlank() { + var imagePrompt = new ImagePrompt("test prompt"); + + assertThatThrownBy( + () -> ImageModelObservationContext.builder().imagePrompt(imagePrompt).provider(" ").build()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("provider cannot be null or empty"); + } + + @Test + void shouldBuildMultipleContextsIndependently() { + var imagePrompt1 = new ImagePrompt("first prompt"); + var imagePrompt2 = new ImagePrompt("second prompt"); + + var context1 = ImageModelObservationContext.builder() + .imagePrompt(imagePrompt1) + .provider("provider-alpha") + .build(); + + var context2 = ImageModelObservationContext.builder() + .imagePrompt(imagePrompt2) + .provider("provider-beta") + .build(); + + assertThat(context1).isNotNull(); + assertThat(context2).isNotNull(); + assertThat(context1).isNotEqualTo(context2); + } + private ImagePrompt generateImagePrompt(ImageOptions imageOptions) { return new ImagePrompt("here comes the sun"); } diff --git a/spring-ai-model/src/test/java/org/springframework/ai/tool/function/FunctionToolCallbackTest.java b/spring-ai-model/src/test/java/org/springframework/ai/tool/function/FunctionToolCallbackTest.java index 707ab490154..4c3361d25e0 100644 --- a/spring-ai-model/src/test/java/org/springframework/ai/tool/function/FunctionToolCallbackTest.java +++ b/spring-ai-model/src/test/java/org/springframework/ai/tool/function/FunctionToolCallbackTest.java @@ -138,6 +138,18 @@ void testThrowToolExecutionException() { .isInstanceOf(ToolExecutionException.class); } + @Test + void testEmptyStringInput() { + TestFunctionTool tool = new TestFunctionTool(); + FunctionToolCallback callback = FunctionToolCallback.builder("testTool", tool.stringConsumer()) + .description("test empty string") + .inputType(String.class) + .build(); + + callback.call("\"\""); + assertEquals("", tool.calledValue.get()); + } + static class TestFunctionTool { AtomicReference calledValue = new AtomicReference<>();