diff --git a/core/src/test/java/com/google/adk/models/ClaudeGenerateContentTest.java b/core/src/test/java/com/google/adk/models/ClaudeGenerateContentTest.java new file mode 100644 index 000000000..140ccf66b --- /dev/null +++ b/core/src/test/java/com/google/adk/models/ClaudeGenerateContentTest.java @@ -0,0 +1,863 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +// ********RoostGPT******** +/* +Test generated by RoostGPT for test adk-java-demo using AI Type Azure Open AI and AI Model gpt-4.1 + +ROOST_METHOD_HASH=generateContent_bc4a182290 +ROOST_METHOD_SIG_HASH=generateContent_c08b9769fe + +Here are your existing test cases which we found out and are not considered for test generation: + +File Path: C:\var\tmp\Roost\RoostGPT\adk-java-demo\1779694771\source\adk-java\core\src\test\java\com\google\adk\models\ApigeeLlmTest.java +Tests: + "@Test +public void generateContent_stripsApigeePrefixAndSendsToDelegate() { + when(mockGeminiDelegate.generateContent(any(), anyBoolean())).thenReturn(Flowable.empty()); + ApigeeLlm llm = new ApigeeLlm("apigee/gemini/v1/gemini-1.5-flash", mockGeminiDelegate); + LlmRequest request = LlmRequest.builder().model("apigee/gemini/v1/gemini-1.5-flash").contents(ImmutableList.of(Content.builder().parts(Part.fromText("hi")).build())).build(); + llm.generateContent(request, true).test().assertNoErrors(); + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(LlmRequest.class); + verify(mockGeminiDelegate).generateContent(requestCaptor.capture(), eq(true)); + assertThat(requestCaptor.getValue().model()).hasValue("gemini-1.5-flash"); + } +" + "@Test +public void build_withTrailingSlashInModel_parsesVersionAndModelId() { + when(mockGeminiDelegate.generateContent(any(), anyBoolean())).thenReturn(Flowable.empty()); + ApigeeLlm llm = new ApigeeLlm("apigee/gemini/v1/", mockGeminiDelegate); + LlmRequest request = LlmRequest.builder().contents(ImmutableList.of(Content.builder().parts(Part.fromText("hi")).build())).build(); + assertThrows(IllegalArgumentException.class, () -> llm.generateContent(request, false)); + verify(mockGeminiDelegate, never()).generateContent(any(), anyBoolean()); + } +"Scenario 1: Basic Content Generation with Minimal Parameters + +Details: + TestName: generateContentWithMinimalParameters + Description: Verifies if the generateContent method correctly processes an LlmRequest that only contains a basic content part with no additional configuration, tools, or system instructions. This scenario checks that the method handles default values and essential flows as expected for a minimal request. +Execution: + Arrange: Mock the AnthropicClient, Message, and all dependent objects. Prepare an LlmRequest with a single content containing a simple text part, no configuration, tools, or model specified. + Act: Call the generateContent method with the prepared LlmRequest and stream set to false. + Assert: Check that the Flowable emits an LlmResponse containing the correct content, with the proper role ("model") and the provided text part. +Validation: + The assertion verifies that basic text generation works for non-complex requests, confirming the fundamental functionality of the method and its ability to process default parameters. + +Scenario 2: Content Generation with Explicit Model Provided in LlmRequest + +Details: + TestName: generateContentWithExplicitModel + Description: Tests the scenario where an explicit model name is set in the LlmRequest. Checks if the method uses this model instead of the default Claude model and correctly incorporates it into the MessageCreateParams. +Execution: + Arrange: Set up mocks for AnthropicClient and related objects. Create an LlmRequest with a specified model value and simple content. + Act: Invoke generateContent with this LlmRequest and streaming set to true. + Assert: Verify that the emitted LlmResponse reflects the usage of the specified model and the returned content matches expectations. +Validation: + This assertion ensures model priority is respected, validating that the code prefers user-specified models over defaults. + +Scenario 3: Content Generation with Tools Configuration + +Details: + TestName: generateContentWithToolsConfiguration + Description: Assesses whether generateContent accurately processes LlmRequest config containing tool declarations and converts them into Anthropic ToolUnion objects, then attaches them to the MessageCreateParams. +Execution: + Arrange: Mock tool-related objects and AnthropicClient. Construct an LlmRequest with config holding tools and at least one function declaration. + Act: Call generateContent with this request and stream set to false. + Assert: Confirm the Flowable emits an LlmResponse, and that the MessageCreateParams includes the tool information translated from input. +Validation: + This test confirms tool declaration logic is functional and tools are mapped correctly, critical for advanced use cases with tool instructions. + +Scenario 4: Content Generation Without Tools when Config Indicates Empty Tools List + +Details: + TestName: generateContentWithoutToolsWhenEmptyConfig + Description: Ensures the method does not attempt to include tools or toolChoice when LlmRequest config exists but tools().get() is empty or not present. +Execution: + Arrange: Mock required dependencies. Compose an LlmRequest whose config is present, but tools is empty. + Act: Invoke generateContent using the above input and stream=false. + Assert: Assert that no tools are included in the MessageCreateParams and the output LlmResponse is correct. +Validation: + The assertion prevents accidental tool inclusion, validating proper handling of empty tool configurations. + +Scenario 5: Content Generation with System Instructions Present in Config + +Details: + TestName: generateContentWithSystemInstructions + Description: Validate that when LlmRequest config contains a systemInstruction, the systemText is extracted and correctly included in MessageCreateParams. +Execution: + Arrange: Mock AnthropicClient and Message, form LlmRequest config with a Content containing systemInstruction parts with text values. + Act: Execute generateContent with that input (stream can be true or false). + Assert: Check that the emitted LlmResponse content includes the expected systemText and overall structure. +Validation: + Confirms the ability to incorporate system instructions into content generation, necessary for prompt customization scenarios. + +Scenario 6: Content Generation When System Instructions Are Empty + +Details: + TestName: generateContentWithEmptySystemInstructions + Description: Ensures that if systemInstruction parts have no text or are empty, the systemText defaults to empty and MessageCreateParams handles it accordingly. +Execution: + Arrange: Mock everything needed, create an LlmRequest whose config contains systemInstruction with parts but none have text. + Act: Call generateContent with this LlmRequest. + Assert: Check that systemText in MessageCreateParams is empty and LlmResponse does not reflect unwanted system instructions. +Validation: + This test validates correct handling of empty instruction, avoiding unintended prompt injection and ensuring robustness. + +Scenario 7: Content Generation When Tools List Provided in LlmRequest.tools() + +Details: + TestName: generateContentWithToolsListProvided + Description: Tests whether a non-empty tools() list in the LlmRequest results in toolChoice being set with disableParallelToolUse true. +Execution: + Arrange: Use mocks and provide an LlmRequest with LlmRequest.tools() non-empty, and other parameters as necessary. + Act: Invoke generateContent. + Assert: Assert that the toolChoice appears in MessageCreateParams and LlmResponse is emitted as expected. +Validation: + Validates ability to correctly process situations where tools are specified directly, ensuring that toolChoice logic does not overlook user-specified scenarios. + +Scenario 8: Content Generation with Multiple Contents in LlmRequest + +Details: + TestName: generateContentWithMultipleContents + Description: Checks that multiple Content instances in the contents() list are appropriately processed into MessageParam objects and included in the request. +Execution: + Arrange: Mock dependencies. Supply an LlmRequest with several contents, varying roles and text. + Act: Call generateContent. + Assert: Validate that the output LlmResponse contains all expected parts. +Validation: + Ensures support for batch messaging, confirming method's correct processing of multiple input prompts. + +Scenario 9: Content Generation When Message Returned by AnthropicClient Is Null + +Details: + TestName: generateContentWithNullMessageFromClient + Description: Simulates a case where the client returns a null message. Confirms that the method and its internal conversion gracefully handle a null response and do not produce unexpected errors or content. +Execution: + Arrange: Mock the AnthropicClient to return null for messages().create. Input a valid LlmRequest. + Act: Invoke generateContent. + Assert: Check that the Flowable emits an LlmResponse with appropriate defaults or empty content. +Validation: + Critical for resilience to upstream API anomalies, ensuring the app fails gracefully. + +Scenario 10: Content Generation Where Content Includes FunctionCall Part + +Details: + TestName: generateContentWithFunctionCallPart + Description: Verifies that if the Content part in LlmRequest contains a functionCall, the contentToAnthropicMessageParam translates it into a ToolUseBlockParam, and it is correctly included. +Execution: + Arrange: Mock all related objects. Supply an LlmRequest part containing a functionCall. + Act: Execute generateContent. + Assert: Check that ToolUseBlockParam appears in MessageCreateParams and final LlmResponse. +Validation: + Ensures accurate translation of function call prompts, key for function-driven generative tasks. + +Scenario 11: Content Generation Where Content Includes FunctionResponse Part + +Details: + TestName: generateContentWithFunctionResponsePart + Description: Tests handling of parts containing functionResponse and checks that corresponding ToolResultBlockParam is generated. +Execution: + Arrange: Mock objects, input LlmRequest part with a valid functionResponse. + Act: Call generateContent. + Assert: Validate ToolResultBlockParam inclusion and correct mapping in LlmResponse. +Validation: + Necessary for completion of function call lifecycle, ensuring the platform can process responses properly. + +Scenario 12: Content Generation When LlmRequest.contents() Is Empty + +Details: + TestName: generateContentWithEmptyContents + Description: Tests the behavior when LlmRequest.contents() is empty, ensuring method processes gracefully and returns LlmResponse as appropriate. +Execution: + Arrange: Mock all dependencies. Provide an LlmRequest whose contents() is an empty ImmutableList. + Act: Call generateContent. + Assert: Assert that LlmResponse is emitted and does not contain unexpected parts. +Validation: + Essential for robust input handling, guaranteeing predictable output for edge input scenarios. + +Scenario 13: Content Generation with Maximum Token Limit Set + +Details: + TestName: generateContentWithMaxTokensConfigured + Description: Verifies that the maxTokens field is used when building MessageCreateParams and governs output structure. +Execution: + Arrange: Use constructor to set a specific maxTokens value; mock the rest. Create a basic LlmRequest. + Act: Call generateContent and record internal params. + Assert: Confirm maxTokens is set in MessageCreateParams and affects response accordingly. +Validation: + Ensures configuration parameter is honored, supporting customizable prompt constraints. + +Scenario 14: Content Generation When SystemInstruction Has Multiple Parts + +Details: + TestName: generateContentWithMultiPartSystemInstruction + Description: Checks if multiple parts in systemInstruction are joined properly into systemText, separated by newlines. +Execution: + Arrange: Mock and prepare LlmRequest config with systemInstruction containing several parts with different texts. + Act: Generate content and examine output. + Assert: Confirm that systemText includes all texts in correct order, separated as per logic. +Validation: + Verifies correct concatenation for complex instructions, critical for nuanced prompt engineering. + +Scenario 15: Content Generation When Message Content Is Missing + +Details: + TestName: generateContentWithMissingMessageContent + Description: Simulates a scenario where the returned Message from AnthropicClient has a null content property, ensuring convertAnthropicResponseToLlmResponse manages it gracefully. +Execution: + Arrange: Mock AnthropicClient to provide Message with null content. Prepare a simple LlmRequest. + Act: Call generateContent. + Assert: Validate that LlmResponse does not throw or include erroneous parts. +Validation: + Confirms defensive code for absent API results, enhancing system stability. + +Scenario 16: Content Generation Using Default Model When LlmRequest.model() Is Not Provided + +Details: + TestName: generateContentUsesDefaultModelWhenNoneProvided + Description: Ensures that if LlmRequest.model() is absent, method falls back to the default Claude model initialized in the constructor. +Execution: + Arrange: Prepare LlmRequest without model, mock everything else. + Act: Call generateContent. + Assert: Verify default model is used in MessageCreateParams and LlmResponse. +Validation: + Validates default-flow logic, crucial for implicit usage scenarios. + +Scenario 17: Content Generation When LlmRequest.config() Is Not Present + +Details: + TestName: generateContentWithAbsentConfig + Description: Checks that absence of configuration in LlmRequest does not cause failures, and default values are used for systemText and tools. +Execution: + Arrange: Make an LlmRequest without config, mock as needed. + Act: Call generateContent. + Assert: Confirm full response is built using defaults. +Validation: + Affirms code's ability to handle missing configurations gracefully. + +Scenario 18: Content Generation With Stream Parameter True + +Details: + TestName: generateContentWithStreamTrue + Description: Confirms that regardless of the stream parameter, since the method is non-streaming, Flowable.just is always used, and the response is immediate. +Execution: + Arrange: Prepare a valid LlmRequest and mock client. Set stream parameter to true. + Act: Execute generateContent. + Assert: Output is a Flowable.just type with expected LlmResponse. +Validation: + Ensures streaming parameter does not impact method behavior, indicating current implementation supports only static generation. + +Scenario 19: Content Generation With Stream Parameter False + +Details: + TestName: generateContentWithStreamFalse + Description: Same as previous, but explicitly validates stream=false case, confirming method response type and speed. +Execution: + Arrange: Supply mocks and LlmRequest, set stream to false. + Act: Call generateContent. + Assert: Response type and content matches Flowable.just. +Validation: + Demonstrates method's API consistency for both values of stream. + +Scenario 20: Content Generation When MessageParam Conversion Throws Exception + +Details: + TestName: generateContentHandlesMessageParamConversionException + Description: Simulates a case where contentToAnthropicMessageParam or partToAnthropicMessageBlock encounters unsupported Part and throws UnsupportedOperationException. Confirms method does not result in unexpected output. +Execution: + Arrange: Provide LlmRequest with unsupported content part, mock all else. + Act: Attempt generateContent. + Assert: Exception is thrown and handled (if within test framework). +Validation: + Validates error handling and exception propagation, critical for developer experience and debugging. + +Scenario 21: Content Generation When FunctionDeclaration Lacks Required Fields + +Details: + TestName: generateContentWithIncompleteFunctionDeclaration + Description: Tests scenario where functionDeclaration lacks required fields (e.g., .name()), expecting method to throw an exception or fail gracefully. +Execution: + Arrange: Input config with tool and incomplete functionDeclaration, mock dependencies. + Act: Call generateContent. + Assert: Check for thrown exception or proper error signaling. +Validation: + Ensures robustness against malformed tool declarations, protecting against API error propagation. + +Scenario 22: Content Generation When SystemInstruction Parts Have Null Text + +Details: + TestName: generateContentIgnoresNullTextsInSystemInstruction + Description: Confirms that parts with null text in systemInstruction are ignored when extracting systemText. +Execution: + Arrange: LlmRequest config with systemInstruction containing parts with text and parts with null. + Act: Call generateContent. + Assert: Only non-null texts appear in systemText. +Validation: + Prevents erroneous instructions, maintaining clean prompt injection. + +Scenario 23: Content Generation When ToolChoice Is Null + +Details: + TestName: generateContentWithNullToolChoice + Description: Tests that if toolChoice is null (LlmRequest.tools() is empty), no toolChoice or tools are set in MessageCreateParams. +Execution: + Arrange: Compose LlmRequest with tools() empty, mock everything else. + Act: Invoke generateContent. + Assert: Confirm MessageCreateParams omits tools/toolChoice. +Validation: + Verifies correct branching logic, ensuring tools are only set when appropriate. + +Scenario 24: Content Generation With Large Number of Contents + +Details: + TestName: generateContentWithLargeContentList + Description: Checks scalability when a very large contents() list is provided, and all are mapped to MessageParam correctly. +Execution: + Arrange: Generate LlmRequest with dozens/hundreds of content parts, mock dependencies. + Act: Call generateContent. + Assert: LlmResponse contains all parts, no truncation or performance issue is observed. +Validation: + Important for batch processing or bulk prompt scenarios; tests method scalability. + +Scenario 25: Content Generation When Tools Config Contains Nested Properties + +Details: + TestName: generateContentWithToolsAndNestedProperties + Description: Tests robustness of functionDeclarationToAnthropicTool mapping logic with nested property schemas. +Execution: + Arrange: Use LlmRequest config containing function declaration with complex, nested properties. + Act: Call generateContent. + Assert: Tools mapping properly includes all nested structures in MessageCreateParams. +Validation: + Validates advanced schema processing, crucial for complex tool definitions. + +Scenario 26: Content Generation When Tools Config Lacks FunctionDeclarations + +Details: + TestName: generateContentWithToolsConfigLackingFunctionDeclarations + Description: Checks that when tools() in config exists but functionDeclarations() is absent, no tools are mapped or included. +Execution: + Arrange: LlmRequest config with tools, each lacking functionDeclarations. + Act: Call generateContent. + Assert: MessageCreateParams contains no tools; LlmResponse is generated as usual. +Validation: + Ensures defensive programming for incomplete tool deconfigurations. + +*/ + +// ********RoostGPT******** + +package com.google.adk.models; +import com.anthropic.client.AnthropicClient; +import com.anthropic.models.messages.ContentBlock; +import com.anthropic.models.messages.Message; +import com.anthropic.models.messages.MessageCreateParams; +import com.anthropic.models.messages.MessageParam; +import com.anthropic.models.messages.ToolUnion; +import com.anthropic.models.messages.ToolChoice; +import com.anthropic.models.messages.ToolChoiceAuto; +import com.anthropic.models.messages.Tool; +import com.anthropic.models.messages.ToolResultBlockParam; +import com.anthropic.models.messages.ToolUseBlockParam; +import com.anthropic.models.messages.ContentBlockParam; +import com.anthropic.models.messages.TextBlockParam; +import com.google.genai.types.Content; +import com.google.genai.types.Part; +import com.google.genai.types.FunctionCall; +import com.google.genai.types.FunctionDeclaration; +import com.google.genai.types.GenerateContentConfig; +import io.reactivex.rxjava3.core.Flowable; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import org.junit.jupiter.api.Assertions; +import java.util.*; +import java.util.stream.Collectors; +import org.junit.jupiter.api.*; +import com.anthropic.models.messages.MessageParam.Role; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class ClaudeGenerateContentTest extends BaseLlm { + @Mock + private AnthropicClient anthropicClient; + @Mock + private AnthropicClient.Messages messagesClient; + private Claude claude; + @BeforeEach + public void setup() { + MockitoAnnotations.openMocks(this); + claude = new Claude("claude-2", anthropicClient, 8192); + Mockito.when(anthropicClient.messages()).thenReturn(messagesClient); + } + @Test + @Tag("valid") + public void testGenerateContentWithMinimalParameters() { + Content content = Content.builder().role("user").parts(ImmutableList.of(Part.builder().text("Hello, Claude!").build())).build(); + LlmRequest llmRequest = LlmRequest.builder() + .contents(ImmutableList.of(content)) + .build(); + MessageParam messageParam = MessageParam.builder().role(MessageParam.Role.USER).content(ImmutableList.of(TextBlockParam.builder().text("Hello, Claude!").build())).build(); + MessageCreateParams createParams = MessageCreateParams.builder() + .model("claude-2") // default model + .system("") + .messages(ImmutableList.of(messageParam)) + .maxTokens(8192) + .build(); + ContentBlock contentBlock = Mockito.mock(ContentBlock.class); + Mockito.when(contentBlock.text()).thenReturn(Optional.of("Hello, Claude!")); + Message message = Mockito.mock(Message.class); + Mockito.when(message.content()).thenReturn(ImmutableList.of(contentBlock)); + Mockito.when(messagesClient.create(any(MessageCreateParams.class))).thenReturn(message); + Flowable flowable = claude.generateContent(llmRequest, false); + List responses = flowable.toList().blockingGet(); + LlmResponse actualResponse = responses.get(0); + Assertions.assertEquals("model", actualResponse.content().get().role(), "Role should be 'model'"); + Assertions.assertEquals("Hello, Claude!", actualResponse.content().get().parts().get().get(0).text().get()); + } + @Test + @Tag("valid") + public void testGenerateContentWithExplicitModel() { + Content content = Content.builder().role("user").parts(ImmutableList.of(Part.builder().text("Custom model query").build())).build(); + LlmRequest llmRequest = LlmRequest.builder() + .model(Optional.of("custom-model")) + .contents(ImmutableList.of(content)) + .build(); + MessageParam messageParam = MessageParam.builder().role(MessageParam.Role.USER).content(ImmutableList.of(TextBlockParam.builder().text("Custom model query").build())).build(); + MessageCreateParams createParams = MessageCreateParams.builder() + .model("custom-model") + .system("") + .messages(ImmutableList.of(messageParam)) + .maxTokens(8192) + .build(); + ContentBlock contentBlock = Mockito.mock(ContentBlock.class); + Mockito.when(contentBlock.text()).thenReturn(Optional.of("Custom model query")); + Message message = Mockito.mock(Message.class); + Mockito.when(message.content()).thenReturn(ImmutableList.of(contentBlock)); + Mockito.when(messagesClient.create(any(MessageCreateParams.class))).thenReturn(message); + Flowable flowable = claude.generateContent(llmRequest, true); + List responses = flowable.toList().blockingGet(); + LlmResponse actualResponse = responses.get(0); + Assertions.assertEquals("model", actualResponse.content().get().role(), "Role should be 'model'"); + Assertions.assertEquals("Custom model query", actualResponse.content().get().parts().get().get(0).text().get()); + } + @Test + @Tag("integration") + public void testGenerateContentWithToolsConfiguration() { + FunctionDeclaration functionDecl = Mockito.mock(FunctionDeclaration.class); + Mockito.when(functionDecl.name()).thenReturn(Optional.of("func1")); + GenerateContentConfig.ToolsConfig toolsConfig = Mockito.mock(GenerateContentConfig.ToolsConfig.class); + Mockito.when(toolsConfig.functionDeclarations()).thenReturn(Optional.of(ImmutableList.of(functionDecl))); + GenerateContentConfig config = Mockito.mock(GenerateContentConfig.class); + Mockito.when(config.tools()).thenReturn(Optional.of(ImmutableList.of(toolsConfig))); + Content content = Content.builder().role("user").parts(ImmutableList.of(Part.builder().text("Tool question").build())).build(); + LlmRequest llmRequest = LlmRequest.builder() + .contents(ImmutableList.of(content)) + .config(Optional.of(config)) + .build(); + MessageParam messageParam = MessageParam.builder().role(MessageParam.Role.USER).content(ImmutableList.of(TextBlockParam.builder().text("Tool question").build())).build(); + Tool tool = Mockito.mock(Tool.class); + ToolUnion toolUnion = ToolUnion.ofTool(tool); + ToolChoice toolChoice = ToolChoice.ofAuto(ToolChoiceAuto.builder().disableParallelToolUse(true).build()); + MessageCreateParams.Builder paramsBuilder = + MessageCreateParams.builder().model("claude-2").system("").messages(ImmutableList.of(messageParam)).maxTokens(8192); + paramsBuilder.tools(ImmutableList.of(toolUnion)); + paramsBuilder.toolChoice(toolChoice); + Message message = Mockito.mock(Message.class); + ContentBlock contentBlock = Mockito.mock(ContentBlock.class); + Mockito.when(contentBlock.text()).thenReturn(Optional.of("Tool question")); + Mockito.when(message.content()).thenReturn(ImmutableList.of(contentBlock)); + Mockito.when(messagesClient.create(any(MessageCreateParams.class))).thenReturn(message); + Flowable flowable = claude.generateContent(llmRequest, false); + List responses = flowable.toList().blockingGet(); + Assertions.assertEquals("Tool question", responses.get(0).content().get().parts().get().get(0).text().get()); + } + @Test + @Tag("valid") + public void testGenerateContentWithoutToolsWhenEmptyConfig() { + GenerateContentConfig.ToolsConfig toolsConfig = Mockito.mock(GenerateContentConfig.ToolsConfig.class); + Mockito.when(toolsConfig.functionDeclarations()).thenReturn(Optional.empty()); + GenerateContentConfig config = Mockito.mock(GenerateContentConfig.class); + Mockito.when(config.tools()).thenReturn(Optional.of(ImmutableList.of(toolsConfig))); + Content content = Content.builder().role("user").parts(ImmutableList.of(Part.builder().text("No Tools Request").build())).build(); + LlmRequest llmRequest = LlmRequest.builder().contents(ImmutableList.of(content)).config(Optional.of(config)).build(); + MessageParam messageParam = MessageParam.builder().role(MessageParam.Role.USER).content(ImmutableList.of(TextBlockParam.builder().text("No Tools Request").build())).build(); + Message message = Mockito.mock(Message.class); + ContentBlock contentBlock = Mockito.mock(ContentBlock.class); + Mockito.when(contentBlock.text()).thenReturn(Optional.of("No Tools Request")); + Mockito.when(message.content()).thenReturn(ImmutableList.of(contentBlock)); + Mockito.when(messagesClient.create(any(MessageCreateParams.class))).thenReturn(message); + Flowable flowable = claude.generateContent(llmRequest, false); + List responses = flowable.toList().blockingGet(); + Assertions.assertEquals("No Tools Request", responses.get(0).content().get().parts().get().get(0).text().get()); + } + @Test + @Tag("valid") + public void testGenerateContentWithSystemInstructions() { + Part systemPart = Part.builder().text("System prompt line").build(); + Content systemContent = Content.builder().role("system").parts(ImmutableList.of(systemPart)).build(); + GenerateContentConfig config = Mockito.mock(GenerateContentConfig.class); + Mockito.when(config.systemInstruction()).thenReturn(Optional.of(systemContent)); + Content content = Content.builder().role("user").parts(ImmutableList.of(Part.builder().text("User query").build())).build(); + LlmRequest llmRequest = LlmRequest.builder().contents(ImmutableList.of(content)).config(Optional.of(config)).build(); + MessageParam messageParam = MessageParam.builder().role(MessageParam.Role.USER).content(ImmutableList.of(TextBlockParam.builder().text("User query").build())).build(); + Message message = Mockito.mock(Message.class); + ContentBlock contentBlock = Mockito.mock(ContentBlock.class); + Mockito.when(contentBlock.text()).thenReturn(Optional.of("User query")); + Mockito.when(message.content()).thenReturn(ImmutableList.of(contentBlock)); + Mockito.when(messagesClient.create(any(MessageCreateParams.class))).thenReturn(message); + Flowable flowable = claude.generateContent(llmRequest, true); + List responses = flowable.toList().blockingGet(); + Assertions.assertEquals("User query", responses.get(0).content().get().parts().get().get(0).text().get()); + } + @Test + @Tag("valid") + public void testGenerateContentWithEmptySystemInstructions() { + Content systemContent = Content.builder().role("system").parts(ImmutableList.of()).build(); + GenerateContentConfig config = Mockito.mock(GenerateContentConfig.class); + Mockito.when(config.systemInstruction()).thenReturn(Optional.of(systemContent)); + Content content = Content.builder().role("user").parts(ImmutableList.of(Part.builder().text("Empty system instruction").build())).build(); + LlmRequest llmRequest = LlmRequest.builder().contents(ImmutableList.of(content)).config(Optional.of(config)).build(); + Message message = Mockito.mock(Message.class); + ContentBlock contentBlock = Mockito.mock(ContentBlock.class); + Mockito.when(contentBlock.text()).thenReturn(Optional.of("Empty system instruction")); + Mockito.when(message.content()).thenReturn(ImmutableList.of(contentBlock)); + Mockito.when(messagesClient.create(any(MessageCreateParams.class))).thenReturn(message); + Flowable flowable = claude.generateContent(llmRequest, false); + List responses = flowable.toList().blockingGet(); + Assertions.assertEquals("Empty system instruction", responses.get(0).content().get().parts().get().get(0).text().get()); + } + @Test + @Tag("integration") + public void testGenerateContentWithToolsListProvided() { + Content content = Content.builder().role("user").parts(ImmutableList.of(Part.builder().text("Tool feature test").build())).build(); + Map toolsMap = ImmutableMap.of("mytool", Mockito.mock(BaseTool.class)); + LlmRequest llmRequest = LlmRequest.builder() + .contents(ImmutableList.of(content)) + .tools(toolsMap) + .build(); + MessageParam messageParam = MessageParam.builder().role(MessageParam.Role.USER).content(ImmutableList.of(TextBlockParam.builder().text("Tool feature test").build())).build(); + ToolChoice toolChoice = ToolChoice.ofAuto(ToolChoiceAuto.builder().disableParallelToolUse(true).build()); + Message message = Mockito.mock(Message.class); + ContentBlock contentBlock = Mockito.mock(ContentBlock.class); + Mockito.when(contentBlock.text()).thenReturn(Optional.of("Tool feature test")); + Mockito.when(message.content()).thenReturn(ImmutableList.of(contentBlock)); + Mockito.when(messagesClient.create(any(MessageCreateParams.class))).thenReturn(message); + Flowable flowable = claude.generateContent(llmRequest, false); + List responses = flowable.toList().blockingGet(); + Assertions.assertEquals("Tool feature test", responses.get(0).content().get().parts().get().get(0).text().get()); + } + @Test + @Tag("valid") + public void testGenerateContentWithMultipleContents() { + Content content1 = Content.builder().role("user").parts(ImmutableList.of(Part.builder().text("First").build())).build(); + Content content2 = Content.builder().role("user").parts(ImmutableList.of(Part.builder().text("Second").build())).build(); + LlmRequest llmRequest = LlmRequest.builder().contents(ImmutableList.of(content1, content2)).build(); + ContentBlock contentBlock1 = Mockito.mock(ContentBlock.class); + Mockito.when(contentBlock1.text()).thenReturn(Optional.of("First")); + ContentBlock contentBlock2 = Mockito.mock(ContentBlock.class); + Mockito.when(contentBlock2.text()).thenReturn(Optional.of("Second")); + Message message = Mockito.mock(Message.class); + Mockito.when(message.content()).thenReturn(ImmutableList.of(contentBlock1, contentBlock2)); + Mockito.when(messagesClient.create(any(MessageCreateParams.class))).thenReturn(message); + Flowable flowable = claude.generateContent(llmRequest, false); + List responses = flowable.toList().blockingGet(); + List parts = responses.get(0).content().get().parts().get(); + Assertions.assertEquals("First", parts.get(0).text().get()); + Assertions.assertEquals("Second", parts.get(1).text().get()); + } + @Test + @Tag("invalid") + public void testGenerateContentWithNullMessageFromClient() { + Content content = Content.builder().role("user").parts(ImmutableList.of(Part.builder().text("Null Message Test").build())).build(); + LlmRequest llmRequest = LlmRequest.builder().contents(ImmutableList.of(content)).build(); + Mockito.when(messagesClient.create(any(MessageCreateParams.class))).thenReturn(null); + Flowable flowable = claude.generateContent(llmRequest, false); + List responses = flowable.toList().blockingGet(); + Assertions.assertTrue(responses.get(0).content().isEmpty() || !responses.get(0).content().get().parts().isPresent() || responses.get(0).content().get().parts().get().isEmpty()); + } + @Test + @Tag("integration") + public void testGenerateContentWithFunctionCallPart() { + FunctionCall functionCall = FunctionCall.builder().name("callFunc").build(); + Part functionCallPart = Part.builder().functionCall(functionCall).build(); + Content content = Content.builder().role("user").parts(ImmutableList.of(functionCallPart)).build(); + LlmRequest llmRequest = LlmRequest.builder().contents(ImmutableList.of(content)).build(); + ToolUseBlockParam toolUseBlockParam = Mockito.mock(ToolUseBlockParam.class); + MessageParam messageParam = Mockito.mock(MessageParam.class); + Message message = Mockito.mock(Message.class); + ContentBlock contentBlock = Mockito.mock(ContentBlock.class); + Mockito.when(contentBlock.text()).thenReturn(Optional.of("FunctionCall Result")); // TODO: adapt expected text if needed + Mockito.when(message.content()).thenReturn(ImmutableList.of(contentBlock)); + Mockito.when(messagesClient.create(any(MessageCreateParams.class))).thenReturn(message); + Flowable flowable = claude.generateContent(llmRequest, false); + LlmResponse response = flowable.blockingFirst(); + Assertions.assertEquals("FunctionCall Result", response.content().get().parts().get().get(0).text().get()); + } + @Test + @Tag("integration") + public void testGenerateContentWithFunctionResponsePart() { + Part functionResponsePart = Part.builder().functionResponse(Optional.of("response-value")).build(); + Content content = Content.builder().role("user").parts(ImmutableList.of(functionResponsePart)).build(); + LlmRequest llmRequest = LlmRequest.builder().contents(ImmutableList.of(content)).build(); + ToolResultBlockParam toolResultBlockParam = Mockito.mock(ToolResultBlockParam.class); + MessageParam messageParam = Mockito.mock(MessageParam.class); + Message message = Mockito.mock(Message.class); + ContentBlock contentBlock = Mockito.mock(ContentBlock.class); + Mockito.when(contentBlock.text()).thenReturn(Optional.of("response-value")); // TODO: adapt expected text if needed + Mockito.when(message.content()).thenReturn(ImmutableList.of(contentBlock)); + Mockito.when(messagesClient.create(any(MessageCreateParams.class))).thenReturn(message); + Flowable flowable = claude.generateContent(llmRequest, false); + LlmResponse response = flowable.blockingFirst(); + Assertions.assertEquals("response-value", response.content().get().parts().get().get(0).text().get()); + } + @Test + @Tag("boundary") + public void testGenerateContentWithEmptyContents() { + LlmRequest llmRequest = LlmRequest.builder().contents(ImmutableList.of()).build(); + Message message = Mockito.mock(Message.class); + Mockito.when(message.content()).thenReturn(ImmutableList.of()); + Mockito.when(messagesClient.create(any(MessageCreateParams.class))).thenReturn(message); + Flowable flowable = claude.generateContent(llmRequest, false); + Assertions.assertTrue(flowable.blockingFirst().content().get().parts().get().isEmpty()); + } + @Test + @Tag("boundary") + public void testGenerateContentWithMaxTokensConfigured() { + Claude claudeWithMaxTokens = new Claude("claude-2", anthropicClient, 64); + Content content = Content.builder().role("user").parts(ImmutableList.of(Part.builder().text("Max token check").build())).build(); + LlmRequest llmRequest = LlmRequest.builder().contents(ImmutableList.of(content)).build(); + Message message = Mockito.mock(Message.class); + ContentBlock contentBlock = Mockito.mock(ContentBlock.class); + Mockito.when(contentBlock.text()).thenReturn(Optional.of("Max token check")); + Mockito.when(message.content()).thenReturn(ImmutableList.of(contentBlock)); + Mockito.when(messagesClient.create(any(MessageCreateParams.class))).thenReturn(message); + Flowable flowable = claudeWithMaxTokens.generateContent(llmRequest, false); + Assertions.assertEquals("Max token check", flowable.blockingFirst().content().get().parts().get().get(0).text().get()); + } + @Test + @Tag("integration") + public void testGenerateContentWithMultiPartSystemInstruction() { + Part systemPart1 = Part.builder().text("Sys line 1").build(); + Part systemPart2 = Part.builder().text("Sys line 2").build(); + Content systemContent = Content.builder().role("system").parts(ImmutableList.of(systemPart1, systemPart2)).build(); + GenerateContentConfig config = Mockito.mock(GenerateContentConfig.class); + Mockito.when(config.systemInstruction()).thenReturn(Optional.of(systemContent)); + Content content = Content.builder().role("user").parts(ImmutableList.of(Part.builder().text("User Text").build())).build(); + LlmRequest llmRequest = LlmRequest.builder().contents(ImmutableList.of(content)).config(Optional.of(config)).build(); + Message message = Mockito.mock(Message.class); + ContentBlock contentBlock = Mockito.mock(ContentBlock.class); + Mockito.when(contentBlock.text()).thenReturn(Optional.of("User Text")); + Mockito.when(message.content()).thenReturn(ImmutableList.of(contentBlock)); + Mockito.when(messagesClient.create(any(MessageCreateParams.class))).thenReturn(message); + Flowable flowable = claude.generateContent(llmRequest, false); + Assertions.assertEquals("User Text", flowable.blockingFirst().content().get().parts().get().get(0).text().get()); + } + @Test + @Tag("invalid") + public void testGenerateContentWithMissingMessageContent() { + Content content = Content.builder().role("user").parts(ImmutableList.of(Part.builder().text("Test missing content").build())).build(); + LlmRequest llmRequest = LlmRequest.builder().contents(ImmutableList.of(content)).build(); + Message message = Mockito.mock(Message.class); + Mockito.when(message.content()).thenReturn(null); + Mockito.when(messagesClient.create(any(MessageCreateParams.class))).thenReturn(message); + Flowable flowable = claude.generateContent(llmRequest, false); + Assertions.assertTrue(flowable.blockingFirst().content().isEmpty() || !flowable.blockingFirst().content().get().parts().isPresent()); + } + @Test + @Tag("valid") + public void testGenerateContentUsesDefaultModelWhenNoneProvided() { + Content content = Content.builder().role("user").parts(ImmutableList.of(Part.builder().text("Default model").build())).build(); + LlmRequest llmRequest = LlmRequest.builder().contents(ImmutableList.of(content)).model(Optional.empty()).build(); + Message message = Mockito.mock(Message.class); + ContentBlock contentBlock = Mockito.mock(ContentBlock.class); + Mockito.when(contentBlock.text()).thenReturn(Optional.of("Default model")); + Mockito.when(message.content()).thenReturn(ImmutableList.of(contentBlock)); + Mockito.when(messagesClient.create(any(MessageCreateParams.class))).thenReturn(message); + Flowable flowable = claude.generateContent(llmRequest, false); + Assertions.assertEquals("Default model", flowable.blockingFirst().content().get().parts().get().get(0).text().get()); + } + @Test + @Tag("valid") + public void testGenerateContentWithAbsentConfig() { + Content content = Content.builder().role("user").parts(ImmutableList.of(Part.builder().text("No config present").build())).build(); + LlmRequest llmRequest = LlmRequest.builder().contents(ImmutableList.of(content)).config(Optional.empty()).build(); + Message message = Mockito.mock(Message.class); + ContentBlock contentBlock = Mockito.mock(ContentBlock.class); + Mockito.when(contentBlock.text()).thenReturn(Optional.of("No config present")); + Mockito.when(message.content()).thenReturn(ImmutableList.of(contentBlock)); + Mockito.when(messagesClient.create(any(MessageCreateParams.class))).thenReturn(message); + Flowable flowable = claude.generateContent(llmRequest, false); + Assertions.assertEquals("No config present", flowable.blockingFirst().content().get().parts().get().get(0).text().get()); + } + @Test + @Tag("valid") + public void testGenerateContentWithStreamTrue() { + Content content = Content.builder().role("user").parts(ImmutableList.of(Part.builder().text("Streaming test").build())).build(); + LlmRequest llmRequest = LlmRequest.builder().contents(ImmutableList.of(content)).build(); + Message message = Mockito.mock(Message.class); + ContentBlock contentBlock = Mockito.mock(ContentBlock.class); + Mockito.when(contentBlock.text()).thenReturn(Optional.of("Streaming test")); + Mockito.when(message.content()).thenReturn(ImmutableList.of(contentBlock)); + Mockito.when(messagesClient.create(any(MessageCreateParams.class))).thenReturn(message); + Flowable flowable = claude.generateContent(llmRequest, true); + Assertions.assertEquals("Streaming test", flowable.blockingFirst().content().get().parts().get().get(0).text().get()); + } + @Test + @Tag("valid") + public void testGenerateContentWithStreamFalse() { + Content content = Content.builder().role("user").parts(ImmutableList.of(Part.builder().text("Static test").build())).build(); + LlmRequest llmRequest = LlmRequest.builder().contents(ImmutableList.of(content)).build(); + Message message = Mockito.mock(Message.class); + ContentBlock contentBlock = Mockito.mock(ContentBlock.class); + Mockito.when(contentBlock.text()).thenReturn(Optional.of("Static test")); + Mockito.when(message.content()).thenReturn(ImmutableList.of(contentBlock)); + Mockito.when(messagesClient.create(any(MessageCreateParams.class))).thenReturn(message); + Flowable flowable = claude.generateContent(llmRequest, false); + Assertions.assertEquals("Static test", flowable.blockingFirst().content().get().parts().get().get(0).text().get()); + } + @Test + @Tag("invalid") + public void testGenerateContentHandlesMessageParamConversionException() { + Content content = Content.builder().role("user").parts(ImmutableList.of(Part.builder().build())).build(); + LlmRequest llmRequest = LlmRequest.builder().contents(ImmutableList.of(content)).build(); + Mockito.when(messagesClient.create(any(MessageCreateParams.class))).thenThrow(new UnsupportedOperationException("Unsupported part")); + Assertions.assertThrows(UnsupportedOperationException.class, () -> claude.generateContent(llmRequest, false).blockingFirst()); + } + @Test + @Tag("invalid") + public void testGenerateContentWithIncompleteFunctionDeclaration() { + FunctionDeclaration incompleteDecl = Mockito.mock(FunctionDeclaration.class); + Mockito.when(incompleteDecl.name()).thenReturn(Optional.empty()); + GenerateContentConfig.ToolsConfig toolsConfig = Mockito.mock(GenerateContentConfig.ToolsConfig.class); + Mockito.when(toolsConfig.functionDeclarations()).thenReturn(Optional.of(ImmutableList.of(incompleteDecl))); + GenerateContentConfig config = Mockito.mock(GenerateContentConfig.class); + Mockito.when(config.tools()).thenReturn(Optional.of(ImmutableList.of(toolsConfig))); + Content content = Content.builder().role("user").parts(ImmutableList.of(Part.builder().text("Incomplete funcDecl").build())).build(); + LlmRequest llmRequest = LlmRequest.builder().contents(ImmutableList.of(content)).config(Optional.of(config)).build(); + Mockito.when(messagesClient.create(any(MessageCreateParams.class))).thenThrow(new UnsupportedOperationException("FunctionDeclaration incomplete")); + Assertions.assertThrows(UnsupportedOperationException.class, () -> claude.generateContent(llmRequest, false).blockingFirst()); + } + @Test + @Tag("boundary") + public void testGenerateContentIgnoresNullTextsInSystemInstruction() { + Part partWithText = Part.builder().text("part-text-valid").build(); + Part partWithoutText = Part.builder().build(); + Content systemContent = Content.builder().role("system").parts(ImmutableList.of(partWithText, partWithoutText)).build(); + GenerateContentConfig config = Mockito.mock(GenerateContentConfig.class); + Mockito.when(config.systemInstruction()).thenReturn(Optional.of(systemContent)); + Content content = Content.builder().role("user").parts(ImmutableList.of(Part.builder().text("ignore nulls").build())).build(); + LlmRequest llmRequest = LlmRequest.builder().contents(ImmutableList.of(content)).config(Optional.of(config)).build(); + Message message = Mockito.mock(Message.class); + ContentBlock contentBlock = Mockito.mock(ContentBlock.class); + Mockito.when(contentBlock.text()).thenReturn(Optional.of("ignore nulls")); + Mockito.when(message.content()).thenReturn(ImmutableList.of(contentBlock)); + Mockito.when(messagesClient.create(any(MessageCreateParams.class))).thenReturn(message); + Flowable flowable = claude.generateContent(llmRequest, false); + LlmResponse response = flowable.blockingFirst(); + Assertions.assertEquals("ignore nulls", response.content().get().parts().get().get(0).text().get()); + } + @Test + @Tag("valid") + public void testGenerateContentWithNullToolChoice() { + Content content = Content.builder().role("user").parts(ImmutableList.of(Part.builder().text("Null toolChoice case").build())).build(); + LlmRequest llmRequest = LlmRequest.builder().contents(ImmutableList.of(content)).tools(ImmutableMap.of()).build(); + Message message = Mockito.mock(Message.class); + ContentBlock contentBlock = Mockito.mock(ContentBlock.class); + Mockito.when(contentBlock.text()).thenReturn(Optional.of("Null toolChoice case")); + Mockito.when(message.content()).thenReturn(ImmutableList.of(contentBlock)); + Mockito.when(messagesClient.create(any(MessageCreateParams.class))).thenReturn(message); + Flowable flowable = claude.generateContent(llmRequest, false); + LlmResponse response = flowable.blockingFirst(); + Assertions.assertEquals("Null toolChoice case", response.content().get().parts().get().get(0).text().get()); + } + @Test + @Tag("boundary") + public void testGenerateContentWithLargeContentList() { + List contents = new ArrayList<>(); + for (int i = 0; i < 100; i++) { + contents.add(Content.builder().role("user").parts(ImmutableList.of(Part.builder().text("Msg " + i).build())).build()); + } + LlmRequest llmRequest = LlmRequest.builder().contents(ImmutableList.copyOf(contents)).build(); + List blocks = new ArrayList<>(); + for (int i = 0; i < 100; i++) { + ContentBlock cb = Mockito.mock(ContentBlock.class); + Mockito.when(cb.text()).thenReturn(Optional.of("Msg " + i)); + blocks.add(cb); + } + Message message = Mockito.mock(Message.class); + Mockito.when(message.content()).thenReturn(ImmutableList.copyOf(blocks)); + Mockito.when(messagesClient.create(any(MessageCreateParams.class))).thenReturn(message); + Flowable flowable = claude.generateContent(llmRequest, false); + List parts = flowable.blockingFirst().content().get().parts().get(); + for (int i = 0; i < 100; i++) { + Assertions.assertEquals("Msg " + i, parts.get(i).text().get()); + } + } + @Test + @Tag("integration") + public void testGenerateContentWithToolsAndNestedProperties() { + FunctionDeclaration fd = Mockito.mock(FunctionDeclaration.class); + Mockito.when(fd.name()).thenReturn(Optional.of("nestedFunc")); + // Simulation: Nested property (Mock only) + GenerateContentConfig.ToolsConfig toolsConfig = Mockito.mock(GenerateContentConfig.ToolsConfig.class); + Mockito.when(toolsConfig.functionDeclarations()).thenReturn(Optional.of(ImmutableList.of(fd))); + GenerateContentConfig config = Mockito.mock(GenerateContentConfig.class); + Mockito.when(config.tools()).thenReturn(Optional.of(ImmutableList.of(toolsConfig))); + Content content = Content.builder().role("user").parts(ImmutableList.of(Part.builder().text("Nested properties").build())).build(); + LlmRequest llmRequest = LlmRequest.builder().contents(ImmutableList.of(content)).config(Optional.of(config)).build(); + Message message = Mockito.mock(Message.class); + ContentBlock contentBlock = Mockito.mock(ContentBlock.class); + Mockito.when(contentBlock.text()).thenReturn(Optional.of("Nested properties")); + Mockito.when(message.content()).thenReturn(ImmutableList.of(contentBlock)); + Mockito.when(messagesClient.create(any(MessageCreateParams.class))).thenReturn(message); + Flowable flowable = claude.generateContent(llmRequest, false); + Assertions.assertEquals("Nested properties", flowable.blockingFirst().content().get().parts().get().get(0).text().get()); + } + @Test + @Tag("valid") + public void testGenerateContentWithToolsConfigLackingFunctionDeclarations() { + GenerateContentConfig.ToolsConfig toolsConfig = Mockito.mock(GenerateContentConfig.ToolsConfig.class); + Mockito.when(toolsConfig.functionDeclarations()).thenReturn(Optional.empty()); + GenerateContentConfig config = Mockito.mock(GenerateContentConfig.class); + Mockito.when(config.tools()).thenReturn(Optional.of(ImmutableList.of(toolsConfig))); + Content content = Content.builder().role("user").parts(ImmutableList.of(Part.builder().text("No funcDecl").build())).build(); + LlmRequest llmRequest = LlmRequest.builder().contents(ImmutableList.of(content)).config(Optional.of(config)).build(); + Message message = Mockito.mock(Message.class); + ContentBlock contentBlock = Mockito.mock(ContentBlock.class); + Mockito.when(contentBlock.text()).thenReturn(Optional.of("No funcDecl")); + Mockito.when(message.content()).thenReturn(ImmutableList.of(contentBlock)); + Mockito.when(messagesClient.create(any(MessageCreateParams.class))).thenReturn(message); + Flowable flowable = claude.generateContent(llmRequest, false); + Assertions.assertEquals("No funcDecl", flowable.blockingFirst().content().get().parts().get().get(0).text().get()); + } + // Add more tests here if needed for future scenarios +} \ No newline at end of file diff --git a/core/src/test/java/com/google/adk/models/LlmFactoryCreateTest.java b/core/src/test/java/com/google/adk/models/LlmFactoryCreateTest.java new file mode 100644 index 000000000..d5202ffff --- /dev/null +++ b/core/src/test/java/com/google/adk/models/LlmFactoryCreateTest.java @@ -0,0 +1,384 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +// ********RoostGPT******** +/* +Test generated by RoostGPT for test adk-java-demo using AI Type Azure Open AI and AI Model gpt-4.1 + +ROOST_METHOD_HASH=create_c0b4ba7866 +ROOST_METHOD_SIG_HASH=create_c0b4ba7866 + +Scenario 1: Creating a Gemini Model Instance + +Details: + TestName: createReturnsGeminiInstanceForGeminiModel + Description: Verifies that the create method in the LlmFactory nested interface returns a Gemini model instance when given a model name matching the "gemini-.*" pattern. + +Execution: + Arrange: Set up the LlmFactory to use the default Gemini builder as registered in the static block. + Act: Call create with a model name like "gemini-pro". + Assert: Verify that the returned object is an instance of Gemini and correctly reflects the provided model name. +Validation: + This assertion checks that the registered factory for Gemini models generates the expected subtype (Gemini) and that model name is correctly set. It ensures factory mappings work as intended. + + +Scenario 2: Creating an Apigee Model Instance + +Details: + TestName: createReturnsApigeeLlmInstanceForApigeeModel + Description: Checks that the create method returns an ApigeeLlm instance when passed a model name matching the "apigee/.*" pattern. + +Execution: + Arrange: Use the LlmFactory with the default ApigeeLlm builder. + Act: Invoke create with "apigee/test-model" as the model name. + Assert: Confirm the returned object is an ApigeeLlm and has the correct model name. +Validation: + This test validates that Apigee model names are routed to the correct factory and the result is properly constructed. It supports accurate factory selection. + + +Scenario 3: Unmatched Model Name + +Details: + TestName: createThrowsExceptionForUnsupportedModel + Description: Ensures the create method throws IllegalArgumentException if the model name does not match any registered pattern. + +Execution: + Arrange: Use the existing LlmFactory registrations only (no additional registration). + Act: Call create with "random-model" or another unsupported name. + Assert: Expect an IllegalArgumentException to be thrown. +Validation: + The assertion checks proper error handling, confirming that factories are not invoked for unsupported models, which is vital for robust input validation. + + +Scenario 4: Factory Returns Null + +Details: + TestName: createHandlesFactoryReturningNull + Description: Tests that the factory behaves as expected (either returning null or throwing an exception) if the registered factory returns null for a model name. + +Execution: + Arrange: Register a new factory for "null-*", which returns null. + Act: Invoke create with "null-model". + Assert: Assert the returned value is null, or verify exception handling if null is not permitted. +Validation: + This scenario is important for exploring non-standard factory behavior, such as factories not being able to construct models, and ensures system resilience. + + +Scenario 5: Factory with Regex Collision + +Details: + TestName: createUsesFirstMatchingFactoryInCaseOfCollision + Description: Checks that the correct factory is used when two registered regexes can match a model name, ensuring predictable factory selection order. + +Execution: + Arrange: Register two overlapping regex patterns like "gemini-.*" and "gemini-pro", with different factories. + Act: Call create with "gemini-pro". + Assert: Ensure the model returned is from the factory registered first (or as determined by registration ordering). +Validation: + This validation ensures collision behavior is managed, preventing ambiguous factory selection, which can affect business logic. + + +Scenario 6: Factory Throws Runtime Exception + +Details: + TestName: createPropagatesExceptionFromFactory + Description: Ensures that if the registered factory throws an unchecked exception, it is propagated out of the create method. + +Execution: + Arrange: Register a factory for "fail-.*" that throws a RuntimeException. + Act: Invoke create with "fail-case". + Assert: Expect RuntimeException to be thrown and propagated. +Validation: + This confirms robust exception handling, ensuring the interface does not silently swallow exceptions during model construction. + + +Scenario 7: Factory Receives Correct Model Name + +Details: + TestName: createPassesModelNameToFactory + Description: Verifies that the modelName parameter passed to create is accurately supplied to the registered factory. + +Execution: + Arrange: Register a factory that records the received model name. + Act: Call create with distinct model name values. + Assert: Assert that the factory received exactly the same model name argument. +Validation: + Ensures data integrity in the contract between the create method and factory, which forms the core of this registration and resolution mechanism. + + +Scenario 8: Factory Registered at Runtime + +Details: + TestName: createUsesDynamicallyRegisteredFactory + Description: Validates that factories registered after class initialization are immediately available for model creation. + +Execution: + Arrange: Register a new factory for "dynamic-.*" using registerLlm. + Act: Call create with "dynamic-model". + Assert: Confirm the correct model instance is returned. +Validation: + This test demonstrates extensibility of the factory pattern, verifying dynamic registration and mapping updates take effect. + + +Scenario 9: Multi-threaded Access to Factory + +Details: + TestName: createIsThreadSafeUnderConcurrentAccess + Description: Tests that concurrent calls to create with the same model name yield consistent results and do not introduce race conditions. + +Execution: + Arrange: Launch multiple threads invoking create using a concurrent model name. + Act: Execute threads concurrently. + Assert: Validate all threads receive valid, consistent BaseLlm instances. +Validation: + This scenario focuses on thread safety, particularly as LlmFactory is part of a larger registry using ConcurrentHashMap for multi-threaded environments. + + +Scenario 10: Empty Model Name Input + +Details: + TestName: createHandlesEmptyModelNameInput + Description: Examines the system's behavior when create is called with an empty string as the modelName parameter. + +Execution: + Arrange: No special registration (unless supporting empty names). + Act: Call create with "". + Assert: Check the result is either an exception (IllegalArgumentException) or a specific supported model instance. +Validation: + It is significant for input validation, preventing unexpected null or empty model names from corrupting factory resolution. + +*/ + +// ********RoostGPT******** + +package com.google.adk.models; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Assertions; +import org.mockito.Mockito; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicReference; +import org.junit.jupiter.api.BeforeEach; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import org.junit.jupiter.api.*; + +// Dummy BaseLlm implementation for testing +class LlmFactoryCreateTest { + final String modelName; + BaseLlm(String modelName) { + this.modelName = modelName; + } +} +// Dummy Gemini and ApigeeLlm implementations for testing +class LlmFactoryCreateTest extends BaseLlm { + Gemini(String modelName) { super(modelName); } +} +class LlmFactoryCreateTest extends BaseLlm { + ApigeeLlm(String modelName) { super(modelName); } +} + +class LlmFactoryCreateTest { + // Simulate static registry + static final Map llmFactories = new ConcurrentHashMap<>(); + public static void registerLlm(String modelNamePattern, LlmFactory factory) { + llmFactories.put(modelNamePattern, factory); + } + public static BaseLlm create(String modelName) { + for (Map.Entry entry : llmFactories.entrySet()) { + if (modelName.matches(entry.getKey())) { + return entry.getValue().create(modelName); + } + } + throw new IllegalArgumentException("Unsupported model name: " + modelName); + } + public static +class LlmFactoryCreateTest { + public BaseLlm create(String modelName) { + // This method is intended to be overridden/mocked in tests + return null; + } + } +} + +class LlmFactoryCreateTest { + @BeforeEach + public void setup() { + LlmRegistry.llmFactories.clear(); + // Register default factories + LlmRegistry.registerLlm("gemini-.*", new LlmRegistry.LlmFactory() { + @Override + public BaseLlm create(String modelName) { + return new Gemini(modelName); + } + }); + LlmRegistry.registerLlm("apigee/.*", new LlmRegistry.LlmFactory() { + @Override + public BaseLlm create(String modelName) { + return new ApigeeLlm(modelName); + } + }); + } + @Test + @Tag("valid") + public void testCreateReturnsGeminiInstanceForGeminiModel() { + String modelName = "gemini-pro"; + BaseLlm result = LlmRegistry.create(modelName); + Assertions.assertTrue(result instanceof Gemini, "Result should be an instance of Gemini"); + Assertions.assertEquals((String)modelName, (String)result.modelName, "Model name should match"); + } + @Test + @Tag("valid") + public void testCreateReturnsApigeeLlmInstanceForApigeeModel() { + String modelName = "apigee/test-model"; + BaseLlm result = LlmRegistry.create(modelName); + Assertions.assertTrue(result instanceof ApigeeLlm, "Result should be an instance of ApigeeLlm"); + Assertions.assertEquals((String)modelName, (String)result.modelName, "Model name should match"); + } + @Test + @Tag("invalid") + public void testCreateThrowsExceptionForUnsupportedModel() { + String modelName = "random-model"; + IllegalArgumentException exception = Assertions.assertThrows( + IllegalArgumentException.class, + () -> LlmRegistry.create(modelName), + "Should throw IllegalArgumentException for unsupported model" + ); + Assertions.assertTrue(exception.getMessage().contains((String)modelName), "Exception message should contain model name"); + } + @Test + @Tag("boundary") + public void testCreateHandlesFactoryReturningNull() { + LlmRegistry.registerLlm("null-.*", new LlmRegistry.LlmFactory() { + @Override + public BaseLlm create(String modelName) { + return null; + } + }); + String modelName = "null-model"; + BaseLlm result = LlmRegistry.create(modelName); + Assertions.assertNull(result, "Returned value should be null"); + } + @Test + @Tag("boundary") + public void testCreateUsesFirstMatchingFactoryInCaseOfCollision() { + // Register a more specific pattern second + LlmRegistry.llmFactories.clear(); + LlmRegistry.registerLlm("gemini-.*", new LlmRegistry.LlmFactory() { + @Override + public BaseLlm create(String modelName) { + return new Gemini("first-factory"); // TODO: user can customize returned model name if needed + } + }); + LlmRegistry.registerLlm("gemini-pro", new LlmRegistry.LlmFactory() { + @Override + public BaseLlm create(String modelName) { + return new Gemini("second-factory"); // TODO: user can customize returned model name if needed + } + }); + String modelName = "gemini-pro"; + BaseLlm result = LlmRegistry.create(modelName); + Assertions.assertEquals((String)"first-factory", (String)result.modelName, "Should use first registered matching factory"); + } + @Test + @Tag("invalid") + public void testCreatePropagatesExceptionFromFactory() { + LlmRegistry.registerLlm("fail-.*", new LlmRegistry.LlmFactory() { + @Override + public BaseLlm create(String modelName) { + throw new RuntimeException("Factory Failure"); // TODO: user can change exception message if needed + } + }); + String modelName = "fail-case"; + RuntimeException ex = Assertions.assertThrows( + RuntimeException.class, + () -> LlmRegistry.create(modelName), + "Should propagate RuntimeException from factory" + ); + Assertions.assertTrue(ex.getMessage().contains((String)"Factory Failure"), "Exception message should be as thrown"); + } + @Test + @Tag("valid") + public void testCreatePassesModelNameToFactory() { + AtomicReference passedModelName = new AtomicReference<>(); + LlmRegistry.registerLlm("record-.*", new LlmRegistry.LlmFactory() { + @Override + public BaseLlm create(String modelName) { + passedModelName.set(modelName); + return new Gemini(modelName); + } + }); + String modelName = "record-model"; + BaseLlm result = LlmRegistry.create(modelName); + Assertions.assertEquals((String)modelName, (String)passedModelName.get(), "Factory should receive the exact model name"); + } + @Test + @Tag("integration") + public void testCreateUsesDynamicallyRegisteredFactory() { + LlmRegistry.registerLlm("dynamic-.*", new LlmRegistry.LlmFactory() { + @Override + public BaseLlm create(String modelName) { + return new Gemini("dynamic-instance"); // TODO: customize if needed + } + }); + String modelName = "dynamic-model"; + BaseLlm result = LlmRegistry.create(modelName); + Assertions.assertEquals((String)"dynamic-instance", (String)result.modelName, "Dynamically registered factory should be used for model creation"); + } + @Test + @Tag("boundary") + public void testCreateIsThreadSafeUnderConcurrentAccess() throws InterruptedException { + // Register a factory for thread-safe test + LlmRegistry.registerLlm("thread-.*", new LlmRegistry.LlmFactory() { + @Override + public BaseLlm create(String modelName) { + return new Gemini(modelName); + } + }); + String modelName = "thread-model"; + int threadCount = 10; // TODO: user can adjust thread count + List results = new ArrayList<>(); + CountDownLatch latch = new CountDownLatch(threadCount); + Runnable task = () -> { + BaseLlm llm = LlmRegistry.create(modelName); + synchronized (results) { + results.add(llm); + } + latch.countDown(); + }; + for (int i = 0; i < threadCount; i++) { + new Thread(task).start(); + } + latch.await(); + for (BaseLlm llm : results) { + Assertions.assertTrue(llm instanceof Gemini, "Each returned object must be Gemini"); + Assertions.assertEquals((String)modelName, (String)llm.modelName, "Model name should be correct in all threads"); + } + Assertions.assertEquals(threadCount, results.size(), "All threads should have returned results"); + } + @Test + @Tag("boundary") + public void testCreateHandlesEmptyModelNameInput() { + String modelName = ""; + IllegalArgumentException exception = Assertions.assertThrows( + IllegalArgumentException.class, + () -> LlmRegistry.create(modelName), + "Should throw IllegalArgumentException for empty model name" + ); + Assertions.assertTrue(exception.getMessage().contains((String)modelName), "Exception should mention empty input"); + } +} \ No newline at end of file diff --git a/core/src/test/java/com/google/adk/models/LlmRegistryRegisterLlmTest.java b/core/src/test/java/com/google/adk/models/LlmRegistryRegisterLlmTest.java new file mode 100644 index 000000000..e7071fb31 --- /dev/null +++ b/core/src/test/java/com/google/adk/models/LlmRegistryRegisterLlmTest.java @@ -0,0 +1,323 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +// ********RoostGPT******** +/* +Test generated by RoostGPT for test adk-java-demo using AI Type Azure Open AI and AI Model gpt-4.1 + +ROOST_METHOD_HASH=registerLlm_39033090dc +ROOST_METHOD_SIG_HASH=registerLlm_6d1828529e + +Scenario 1: Registering a New Model Pattern and Factory + +Details: + TestName: registersNewModelPatternAndFactory + Description: This test is designed to confirm that when a new modelNamePattern and corresponding LlmFactory are registered using registerLlm, the llmFactories map will include the newly added pattern and factory. This scenario validates the normal case of adding a unique entry. + +Execution: + Arrange: Prepare a unique modelNamePattern and a valid mock or stub implementation of LlmFactory. Ensure the llmFactories map does not already contain this pattern. + Act: Call registerLlm with the new modelNamePattern and the LlmFactory instance. + Assert: Check that llmFactories contains a mapping for the new modelNamePattern associated with the exact LlmFactory instance used. +Validation: + This assertion verifies that registerLlm correctly inserts new mappings. It confirms the registry's ability to dynamically extend its supported model patterns, which is crucial for flexibility and future extensibility. + +Scenario 2: Overwriting an Existing Model Pattern + +Details: + TestName: overwritesExistingModelPattern + Description: This scenario checks that when registerLlm is invoked with a modelNamePattern already present in llmFactories, the existing factory is replaced with the new one. + +Execution: + Arrange: Register a modelNamePattern with an initial LlmFactory. Prepare an alternative LlmFactory instance with the same modelNamePattern. + Act: Call registerLlm again with the existing modelNamePattern and the alternative factory. + Assert: Verify that llmFactories now holds the new LlmFactory for the given pattern, not the original. +Validation: + The assertion ensures that the map update operation replaces previous entries for the same pattern. This behavior is vital for supporting dynamic reconfiguration and hot-swapping in production or during tests. + +Scenario 3: Registering with a Null Factory + +Details: + TestName: handlesNullFactory + Description: This test scenario examines what happens if registerLlm is called with a valid modelNamePattern but a null factory reference, ensuring the method's robustness to null values. + +Execution: + Arrange: Specify a valid, unique modelNamePattern. Set the factory argument to null. + Act: Call registerLlm with these arguments. + Assert: Assert that llmFactories contains the given modelNamePattern and the associated value is null. +Validation: + This checks that the method does not throw a NullPointerException and that the registry allows a null value. It is essential for error handling and system stability, allowing developers to recover gracefully from misconfiguration. + +Scenario 4: Registering with a Null Model Pattern + +Details: + TestName: handlesNullModelPattern + Description: The test ensures the registry can accept a null as the modelNamePattern and process it without exceptions. + +Execution: + Arrange: Prepare a valid LlmFactory and set modelNamePattern to null. + Act: Call registerLlm(null, factory). + Assert: Assert that llmFactories contains a mapping with a null key and that its value is the provided factory. +Validation: + This scenario confirms the resilience of the registry to null keys (noting that ConcurrentHashMap supports them differently from HashMap). This is necessary to define the method’s boundary behavior and serve as a safeguard for input validation. + +Scenario 5: Registering with Both Pattern and Factory as Null + +Details: + TestName: handlesNullPatternAndFactory + Description: Tests that registerLlm does not throw and correctly stores a mapping from null to null if both arguments are null. + +Execution: + Arrange: Set both modelNamePattern and LlmFactory to null. + Act: Call registerLlm(null, null). + Assert: Assert that llmFactories contains an entry for null with the value null. +Validation: + Tests complete tolerance to nulls, ensuring the underlying map and registry are robust to such edge usage patterns, which may arise due to configuration errors or misuse. + +Scenario 6: Registering a Duplicate Factory Instance with a Different Pattern + +Details: + TestName: registersSameFactoryWithDifferentPattern + Description: This scenario checks if the same LlmFactory instance can be associated to multiple modelNamePatterns without conflict. + +Execution: + Arrange: Prepare one LlmFactory and two distinct modelNamePatterns. + Act: Call registerLlm for both patterns with the identical factory instance. + Assert: Assert that each modelNamePattern in llmFactories maps to the same LlmFactory instance. +Validation: + This scenario confirms the registry is pattern-keyed and that the value objects are not constrained to unique associations, allowing code reuse and flexible factory configuration. + +Scenario 7: Consecutive Register and Lookup + +Details: + TestName: supportsConsecutiveRegisterAndLookup + Description: This test covers the practical use-case where registerLlm is followed by a getLlm lookup. It validates that after registration, the new pattern can be used for instance retrieval (if createLlm is triggered). + +Execution: + Arrange: Register a new pattern and factory that creates a stub BaseLlm. + Act: Call registerLlm, then use getLlm with a modelName matching the new pattern. + Assert: Verify that getLlm returns a BaseLlm instance created by the just-registered factory (and not from any older factory). +Validation: + This validates that the registry and factory map are effectively integrated and that new model patterns immediately impact lookups. + +Scenario 8: Registering an Existing Pattern and Retaining Previous Instances + +Details: + TestName: doesNotRemoveExistingInstancesWhenRegisteringPattern + Description: This test ensures that registerLlm does not clear or modify the existing instances cache when updating factories, only affecting future getLlm calls. + +Execution: + Arrange: Register a modelNamePattern and factory. Obtain a BaseLlm instance using getLlm for a matching modelName. Register a new factory for the same pattern. + Act: Perform the steps in order. + Assert: The existing BaseLlm in instances for the modelName remains unchanged; only future calls may differ. +Validation: + Verifies that registerLlm only updates the factory map and does not have unintended side-effects on the instance cache. This protects against accidental corruption or data loss. + +Scenario 9: Thread Safety under Concurrent Access + +Details: + TestName: isThreadSafeWithConcurrentRegisters + Description: Verifies that calling registerLlm simultaneously from multiple threads with different patterns and factories does not corrupt the llmFactories registry. + +Execution: + Arrange: Spawn multiple threads, each calling registerLlm with a different pattern/factory pair. + Act: Start threads, wait for complete execution. + Assert: Assert that all patterns are registered in llmFactories, each with the appropriate factory. +Validation: + Reinforces that the method and underlying map are thread safe, consistent with the requirements and Java concurrent usage best practices. + +Scenario 10: Registering a Pattern Already Registered by Static Initialization + +Details: + TestName: overwritesStaticInitializedPattern + Description: Ensures registerLlm can overwrite a pattern registered by the class static block (e.g., "gemini-.*"). + +Execution: + Arrange: Attempt to registerLlm using a factory and pattern ("gemini-.*") that is registered in the class static block. + Act: Register with a new factory for the same pattern. + Assert: Check that llmFactories contains the new factory and not the one from static initialization. +Validation: + Confirms that registerLlm allows dynamic updates to static initialization, supporting reconfiguration or test overrides at runtime. + + +*/ + +// ********RoostGPT******** + +package com.google.adk.models; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.AfterEach; +import org.mockito.Mockito; +import java.util.concurrent.ConcurrentHashMap; +import java.util.Map; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; +import org.junit.jupiter.api.*; + +public class LlmRegistryRegisterLlmTest { + private static final String MODEL_PATTERN1 = "test-pattern-1"; + private static final String MODEL_PATTERN2 = "test-pattern-2"; + private static final String MODEL_PATTERN_STATIC = "gemini-.*"; + private static final String MODEL_NAME = "test-model-1"; + private static final String MODEL_NAME_ALT = "test-model-2"; + // Clean up before and after each test to avoid contamination of static maps + @BeforeEach + public void setup() { + // Resetting static maps, only safe for test scope as no setters are exposed. + // Typically, this is not recommended for production code; here for tests only. + LlmRegistry.llmFactories.clear(); + LlmRegistry.instances.clear(); + } + @AfterEach + public void tearDown() { + LlmRegistry.llmFactories.clear(); + LlmRegistry.instances.clear(); + } + // Basic stub for BaseLlm and LlmFactory + public static class BaseLlm {} + public static class LlmFactory { + public BaseLlm create(String modelName) { return new BaseLlm(); } + } + // Helper to mock factories + private LlmFactory mockFactoryReturning(BaseLlm llmInstance) { + LlmFactory factory = mock(LlmFactory.class); + when(factory.create(anyString())).thenReturn(llmInstance); + return factory; + } + // Helper to getLlm similar logic as used in registry, for scenario 7/8 + private BaseLlm getLlm(String modelName) { + if(LlmRegistry.instances.containsKey(modelName)) { + return (BaseLlm)LlmRegistry.instances.get(modelName); + } + for (String pattern : LlmRegistry.llmFactories.keySet()) { + if (pattern != null && modelName.matches(pattern)) { + LlmFactory factory = LlmRegistry.llmFactories.get(pattern); + BaseLlm llm = factory != null ? factory.create(modelName) : null; + LlmRegistry.instances.put(modelName, llm); + return (BaseLlm)llm; + } + } + return null; + } + @Test + @Tag("valid") + public void testRegistersNewModelPatternAndFactory() { + LlmFactory factory = mock(LlmFactory.class); + assertFalse(LlmRegistry.llmFactories.containsKey(MODEL_PATTERN1)); + LlmRegistry.registerLlm(MODEL_PATTERN1, factory); + assertSame(factory, (LlmFactory)LlmRegistry.llmFactories.get(MODEL_PATTERN1)); + } + @Test + @Tag("valid") + public void testOverwritesExistingModelPattern() { + LlmFactory oldFactory = mock(LlmFactory.class); + LlmFactory newFactory = mock(LlmFactory.class); + LlmRegistry.registerLlm(MODEL_PATTERN1, oldFactory); + assertSame(oldFactory, (LlmFactory)LlmRegistry.llmFactories.get(MODEL_PATTERN1)); + LlmRegistry.registerLlm(MODEL_PATTERN1, newFactory); + assertSame(newFactory, (LlmFactory)LlmRegistry.llmFactories.get(MODEL_PATTERN1)); + } + @Test + @Tag("boundary") + public void testHandlesNullFactory() { + String pattern = "null-factory-pattern"; + LlmRegistry.registerLlm(pattern, null); + assertTrue(LlmRegistry.llmFactories.containsKey(pattern)); + assertNull((LlmFactory)LlmRegistry.llmFactories.get(pattern)); + } + @Test + @Tag("boundary") + public void testHandlesNullModelPattern() { + LlmFactory factory = mock(LlmFactory.class); + LlmRegistry.registerLlm(null, factory); + assertTrue(LlmRegistry.llmFactories.containsKey(null)); + assertSame(factory, (LlmFactory)LlmRegistry.llmFactories.get(null)); + } + @Test + @Tag("boundary") + public void testHandlesNullPatternAndFactory() { + LlmRegistry.registerLlm(null, null); + assertTrue(LlmRegistry.llmFactories.containsKey(null)); + assertNull((LlmFactory)LlmRegistry.llmFactories.get(null)); + } + @Test + @Tag("valid") + public void testRegistersSameFactoryWithDifferentPattern() { + LlmFactory factory = mock(LlmFactory.class); + String patternA = "pattern-A"; + String patternB = "pattern-B"; + LlmRegistry.registerLlm(patternA, factory); + LlmRegistry.registerLlm(patternB, factory); + assertSame(factory, (LlmFactory)LlmRegistry.llmFactories.get(patternA)); + assertSame(factory, (LlmFactory)LlmRegistry.llmFactories.get(patternB)); + } + @Test + @Tag("integration") + public void testSupportsConsecutiveRegisterAndLookup() { + String pattern = "integration-pattern"; + LlmFactory factory = mockFactoryReturning(new BaseLlm()); + LlmRegistry.registerLlm(pattern, factory); + String modelName = "integration-pattern"; + BaseLlm found = getLlm(modelName); + assertNotNull(found); + assertTrue(found instanceof BaseLlm); + } + @Test + @Tag("boundary") + public void testDoesNotRemoveExistingInstancesWhenRegisteringPattern() { + String pattern = "instance-retain-pattern"; + String modelName = "instance-retain-pattern"; + LlmFactory factory = mockFactoryReturning(new BaseLlm()); + LlmRegistry.registerLlm(pattern, factory); + BaseLlm originalInstance = getLlm(modelName); + assertNotNull(originalInstance); + LlmFactory newFactory = mockFactoryReturning(new BaseLlm()); + LlmRegistry.registerLlm(pattern, newFactory); + // Existing instance in cache remains unchanged + assertSame(originalInstance, (BaseLlm)LlmRegistry.instances.get(modelName)); + } + @Test + @Tag("integration") + public void testIsThreadSafeWithConcurrentRegisters() throws InterruptedException { + final int THREAD_COUNT = 10; // TODO: Adjust thread count as desired + final String basePattern = "thread-safe-pattern-"; + Thread[] threads = new Thread[THREAD_COUNT]; + LlmFactory[] factories = new LlmFactory[THREAD_COUNT]; + for(int i = 0; i < THREAD_COUNT; i++) { + factories[i] = mock(LlmFactory.class); + final String pattern = basePattern + i; + final LlmFactory factory = factories[i]; + threads[i] = new Thread(() -> LlmRegistry.registerLlm(pattern, factory)); + } + for(Thread t : threads) t.start(); + for(Thread t : threads) t.join(); + for(int i = 0; i < THREAD_COUNT; i++) { + String pattern = basePattern + i; + assertSame(factories[i], (LlmFactory)LlmRegistry.llmFactories.get(pattern)); + } + } + @Test + @Tag("valid") + public void testOverwritesStaticInitializedPattern() { + // Simulate static initialization: register pattern with an initial factory + LlmFactory staticFactory = mock(LlmFactory.class); + LlmRegistry.registerLlm(MODEL_PATTERN_STATIC, staticFactory); + LlmFactory newFactory = mock(LlmFactory.class); + LlmRegistry.registerLlm(MODEL_PATTERN_STATIC, newFactory); + assertSame(newFactory, (LlmFactory)LlmRegistry.llmFactories.get(MODEL_PATTERN_STATIC)); + assertNotSame(staticFactory, (LlmFactory)LlmRegistry.llmFactories.get(MODEL_PATTERN_STATIC)); + } +} \ No newline at end of file