Skip to content

Commit 1d11337

Browse files
committed
test: Add comprehensive validation tests for OpenAI runtime hints registration
Adds new tests to ensure OpenAI runtime hints are properly registered and stream functionality Co-authored-by: Oleksandr Klymenko <[email protected]> Signed-off-by: Oleksandr Klymenko <[email protected]>
1 parent 27b09fe commit 1d11337

File tree

2 files changed

+166
-1
lines changed

2 files changed

+166
-1
lines changed

models/spring-ai-openai/src/test/java/org/springframework/ai/openai/aot/OpenAiRuntimeHintsTests.java

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,4 +189,127 @@ void verifyNestedClassesAreRegistered() {
189189
assertThat(registeredTypes.contains(TypeReference.of(OpenAiApi.FunctionTool.Function.class))).isTrue();
190190
}
191191

192+
@Test
193+
void verifyPackageSpecificity() {
194+
Set<TypeReference> jsonAnnotatedClasses = findJsonAnnotatedClassesInPackage("org.springframework.ai.openai");
195+
196+
// All found classes should be from the openai package specifically
197+
for (TypeReference classRef : jsonAnnotatedClasses) {
198+
assertThat(classRef.getName()).startsWith("org.springframework.ai.openai");
199+
}
200+
201+
// Should not include classes from other AI packages
202+
for (TypeReference classRef : jsonAnnotatedClasses) {
203+
assertThat(classRef.getName()).doesNotContain("anthropic");
204+
assertThat(classRef.getName()).doesNotContain("vertexai");
205+
assertThat(classRef.getName()).doesNotContain("ollama");
206+
}
207+
}
208+
209+
@Test
210+
void verifyConsistencyAcrossInstances() {
211+
RuntimeHints hints1 = new RuntimeHints();
212+
RuntimeHints hints2 = new RuntimeHints();
213+
214+
OpenAiRuntimeHints openaiHints1 = new OpenAiRuntimeHints();
215+
OpenAiRuntimeHints openaiHints2 = new OpenAiRuntimeHints();
216+
217+
openaiHints1.registerHints(hints1, null);
218+
openaiHints2.registerHints(hints2, null);
219+
220+
// Different instances should register the same hints
221+
Set<TypeReference> types1 = new HashSet<>();
222+
Set<TypeReference> types2 = new HashSet<>();
223+
224+
hints1.reflection().typeHints().forEach(hint -> types1.add(hint.getType()));
225+
hints2.reflection().typeHints().forEach(hint -> types2.add(hint.getType()));
226+
227+
assertThat(types1).isEqualTo(types2);
228+
}
229+
230+
@Test
231+
void verifySpecificApiClassDetails() {
232+
openAiRuntimeHints.registerHints(runtimeHints, null);
233+
234+
Set<TypeReference> registeredTypes = new HashSet<>();
235+
runtimeHints.reflection().typeHints().forEach(typeHint -> registeredTypes.add(typeHint.getType()));
236+
237+
// Verify critical OpenAI API classes are registered
238+
assertThat(registeredTypes.contains(TypeReference.of(OpenAiApi.class))).isTrue();
239+
assertThat(registeredTypes.contains(TypeReference.of(OpenAiAudioApi.class))).isTrue();
240+
assertThat(registeredTypes.contains(TypeReference.of(OpenAiImageApi.class))).isTrue();
241+
242+
// Verify important nested/inner classes
243+
boolean containsChatCompletion = registeredTypes.stream()
244+
.anyMatch(typeRef -> typeRef.getName().contains("ChatCompletion"));
245+
assertThat(containsChatCompletion).isTrue();
246+
247+
boolean containsFunctionTool = registeredTypes.stream()
248+
.anyMatch(typeRef -> typeRef.getName().contains("FunctionTool"));
249+
assertThat(containsFunctionTool).isTrue();
250+
}
251+
252+
@Test
253+
void verifyClassLoaderIndependence() {
254+
RuntimeHints hintsWithNull = new RuntimeHints();
255+
RuntimeHints hintsWithClassLoader = new RuntimeHints();
256+
257+
ClassLoader customClassLoader = Thread.currentThread().getContextClassLoader();
258+
259+
openAiRuntimeHints.registerHints(hintsWithNull, null);
260+
openAiRuntimeHints.registerHints(hintsWithClassLoader, customClassLoader);
261+
262+
// Both should register the same types regardless of ClassLoader
263+
Set<TypeReference> typesWithNull = new HashSet<>();
264+
Set<TypeReference> typesWithClassLoader = new HashSet<>();
265+
266+
hintsWithNull.reflection().typeHints().forEach(hint -> typesWithNull.add(hint.getType()));
267+
hintsWithClassLoader.reflection().typeHints().forEach(hint -> typesWithClassLoader.add(hint.getType()));
268+
269+
assertThat(typesWithNull).isEqualTo(typesWithClassLoader);
270+
}
271+
272+
@Test
273+
void verifyAllApiModulesAreIncluded() {
274+
openAiRuntimeHints.registerHints(runtimeHints, null);
275+
276+
Set<TypeReference> registeredTypes = new HashSet<>();
277+
runtimeHints.reflection().typeHints().forEach(typeHint -> registeredTypes.add(typeHint.getType()));
278+
279+
// Verify all main OpenAI API modules are represented
280+
boolean hasMainApi = registeredTypes.stream().anyMatch(typeRef -> typeRef.getName().contains("OpenAiApi"));
281+
boolean hasAudioApi = registeredTypes.stream()
282+
.anyMatch(typeRef -> typeRef.getName().contains("OpenAiAudioApi"));
283+
boolean hasImageApi = registeredTypes.stream()
284+
.anyMatch(typeRef -> typeRef.getName().contains("OpenAiImageApi"));
285+
boolean hasChatOptions = registeredTypes.stream()
286+
.anyMatch(typeRef -> typeRef.getName().contains("OpenAiChatOptions"));
287+
288+
assertThat(hasMainApi).isTrue();
289+
assertThat(hasAudioApi).isTrue();
290+
assertThat(hasImageApi).isTrue();
291+
assertThat(hasChatOptions).isTrue();
292+
}
293+
294+
@Test
295+
void verifyJsonAnnotatedClassesContainCriticalTypes() {
296+
Set<TypeReference> jsonAnnotatedClasses = findJsonAnnotatedClassesInPackage("org.springframework.ai.openai");
297+
298+
// Verify that critical OpenAI types are found
299+
boolean containsApiClass = jsonAnnotatedClasses.stream()
300+
.anyMatch(typeRef -> typeRef.getName().contains("OpenAiApi") || typeRef.getName().contains("ChatCompletion")
301+
|| typeRef.getName().contains("OpenAiChatOptions"));
302+
303+
assertThat(containsApiClass).isTrue();
304+
305+
// Verify audio and image API classes are found
306+
boolean containsAudioApi = jsonAnnotatedClasses.stream()
307+
.anyMatch(typeRef -> typeRef.getName().contains("AudioApi"));
308+
boolean containsImageApi = jsonAnnotatedClasses.stream()
309+
.anyMatch(typeRef -> typeRef.getName().contains("ImageApi"));
310+
311+
assertThat(containsAudioApi).isTrue();
312+
assertThat(containsImageApi).isTrue();
313+
}
314+
192315
}

models/spring-ai-openai/src/test/java/org/springframework/ai/openai/api/OpenAiStreamFunctionCallingHelperTest.java

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,4 +162,46 @@ public void chunkToChatCompletion_whenInputIsValid() {
162162
assertThat(result.choices()).hasSize(2);
163163
}
164164

165-
}
165+
@Test
166+
public void mergeCombinesChunkFieldsCorrectly() {
167+
var previous = new OpenAiApi.ChatCompletionChunk(null, null, 123456789L, "gpt-4", "default", null, null, null);
168+
var current = new OpenAiApi.ChatCompletionChunk("chat-123", Collections.emptyList(), null, null, null, "fp-456",
169+
"chat.completion.chunk", null);
170+
171+
var result = helper.merge(previous, current);
172+
173+
assertThat(result.id()).isEqualTo("chat-123");
174+
assertThat(result.created()).isEqualTo(123456789L);
175+
assertThat(result.model()).isEqualTo("gpt-4");
176+
assertThat(result.systemFingerprint()).isEqualTo("fp-456");
177+
}
178+
179+
@Test
180+
public void isStreamingToolFunctionCallReturnsFalseForNullOrEmptyChunks() {
181+
assertThat(helper.isStreamingToolFunctionCall(null)).isFalse();
182+
183+
var emptyChunk = new OpenAiApi.ChatCompletionChunk(null, Collections.emptyList(), null, null, null, null, null,
184+
null);
185+
assertThat(helper.isStreamingToolFunctionCall(emptyChunk)).isFalse();
186+
}
187+
188+
@Test
189+
public void isStreamingToolFunctionCall_returnsTrueForValidToolCalls() {
190+
var toolCall = Mockito.mock(OpenAiApi.ChatCompletionMessage.ToolCall.class);
191+
var delta = new OpenAiApi.ChatCompletionMessage(null, null, null, null, List.of(toolCall), null, null, null);
192+
var choice = new OpenAiApi.ChatCompletionChunk.ChunkChoice(null, null, delta, null);
193+
var chunk = new OpenAiApi.ChatCompletionChunk(null, List.of(choice), null, null, null, null, null, null);
194+
195+
assertThat(helper.isStreamingToolFunctionCall(chunk)).isTrue();
196+
}
197+
198+
@Test
199+
public void isStreamingToolFunctionCallFinishDetectsToolCallsFinishReason() {
200+
var choice = new OpenAiApi.ChatCompletionChunk.ChunkChoice(OpenAiApi.ChatCompletionFinishReason.TOOL_CALLS,
201+
null, new OpenAiApi.ChatCompletionMessage(null, null), null);
202+
var chunk = new OpenAiApi.ChatCompletionChunk(null, List.of(choice), null, null, null, null, null, null);
203+
204+
assertThat(helper.isStreamingToolFunctionCallFinish(chunk)).isTrue();
205+
}
206+
207+
}

0 commit comments

Comments
 (0)