diff --git a/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/OpenAiChatOptionsTests.java b/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/OpenAiChatOptionsTests.java index 70b7f1fad66..5b4dbb2f500 100644 --- a/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/OpenAiChatOptionsTests.java +++ b/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/OpenAiChatOptionsTests.java @@ -280,4 +280,173 @@ void testFromOptions_webSearchOptions() { assertThat(target.getWebSearchOptions().userLocation().approximate().timezone()).isEqualTo("UTC+8"); } + @Test + void testEqualsAndHashCode() { + OpenAiChatOptions options1 = OpenAiChatOptions.builder() + .model("test-model") + .temperature(0.7) + .maxTokens(100) + .build(); + + OpenAiChatOptions options2 = OpenAiChatOptions.builder() + .model("test-model") + .temperature(0.7) + .maxTokens(100) + .build(); + + OpenAiChatOptions options3 = OpenAiChatOptions.builder() + .model("different-model") + .temperature(0.7) + .maxTokens(100) + .build(); + + // Test equals + assertThat(options1).isEqualTo(options2); + assertThat(options1).isNotEqualTo(options3); + assertThat(options1).isNotEqualTo(null); + assertThat(options1).isEqualTo(options1); + + // Test hashCode + assertThat(options1.hashCode()).isEqualTo(options2.hashCode()); + assertThat(options1.hashCode()).isNotEqualTo(options3.hashCode()); + } + + @Test + void testBuilderWithNullValues() { + OpenAiChatOptions options = OpenAiChatOptions.builder() + .temperature(null) + .logitBias(null) + .stop(null) + .tools(null) + .metadata(null) + .build(); + + assertThat(options.getModel()).isNull(); + assertThat(options.getTemperature()).isNull(); + assertThat(options.getLogitBias()).isNull(); + assertThat(options.getStop()).isNull(); + assertThat(options.getTools()).isNull(); + assertThat(options.getMetadata()).isNull(); + } + + @Test + void testBuilderChaining() { + OpenAiChatOptions.Builder builder = OpenAiChatOptions.builder(); + + OpenAiChatOptions.Builder result = builder.model("test-model").temperature(0.7).maxTokens(100); + + assertThat(result).isSameAs(builder); + + OpenAiChatOptions options = result.build(); + assertThat(options.getModel()).isEqualTo("test-model"); + assertThat(options.getTemperature()).isEqualTo(0.7); + assertThat(options.getMaxTokens()).isEqualTo(100); + } + + @Test + void testNullAndEmptyCollections() { + OpenAiChatOptions options = new OpenAiChatOptions(); + + // Test setting null collections + options.setLogitBias(null); + options.setStop(null); + options.setTools(null); + options.setMetadata(null); + options.setOutputModalities(null); + + assertThat(options.getLogitBias()).isNull(); + assertThat(options.getStop()).isNull(); + assertThat(options.getTools()).isNull(); + assertThat(options.getMetadata()).isNull(); + assertThat(options.getOutputModalities()).isNull(); + + // Test setting empty collections + options.setLogitBias(new HashMap<>()); + options.setStop(new ArrayList<>()); + options.setTools(new ArrayList<>()); + options.setMetadata(new HashMap<>()); + options.setOutputModalities(new ArrayList<>()); + + assertThat(options.getLogitBias()).isEmpty(); + assertThat(options.getStop()).isEmpty(); + assertThat(options.getTools()).isEmpty(); + assertThat(options.getMetadata()).isEmpty(); + assertThat(options.getOutputModalities()).isEmpty(); + } + + @Test + void testStreamUsageStreamOptionsInteraction() { + OpenAiChatOptions options = new OpenAiChatOptions(); + + // Initially false + assertThat(options.getStreamUsage()).isFalse(); + assertThat(options.getStreamOptions()).isNull(); + + // Setting streamUsage to true should set streamOptions + options.setStreamUsage(true); + assertThat(options.getStreamUsage()).isTrue(); + assertThat(options.getStreamOptions()).isEqualTo(StreamOptions.INCLUDE_USAGE); + + // Setting streamUsage to false should clear streamOptions + options.setStreamUsage(false); + assertThat(options.getStreamUsage()).isFalse(); + assertThat(options.getStreamOptions()).isNull(); + + // Setting streamOptions directly should update streamUsage + options.setStreamOptions(StreamOptions.INCLUDE_USAGE); + assertThat(options.getStreamUsage()).isTrue(); + assertThat(options.getStreamOptions()).isEqualTo(StreamOptions.INCLUDE_USAGE); + + // Setting streamOptions to null should set streamUsage to false + options.setStreamOptions(null); + assertThat(options.getStreamUsage()).isFalse(); + assertThat(options.getStreamOptions()).isNull(); + } + + @Test + void testStopSequencesAlias() { + OpenAiChatOptions options = new OpenAiChatOptions(); + List stopSequences = List.of("stop1", "stop2"); + + // Setting stopSequences should also set stop + options.setStopSequences(stopSequences); + assertThat(options.getStopSequences()).isEqualTo(stopSequences); + assertThat(options.getStop()).isEqualTo(stopSequences); + + // Setting stop should also update stopSequences + List newStop = List.of("stop3", "stop4"); + options.setStop(newStop); + assertThat(options.getStop()).isEqualTo(newStop); + assertThat(options.getStopSequences()).isEqualTo(newStop); + } + + @Test + void testFromOptionsWithWebSearchOptionsNull() { + OpenAiChatOptions source = OpenAiChatOptions.builder() + .model("test-model") + .temperature(0.7) + .webSearchOptions(null) + .build(); + + OpenAiChatOptions result = OpenAiChatOptions.fromOptions(source); + assertThat(result.getModel()).isEqualTo("test-model"); + assertThat(result.getTemperature()).isEqualTo(0.7); + assertThat(result.getWebSearchOptions()).isNull(); + } + + @Test + void testCopyChangeIndependence() { + OpenAiChatOptions original = OpenAiChatOptions.builder().model("original-model").temperature(0.5).build(); + + OpenAiChatOptions copied = original.copy(); + + // Modify original + original.setModel("modified-model"); + original.setTemperature(0.9); + + // Verify copy is unchanged + assertThat(copied.getModel()).isEqualTo("original-model"); + assertThat(copied.getTemperature()).isEqualTo(0.5); + } + }