diff --git a/core/pom.xml b/core/pom.xml index fe65715f3..7d2032c7e 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -1,4 +1,4 @@ - + 4.0.0 - com.google.adk google-adk-parent - 0.4.1-SNAPSHOT + 0.4.1-SNAPSHOT + - google-adk Agent Development Kit Agent Development Kit: an open-source, code-first toolkit designed to simplify building, evaluating, and deploying advanced AI agents anywhere. - - - com.anthropic @@ -201,6 +197,15 @@ maven-compiler-plugin + + io.spring.javaformat + spring-javaformat-maven-plugin + 0.0.40 + + + + + - + \ No newline at end of file diff --git a/core/src/test/java/com/google/adk/models/ClaudeGenerateContentTest.java.invalid b/core/src/test/java/com/google/adk/models/ClaudeGenerateContentTest.java.invalid new file mode 100644 index 000000000..09330fac4 --- /dev/null +++ b/core/src/test/java/com/google/adk/models/ClaudeGenerateContentTest.java.invalid @@ -0,0 +1,826 @@ +//This test file is marked invalid as it contains compilation errors. Change the extension to of this file to .java, to manually edit its contents +/* + * 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: /var/tmp/Roost/RoostGPT/adk-java-demo/2d03a3bd-e91c-428d-9cd7-034af8f4f796/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 request + +Details: + TestName: generateContentWithMinimalValidRequest + Description: This test checks that the generateContent method returns a valid Flowable when given a minimal LlmRequest containing only mandatory fields and a simple text Content, with no config or tools specified. +Execution: + Arrange: Prepare an LlmRequest with only one Content (e.g., text "Hello"), no config, no tools, and provide a mock AnthropicClient that returns a valid Message. + Act: Call generateContent with the LlmRequest and stream set to false. + Assert: Confirm that the Flowable emits a correctly mapped LlmResponse containing the expected content. +Validation: + The assertion verifies that Claude correctly processes the minimal input scenario and invokes AnthropicClient with correct parameters. It ensures fundamental request handling works and downstream conversions do not fail. + +Scenario 2: Content generation with model fallback + +Details: + TestName: generateContentUsesFallbackModelWhenRequestModelMissing + Description: This test validates that if LlmRequest.model() is empty, generateContent uses the Claude instance’s default model instead. +Execution: + Arrange: Create an LlmRequest without a model (model() returns Optional.empty()) and a mock AnthropicClient set up to capture the model parameter. + Act: Invoke generateContent with this LlmRequest. + Assert: Check that the MessageCreateParams passed to the AnthropicClient uses the Claude’s own model as the fallback. +Validation: + This confirms compliance with expected model selection logic and prevents failures due to missing model specification, ensuring reliability in client API usage. + +Scenario 3: Handling request with system instructions + +Details: + TestName: generateContentWithSystemInstructionExtractsTextProperly + Description: This test assesses whether generateContent correctly extracts and concatenates system instruction text from LlmRequest.config().systemInstruction() parts and passes it on to the system parameter. +Execution: + Arrange: Mock a system instruction Content containing multiple Part with text fields, create a GenerateContentConfig with this Content, and attach it to LlmRequest. + Act: Call generateContent as usual. + Assert: Verify that the MessageCreateParams‘ system string is correctly formed (concatenating available texts with “\n”). +Validation: + It checks proper processing and extraction of instructions, which is critical for system prompt handling and model output controls. + +Scenario 4: No system instruction present + +Details: + TestName: generateContentWithNoSystemInstructionLeavesSystemEmpty + Description: This scenario inspects the outcome when LlmRequest’s config lacks a system instruction. Claude should set system to an empty string. +Execution: + Arrange: Construct a LlmRequest with a config but with systemInstruction() as Optional.empty(). + Act: Call generateContent. + Assert: Ensure that the “system” field passed to MessageCreateParams is an empty string. +Validation: + The assertion confirms robustness in the absence of system instructions, preventing unintended prompt leakages. + +Scenario 5: Handling tool configuration presence with function declarations + +Details: + TestName: generateContentWithFunctionDeclarationsMapsToolsProperly + Description: This scenario verifies the logic that maps function declarations into Anthropic tools and ToolUnion, and includes them in the MessageCreateParams. +Execution: + Arrange: Provide LlmRequest.config() with Tools containing a non-empty functionDeclarations() list, mock the conversion and validate AnthropicClient receives ToolUnion in parameters. + Act: Call generateContent. + Assert: Confirm that tools are correctly constructed and included, and that toolChoice is also properly set. +Validation: + Ensures integration of model tool functionality, allowing for advanced agent capabilities, and verifies correct mapping and transmission of function tool constructs. + +Scenario 6: Tools specified in request but no functionDeclarations + +Details: + TestName: generateContentWithToolsButNoFunctionDeclarationsSetsToolsToEmpty + Description: Checks that when tools are present but functionDeclarations() is absent or empty, generateContent doesn’t send any tools to the API. +Execution: + Arrange: Prepare config with tools present but functionDeclarations() returns empty Optional or an empty list. + Act: Call generateContent. + Assert: Assert that the tools parameter is ImmutableList.of(), and toolChoice is set if tools() in LlmRequest is not empty. +Validation: + This prevents accidental attempts to invoke tools and maintains command-and-control integrity when tools are not properly defined. + +Scenario 7: Null or malformed contents in request + +Details: + TestName: generateContentWithEmptyOrMalformedContentsThrowsException + Description: Validates that if LlmRequest.contents() is empty or contains parts that cannot be converted, generateContent throws an exception or handles gracefully. +Execution: + Arrange: Provide an LlmRequest with empty contents or contents with unsupported Part types. + Act: Call generateContent. + Assert: Confirm exception is thrown, or error is propagated as expected. +Validation: + This verifies robustness against malformed requests and the method’s error handling, crucial for input validation and preventing runtime errors. + +Scenario 8: Handling empty tools in LlmRequest + +Details: + TestName: generateContentWithEmptyToolsDoesNotSetToolChoice + Description: Checks that when LlmRequest.tools() is empty, generateContent does not set toolChoice, and MessageCreateParams does not receive toolChoice or tools parameters. +Execution: + Arrange: Construct LlmRequest with empty tools and simulate an AnthropicClient. + Act: Call generateContent. + Assert: Ensure toolChoice is null and neither tools nor toolChoice is set in params. +Validation: + It ensures toolChoice logic is strictly followed and prevents repetitive, unnecessary data transmission when tools are not involved. + +Scenario 9: Multiple contents conversion + +Details: + TestName: generateContentWithMultipleContentsConvertsAllProperly + Description: Verifies that when several Content entries are included in LlmRequest.contents(), each is correctly mapped to MessageParam and aggregated for the request. +Execution: + Arrange: Prepare LlmRequest with several distinct Content elements, mock contentToAnthropicMessageParam. + Act: Execute generateContent. + Assert: Confirm that all MessageParams produced match the input Contents and are passed in messages list. +Validation: + This ensures batch processing of user/model prompts and improves reliability when multi-turn or multi-instruction requests are made. + +Scenario 10: Max tokens setting + +Details: + TestName: generateContentWithCustomMaxTokensPropagatesValue + Description: Ensures that the maxTokens field (either default or custom on construction) is propagated into MessageCreateParams as intended. +Execution: + Arrange: Instantiate Claude with a custom maxTokens setting and build a normal LlmRequest. + Act: Call generateContent. + Assert: Check that MessageCreateParams.maxTokens equals Claude’s maxTokens value. +Validation: + This checks that response length and resource constraints are handled correctly according to instance configuration. + +Scenario 11: Return value mapping correctness + +Details: + TestName: generateContentReturnMappingProducesValidLlmResponse + Description: Validates that the returned Flowable emits an LlmResponse correctly mapped from the AnthropicClient Message, using convertAnthropicResponseToLlmResponse. +Execution: + Arrange: Mock the Claude method convertAnthropicResponseToLlmResponse and setup AnthropicClient to return a Message with predictable content. + Act: Call generateContent. + Assert: Verify that the Flowable contains a valid LlmResponse with content as expected. +Validation: + This assures that end-to-end conversion is functioning reliably, producing expected application-level LlmResponse outputs for all valid input combinations. + +Scenario 12: Logging of API responses + +Details: + TestName: generateContentLogsResponseSuccessfully + Description: Checks that a debug log statement is recorded when a response is received from AnthropicClient. +Execution: + Arrange: Set up logger to capture output, mock AnthropicClient to return a Message. + Act: Call generateContent. + Assert: Validate that a log entry is made with level debug including the Message. +Validation: + Successful logging is crucial for traceability and debugging in production scenarios, and this test confirms correct log behavior. + +Scenario 13: Handling contents with functionCall and functionResponse Parts + +Details: + TestName: generateContentHandlesFunctionCallAndResponseParts + Description: Ensures that Contents using Parts with functionCall or functionResponse types are correctly mapped to corresponding Anthropic message block structures. +Execution: + Arrange: Provide LlmRequest.contents() with Content whose parts include functionCall and functionResponse, mock necessary conversions. + Act: Execute generateContent. + Assert: Confirm that MessageParams and ContentBlockParams are correctly constructed for each case. +Validation: + This scenario tests advanced AI features such as tools invocation and returns, ensuring system and agent workflow integration. + +Scenario 14: Streaming mode parameter + +Details: + TestName: generateContentWithStreamingModeDoesNotAlterBehavior + Description: Verifies that the stream boolean passed to generateContent does not affect output since streaming is not yet implemented. +Execution: + Arrange: Build two identical LlmRequests and call generateContent with stream true and false. + Act: Call generateContent in both configurations. + Assert: Assert that output is identical for both invocations. +Validation: + This ensures that the method is future-proof and stable until streaming support is added. + +Scenario 15: Edge case – missing or empty Part texts in systemInstruction + +Details: + TestName: generateContentIgnoresEmptySystemInstructionParts + Description: Tests that if systemInstruction Parts exist but none have text present, the system parameter remains empty. +Execution: + Arrange: Construct a system instruction Content where all Part elements have text as Optional.empty(). + Act: Call generateContent with this LlmRequest. + Assert: Ensure system field is sent as empty. +Validation: + Important to avoid passing garbage or empty instructions to the model, ensuring clarity and correctness for the AI backend. + +Scenario 16: Exception handling in conversion methods + +Details: + TestName: generateContentThrowsForUnsupportedPartTypes + Description: Validates that the method throws UnsupportedOperationException when Part types are not text, functionCall, or functionResponse. +Execution: + Arrange: Prepare Content with a custom, unsupported Part type, and provide as LlmRequest.contents(). + Act: Call generateContent and expect exception. + Assert: Verify method throws UnsupportedOperationException accordingly. +Validation: + Critical to prevent silent failures or incorrect message formatting, this test ensures conversion logic is explicit and safe. + +Scenario 17: Tools with disableParallelToolUse + +Details: + TestName: generateContentSetsToolChoiceWithDisableParallelToolUse + Description: Ensures that when LlmRequest.tools() is non-empty, toolChoice is set using ToolChoiceAuto with disableParallelToolUse set to true. +Execution: + Arrange: Provide LlmRequest.tools() with at least one tool. + Act: Call generateContent. + Assert: Confirm that toolChoice parameter in MessageCreateParams is set and that disableParallelToolUse is true. +Validation: + This maintains important compliance for how tool usage is controlled and tested, especially for multi-step tool calls. + +Scenario 18: Verify correct building of MessageCreateParams + +Details: + TestName: generateContentBuildsMessageCreateParamsWithAllFields + Description: Validates that MessageCreateParams are built with model, system, messages, maxTokens, tools and toolChoice when all options are present. +Execution: + Arrange: Compose LlmRequest with all fields specified: model, systemInstruction, tools, functionDeclarations, etc. + Act: Call generateContent. + Assert: Check that all relevant fields are correctly passed to AnthropicClient. +Validation: + Comprehensive test ensuring complete and correct request construction for full-featured scenarios. + +Scenario 19: Return type is always Flowable + +Details: + TestName: generateContentAlwaysReturnsFlowable + Description: Ensures that regardless of input, generateContent returns a Flowable, not a single object or null. +Execution: + Arrange: Provide diverse variants of LlmRequest. + Act: Call generateContent multiple times. + Assert: Confirm output is always a Flowable instance. +Validation: + Guarantees API contract and integration reliability for downstream consumers. + +Scenario 20: Empty config option + +Details: + TestName: generateContentWithEmptyConfigDoesNotFail + Description: Verifies that when LlmRequest.config() is absent, method functions and does not throw exception. +Execution: + Arrange: LlmRequest with config Optional.empty(). + Act: Call generateContent. + Assert: Confirm successful execution and valid response. +Validation: + Application behaves safely and predictably even without configuration, increasing flexibility for client code. + + +*/ + +// ********RoostGPT******** + +package com.google.adk.models; +import com.anthropic.client.AnthropicClient; +import com.anthropic.models.messages.ContentBlock; +import com.anthropic.models.messages.MessageCreateParams; +import com.anthropic.models.messages.MessageParam; +import com.anthropic.models.messages.MessageParam.Role; +import com.anthropic.models.messages.Message; +import com.anthropic.models.messages.ToolChoice; +import com.anthropic.models.messages.ToolChoiceAuto; +import com.anthropic.models.messages.ToolUnion; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.genai.types.Content; +import com.google.genai.types.FunctionCall; +import com.google.genai.types.FunctionDeclaration; +import com.google.genai.types.GenerateContentConfig; +import com.google.genai.types.Part; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import io.reactivex.rxjava3.core.Flowable; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.Mockito; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import java.util.*; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; +import org.junit.jupiter.api.*; +import com.anthropic.models.messages.ContentBlockParam; +import com.anthropic.models.messages.TextBlockParam; +import com.anthropic.models.messages.Tool; +import com.anthropic.models.messages.ToolResultBlockParam; +import com.anthropic.models.messages.ToolUseBlockParam; +import com.fasterxml.jackson.core.type.TypeReference; +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 java.util.stream.Collectors; + +// NOTE: AnthropicClient.Messages does not exist as a nested or inner class. Instead, the API usage should be changed to use proper stubbing on the interface itself and remove all references to 'AnthropicClient.Messages'. +// Do not use: +// @Mock +// private AnthropicClient.Messages messagesApi; +// Instead, we invoke methods directly on the `anthropicClient` mock that will be configured to return a mock for the relevant methods. +@ExtendWith(MockitoExtension.class) +class ClaudeGenerateContentTest extends BaseLlm { + @Mock + private AnthropicClient anthropicClient; + @Mock + private Logger mockLogger; + private Claude claude; + private static final String DEFAULT_MODEL = "claude-3-opus"; + private static final int DEFAULT_MAX_TOKENS = 8192; + @BeforeEach + void setUp() { + claude = new Claude(DEFAULT_MODEL, anthropicClient, DEFAULT_MAX_TOKENS); + } + private Content createSimpleContent(String text) { + Part textPart = Part.ofText(text); + return Content.builder().role("user").parts(Optional.of(ImmutableList.of(textPart))).build(); + } + private LlmRequest createMinimalLlmRequest(Content content) { + return LlmRequest.builder() + .contents(ImmutableList.of(content)) + .tools(ImmutableMap.of()) + .liveConnectConfig(LiveConnectConfig.builder().build()) + .build(); + } + @Test + @Tag("valid") + @DisplayName("generateContentWithMinimalValidRequest") + public void testGenerateContentWithMinimalValidRequest() { + Content textContent = createSimpleContent("Hello"); + LlmRequest llmRequest = createMinimalLlmRequest(textContent); + Message message = mock(Message.class); + AnthropicClient.Messages messagesApi = mock(AnthropicClient.Messages.class); + when(anthropicClient.messages()).thenReturn(messagesApi); + when(messagesApi.create(any())).thenReturn(message); + MessageParam expectedMsgParam = MessageParam.builder().role(Role.USER).contentBlocks(ImmutableList.of()).build(); + Claude spyClaude = Mockito.spy(claude); + doReturn(expectedMsgParam).when(spyClaude).contentToAnthropicMessageParam(any()); + LlmResponse expectedResponse = LlmResponse.builder().content(Content.builder().role("model").parts(ImmutableList.of()).build()).build(); + doReturn(expectedResponse).when(spyClaude).convertAnthropicResponseToLlmResponse(message); + Flowable flowable = spyClaude.generateContent(llmRequest, false); + LlmResponse actualResponse = flowable.blockingFirst(); + assertEquals((Object) expectedResponse, actualResponse); + } + @Test + @Tag("boundary") + @DisplayName("generateContentUsesFallbackModelWhenRequestModelMissing") + public void testGenerateContentUsesFallbackModelWhenRequestModelMissing() { + Content textContent = createSimpleContent("Hi, fallback model."); + LlmRequest llmRequest = LlmRequest.builder() + .contents(ImmutableList.of(textContent)) + .tools(ImmutableMap.of()) + .liveConnectConfig(LiveConnectConfig.builder().build()) + .build(); + LlmRequest spyLlmRequest = Mockito.spy(llmRequest); + when(spyLlmRequest.model()).thenReturn(Optional.empty()); + Message message = mock(Message.class); + AnthropicClient.Messages messagesApi = mock(AnthropicClient.Messages.class); + when(anthropicClient.messages()).thenReturn(messagesApi); + when(messagesApi.create(any())).thenReturn(message); + Claude spyClaude = Mockito.spy(claude); + doReturn(MessageParam.builder().role(Role.USER).contentBlocks(ImmutableList.of()).build()) + .when(spyClaude).contentToAnthropicMessageParam(any()); + doReturn(LlmResponse.builder().build()).when(spyClaude).convertAnthropicResponseToLlmResponse(any()); + spyClaude.generateContent(spyLlmRequest, false); + ArgumentCaptor captor = ArgumentCaptor.forClass(MessageCreateParams.class); + verify(messagesApi).create(captor.capture()); + MessageCreateParams params = captor.getValue(); + assertEquals((Object) DEFAULT_MODEL, params.model()); + } + @Test + @Tag("valid") + @DisplayName("generateContentWithSystemInstructionExtractsTextProperly") + public void testGenerateContentWithSystemInstructionExtractsTextProperly() { + Part part1 = Part.ofText("System A"); + Part part2 = Part.ofText("System B"); + Content sysContent = Content.builder().role("system").parts(Optional.of(ImmutableList.of(part1, part2))).build(); + GenerateContentConfig config = GenerateContentConfig.builder().systemInstruction(Optional.of(sysContent)).build(); + LlmRequest llmRequest = LlmRequest.builder() + .contents(ImmutableList.of(createSimpleContent("Hi"))) + .config(Optional.of(config)) + .tools(ImmutableMap.of()) + .liveConnectConfig(LiveConnectConfig.builder().build()) + .build(); + Message message = mock(Message.class); + AnthropicClient.Messages messagesApi = mock(AnthropicClient.Messages.class); + when(anthropicClient.messages()).thenReturn(messagesApi); + when(messagesApi.create(any())).thenReturn(message); + Claude spyClaude = Mockito.spy(claude); + doReturn(MessageParam.builder().role(Role.USER).contentBlocks(ImmutableList.of()).build()) + .when(spyClaude).contentToAnthropicMessageParam(any()); + doReturn(LlmResponse.builder().build()).when(spyClaude).convertAnthropicResponseToLlmResponse(any()); + spyClaude.generateContent(llmRequest, false); + ArgumentCaptor captor = ArgumentCaptor.forClass(MessageCreateParams.class); + verify(messagesApi).create(captor.capture()); + MessageCreateParams params = captor.getValue(); + assertEquals((Object) "System A\nSystem B", params.system()); + } + @Test + @Tag("boundary") + @DisplayName("generateContentWithNoSystemInstructionLeavesSystemEmpty") + public void testGenerateContentWithNoSystemInstructionLeavesSystemEmpty() { + GenerateContentConfig config = GenerateContentConfig.builder().systemInstruction(Optional.empty()).build(); + LlmRequest llmRequest = LlmRequest.builder() + .contents(ImmutableList.of(createSimpleContent("Hello"))) + .config(Optional.of(config)) + .tools(ImmutableMap.of()) + .liveConnectConfig(LiveConnectConfig.builder().build()) + .build(); + Message message = mock(Message.class); + AnthropicClient.Messages messagesApi = mock(AnthropicClient.Messages.class); + when(anthropicClient.messages()).thenReturn(messagesApi); + when(messagesApi.create(any())).thenReturn(message); + Claude spyClaude = Mockito.spy(claude); + doReturn(MessageParam.builder().role(Role.USER).contentBlocks(ImmutableList.of()).build()) + .when(spyClaude).contentToAnthropicMessageParam(any()); + doReturn(LlmResponse.builder().build()).when(spyClaude).convertAnthropicResponseToLlmResponse(any()); + spyClaude.generateContent(llmRequest, false); + ArgumentCaptor captor = ArgumentCaptor.forClass(MessageCreateParams.class); + verify(messagesApi).create(captor.capture()); + MessageCreateParams params = captor.getValue(); + assertEquals((Object) "", params.system()); + } + @Test + @Tag("valid") + @DisplayName("generateContentWithFunctionDeclarationsMapsToolsProperly") + public void testGenerateContentWithFunctionDeclarationsMapsToolsProperly() { + FunctionDeclaration functionDecl = FunctionDeclaration.builder().name("my_func").build(); + Tools toolsWrapper = Tools.builder().functionDeclarations(Optional.of(ImmutableList.of(functionDecl))).build(); + GenerateContentConfig config = GenerateContentConfig.builder().tools(Optional.of(ImmutableList.of(toolsWrapper))).build(); + LlmRequest llmRequest = LlmRequest.builder() + .contents(ImmutableList.of(createSimpleContent("Tool test"))) + .config(Optional.of(config)) + .tools(ImmutableMap.of()) + .liveConnectConfig(LiveConnectConfig.builder().build()) + .build(); + Message message = mock(Message.class); + AnthropicClient.Messages messagesApi = mock(AnthropicClient.Messages.class); + when(anthropicClient.messages()).thenReturn(messagesApi); + when(messagesApi.create(any())).thenReturn(message); + Claude spyClaude = Mockito.spy(claude); + doReturn(MessageParam.builder().role(Role.USER).contentBlocks(ImmutableList.of()).build()) + .when(spyClaude).contentToAnthropicMessageParam(any()); + doReturn(ToolUnion.ofTool(new Tool())).when(spyClaude).functionDeclarationToAnthropicTool(any()); + doReturn(LlmResponse.builder().build()).when(spyClaude).convertAnthropicResponseToLlmResponse(any()); + spyClaude.generateContent(llmRequest, false); + ArgumentCaptor captor = ArgumentCaptor.forClass(MessageCreateParams.class); + verify(messagesApi).create(captor.capture()); + MessageCreateParams params = captor.getValue(); + assertTrue((boolean) (params.tools() != null && !params.tools().isEmpty())); + assertNotNull(params.toolChoice()); + } + @Test + @Tag("boundary") + @DisplayName("generateContentWithToolsButNoFunctionDeclarationsSetsToolsToEmpty") + public void testGenerateContentWithToolsButNoFunctionDeclarationsSetsToolsToEmpty() { + Tools toolsWrapper = Tools.builder().functionDeclarations(Optional.empty()).build(); + GenerateContentConfig config = GenerateContentConfig.builder().tools(Optional.of(ImmutableList.of(toolsWrapper))).build(); + LlmRequest llmRequest = LlmRequest.builder() + .contents(ImmutableList.of(createSimpleContent("Tool test"))) + .config(Optional.of(config)) + .tools(ImmutableMap.of("toolA", mock(BaseTool.class))) + .liveConnectConfig(LiveConnectConfig.builder().build()) + .build(); + Message message = mock(Message.class); + AnthropicClient.Messages messagesApi = mock(AnthropicClient.Messages.class); + when(anthropicClient.messages()).thenReturn(messagesApi); + when(messagesApi.create(any())).thenReturn(message); + Claude spyClaude = Mockito.spy(claude); + doReturn(MessageParam.builder().role(Role.USER).contentBlocks(ImmutableList.of()).build()) + .when(spyClaude).contentToAnthropicMessageParam(any()); + doReturn(LlmResponse.builder().build()).when(spyClaude).convertAnthropicResponseToLlmResponse(any()); + spyClaude.generateContent(llmRequest, false); + ArgumentCaptor captor = ArgumentCaptor.forClass(MessageCreateParams.class); + verify(messagesApi).create(captor.capture()); + MessageCreateParams params = captor.getValue(); + assertTrue((boolean) params.tools().isEmpty()); + assertNotNull(params.toolChoice()); + } + @Test + @Tag("invalid") + @DisplayName("generateContentWithEmptyOrMalformedContentsThrowsException") + public void testGenerateContentWithEmptyOrMalformedContentsThrowsException() { + LlmRequest llmRequest = LlmRequest.builder() + .contents(ImmutableList.of()) + .tools(ImmutableMap.of()) + .liveConnectConfig(LiveConnectConfig.builder().build()) + .build(); + Claude spyClaude = Mockito.spy(claude); + doThrow(new UnsupportedOperationException("Unsupported type")).when(spyClaude).contentToAnthropicMessageParam(any()); + assertThrows(UnsupportedOperationException.class, () -> spyClaude.generateContent(llmRequest, false).blockingFirst()); + } + @Test + @Tag("boundary") + @DisplayName("generateContentWithEmptyToolsDoesNotSetToolChoice") + public void testGenerateContentWithEmptyToolsDoesNotSetToolChoice() { + LlmRequest llmRequest = LlmRequest.builder() + .contents(ImmutableList.of(createSimpleContent("No tool choice"))) + .tools(ImmutableMap.of()) + .liveConnectConfig(LiveConnectConfig.builder().build()) + .build(); + Message message = mock(Message.class); + AnthropicClient.Messages messagesApi = mock(AnthropicClient.Messages.class); + when(anthropicClient.messages()).thenReturn(messagesApi); + when(messagesApi.create(any())).thenReturn(message); + Claude spyClaude = Mockito.spy(claude); + doReturn(MessageParam.builder().role(Role.USER).contentBlocks(ImmutableList.of()).build()) + .when(spyClaude).contentToAnthropicMessageParam(any()); + doReturn(LlmResponse.builder().build()).when(spyClaude).convertAnthropicResponseToLlmResponse(any()); + spyClaude.generateContent(llmRequest, false); + ArgumentCaptor captor = ArgumentCaptor.forClass(MessageCreateParams.class); + verify(messagesApi).create(captor.capture()); + MessageCreateParams params = captor.getValue(); + assertNull(params.toolChoice()); + assertTrue((boolean) params.tools().isEmpty()); + } + @Test + @Tag("valid") + @DisplayName("generateContentWithMultipleContentsConvertsAllProperly") + public void testGenerateContentWithMultipleContentsConvertsAllProperly() { + Content c1 = createSimpleContent("A"); + Content c2 = createSimpleContent("B"); + Content c3 = createSimpleContent("C"); + LlmRequest llmRequest = LlmRequest.builder() + .contents(ImmutableList.of(c1, c2, c3)) + .tools(ImmutableMap.of()) + .liveConnectConfig(LiveConnectConfig.builder().build()) + .build(); + Message message = mock(Message.class); + AnthropicClient.Messages messagesApi = mock(AnthropicClient.Messages.class); + when(anthropicClient.messages()).thenReturn(messagesApi); + when(messagesApi.create(any())).thenReturn(message); + Claude spyClaude = Mockito.spy(claude); + doReturn(MessageParam.builder().role(Role.USER).contentBlocks(ImmutableList.of()).build()) + .when(spyClaude).contentToAnthropicMessageParam(any()); + doReturn(LlmResponse.builder().build()).when(spyClaude).convertAnthropicResponseToLlmResponse(any()); + spyClaude.generateContent(llmRequest, false); + ArgumentCaptor captor = ArgumentCaptor.forClass(MessageCreateParams.class); + verify(messagesApi).create(captor.capture()); + MessageCreateParams params = captor.getValue(); + assertEquals((int) 3, params.messages().size()); + } + @Test + @Tag("boundary") + @DisplayName("generateContentWithCustomMaxTokensPropagatesValue") + public void testGenerateContentWithCustomMaxTokensPropagatesValue() { + int customTokens = 2345; + Claude customClaude = new Claude("custom-model", anthropicClient, customTokens); + Content textContent = createSimpleContent("Tokens please"); + LlmRequest llmRequest = createMinimalLlmRequest(textContent); + Message message = mock(Message.class); + AnthropicClient.Messages messagesApi = mock(AnthropicClient.Messages.class); + when(anthropicClient.messages()).thenReturn(messagesApi); + when(messagesApi.create(any())).thenReturn(message); + Claude spyClaude = Mockito.spy(customClaude); + doReturn(MessageParam.builder().role(Role.USER).contentBlocks(ImmutableList.of()).build()) + .when(spyClaude).contentToAnthropicMessageParam(any()); + doReturn(LlmResponse.builder().build()).when(spyClaude).convertAnthropicResponseToLlmResponse(any()); + spyClaude.generateContent(llmRequest, false); + ArgumentCaptor captor = ArgumentCaptor.forClass(MessageCreateParams.class); + verify(messagesApi).create(captor.capture()); + MessageCreateParams params = captor.getValue(); + assertEquals((int) customTokens, params.maxTokens()); + } + @Test + @Tag("valid") + @DisplayName("generateContentReturnMappingProducesValidLlmResponse") + public void testGenerateContentReturnMappingProducesValidLlmResponse() { + Content textContent = createSimpleContent("Mapping check."); + LlmRequest llmRequest = createMinimalLlmRequest(textContent); + Message message = mock(Message.class); + AnthropicClient.Messages messagesApi = mock(AnthropicClient.Messages.class); + when(anthropicClient.messages()).thenReturn(messagesApi); + when(messagesApi.create(any())).thenReturn(message); + Claude spyClaude = Mockito.spy(claude); + doReturn(MessageParam.builder().role(Role.USER).contentBlocks(ImmutableList.of()).build()) + .when(spyClaude).contentToAnthropicMessageParam(any()); + LlmResponse expectedResponse = LlmResponse.builder().content(Content.builder().role("model").parts(ImmutableList.of()).build()).build(); + doReturn(expectedResponse).when(spyClaude).convertAnthropicResponseToLlmResponse(message); + Flowable flowable = spyClaude.generateContent(llmRequest, false); + assertEquals((Object) expectedResponse, flowable.blockingFirst()); + } + @Test + @Tag("integration") + @DisplayName("generateContentLogsResponseSuccessfully") + public void testGenerateContentLogsResponseSuccessfully() { + Content textContent = createSimpleContent("Log test."); + LlmRequest llmRequest = createMinimalLlmRequest(textContent); + Message message = mock(Message.class); + AnthropicClient.Messages messagesApi = mock(AnthropicClient.Messages.class); + when(anthropicClient.messages()).thenReturn(messagesApi); + when(messagesApi.create(any())).thenReturn(message); + Claude spyClaude = Mockito.spy(claude); + doReturn(MessageParam.builder().role(Role.USER).contentBlocks(ImmutableList.of()).build()) + .when(spyClaude).contentToAnthropicMessageParam(any()); + doReturn(LlmResponse.builder().build()).when(spyClaude).convertAnthropicResponseToLlmResponse(any()); + spyClaude.logger = mockLogger; + spyClaude.generateContent(llmRequest, false); + verify(mockLogger, times(1)).debug(anyString(), eq(message)); + } + @Test + @Tag("valid") + @DisplayName("generateContentHandlesFunctionCallAndResponseParts") + public void testGenerateContentHandlesFunctionCallAndResponseParts() { + Part call = Part.ofFunctionCall(FunctionCall.builder().name("myFunc").build()); + Part response = Part.ofFunctionResponse("result", "payload"); + Content content = Content.builder().role("user").parts(Optional.of(ImmutableList.of(call, response))).build(); + LlmRequest llmRequest = createMinimalLlmRequest(content); + Message message = mock(Message.class); + AnthropicClient.Messages messagesApi = mock(AnthropicClient.Messages.class); + when(anthropicClient.messages()).thenReturn(messagesApi); + when(messagesApi.create(any())).thenReturn(message); + Claude spyClaude = Mockito.spy(claude); + doReturn(MessageParam.builder().role(Role.USER).contentBlocks(ImmutableList.of()).build()) + .when(spyClaude).contentToAnthropicMessageParam(any()); + doReturn(LlmResponse.builder().build()).when(spyClaude).convertAnthropicResponseToLlmResponse(any()); + Flowable flowable = spyClaude.generateContent(llmRequest, false); + assertNotNull((Object) flowable.blockingFirst()); + } + @Test + @Tag("boundary") + @DisplayName("generateContentWithStreamingModeDoesNotAlterBehavior") + public void testGenerateContentWithStreamingModeDoesNotAlterBehavior() { + Content content = createSimpleContent("Stream test"); + LlmRequest llmRequest = createMinimalLlmRequest(content); + Message message = mock(Message.class); + AnthropicClient.Messages messagesApi = mock(AnthropicClient.Messages.class); + when(anthropicClient.messages()).thenReturn(messagesApi); + when(messagesApi.create(any())).thenReturn(message); + Claude spyClaude = Mockito.spy(claude); + doReturn(MessageParam.builder().role(Role.USER).contentBlocks(ImmutableList.of()).build()) + .when(spyClaude).contentToAnthropicMessageParam(any()); + LlmResponse expectedResponse = LlmResponse.builder().build(); + doReturn(expectedResponse).when(spyClaude).convertAnthropicResponseToLlmResponse(message); + Flowable flowable1 = spyClaude.generateContent(llmRequest, true); + Flowable flowable2 = spyClaude.generateContent(llmRequest, false); + assertEquals((Object) flowable1.blockingFirst(), flowable2.blockingFirst()); + } + @Test + @Tag("boundary") + @DisplayName("generateContentIgnoresEmptySystemInstructionParts") + public void testGenerateContentIgnoresEmptySystemInstructionParts() { + Part emptyPart1 = Part.ofText(""); + Part emptyPart2 = Part.ofFunctionCall(FunctionCall.builder().name("empty").build()); + List parts = Arrays.asList(emptyPart1, emptyPart2); + Content systemContent = Content.builder().role("system").parts(Optional.of(ImmutableList.copyOf(parts))).build(); + GenerateContentConfig config = GenerateContentConfig.builder().systemInstruction(Optional.of(systemContent)).build(); + LlmRequest llmRequest = LlmRequest.builder() + .contents(ImmutableList.of(createSimpleContent("Empty system parts"))) + .config(Optional.of(config)) + .tools(ImmutableMap.of()) + .liveConnectConfig(LiveConnectConfig.builder().build()) + .build(); + Message message = mock(Message.class); + AnthropicClient.Messages messagesApi = mock(AnthropicClient.Messages.class); + when(anthropicClient.messages()).thenReturn(messagesApi); + when(messagesApi.create(any())).thenReturn(message); + Claude spyClaude = Mockito.spy(claude); + doReturn(MessageParam.builder().role(Role.USER).contentBlocks(ImmutableList.of()).build()) + .when(spyClaude).contentToAnthropicMessageParam(any()); + doReturn(LlmResponse.builder().build()).when(spyClaude).convertAnthropicResponseToLlmResponse(any()); + spyClaude.generateContent(llmRequest, false); + ArgumentCaptor captor = ArgumentCaptor.forClass(MessageCreateParams.class); + verify(messagesApi).create(captor.capture()); + MessageCreateParams params = captor.getValue(); + assertEquals((Object) "", params.system()); + } + @Test + @Tag("invalid") + @DisplayName("generateContentThrowsForUnsupportedPartTypes") + public void testGenerateContentThrowsForUnsupportedPartTypes() { + Part unsupportedPart = Part.ofCustomType("unsupported", Optional.empty()); + Content content = Content.builder().role("user").parts(Optional.of(ImmutableList.of(unsupportedPart))).build(); + LlmRequest llmRequest = createMinimalLlmRequest(content); + Claude spyClaude = Mockito.spy(claude); + doThrow(new UnsupportedOperationException()).when(spyClaude).contentToAnthropicMessageParam(any()); + assertThrows(UnsupportedOperationException.class, () -> spyClaude.generateContent(llmRequest, false).blockingFirst()); + } + @Test + @Tag("valid") + @DisplayName("generateContentSetsToolChoiceWithDisableParallelToolUse") + public void testGenerateContentSetsToolChoiceWithDisableParallelToolUse() { + LlmRequest llmRequest = LlmRequest.builder() + .contents(ImmutableList.of(createSimpleContent("Parallel tool disable"))) + .tools(ImmutableMap.of("toolA", mock(BaseTool.class))) + .liveConnectConfig(LiveConnectConfig.builder().build()) + .build(); + Message message = mock(Message.class); + AnthropicClient.Messages messagesApi = mock(AnthropicClient.Messages.class); + when(anthropicClient.messages()).thenReturn(messagesApi); + when(messagesApi.create(any())).thenReturn(message); + Claude spyClaude = Mockito.spy(claude); + doReturn(MessageParam.builder().role(Role.USER).contentBlocks(ImmutableList.of()).build()) + .when(spyClaude).contentToAnthropicMessageParam(any()); + doReturn(LlmResponse.builder().build()).when(spyClaude).convertAnthropicResponseToLlmResponse(any()); + spyClaude.generateContent(llmRequest, false); + ArgumentCaptor captor = ArgumentCaptor.forClass(MessageCreateParams.class); + verify(messagesApi).create(captor.capture()); + MessageCreateParams params = captor.getValue(); + assertNotNull(params.toolChoice()); + ToolChoiceAuto autoChoice = (ToolChoiceAuto) params.toolChoice().choice(); + assertTrue((boolean) autoChoice.disableParallelToolUse()); + } + @Test + @Tag("integration") + @DisplayName("generateContentBuildsMessageCreateParamsWithAllFields") + public void testGenerateContentBuildsMessageCreateParamsWithAllFields() { + FunctionDeclaration functionDecl = FunctionDeclaration.builder().name("fullfunc").build(); + Tools toolsWrapper = Tools.builder().functionDeclarations(Optional.of(ImmutableList.of(functionDecl))).build(); + Part sysPart = Part.ofText("System Full"); + Content sysContent = Content.builder().role("system").parts(Optional.of(ImmutableList.of(sysPart))).build(); + GenerateContentConfig config = GenerateContentConfig.builder().systemInstruction(Optional.of(sysContent)) + .tools(Optional.of(ImmutableList.of(toolsWrapper))) + .build(); + Content content = createSimpleContent("Full-featured"); + LlmRequest llmRequest = LlmRequest.builder() + .contents(ImmutableList.of(content)) + .config(Optional.of(config)) + .tools(ImmutableMap.of("toolA", mock(BaseTool.class))) + .liveConnectConfig(LiveConnectConfig.builder().build()) + .build(); + Message message = mock(Message.class); + AnthropicClient.Messages messagesApi = mock(AnthropicClient.Messages.class); + when(anthropicClient.messages()).thenReturn(messagesApi); + when(messagesApi.create(any())).thenReturn(message); + Claude spyClaude = Mockito.spy(claude); + doReturn(MessageParam.builder().role(Role.USER).contentBlocks(ImmutableList.of()).build()) + .when(spyClaude).contentToAnthropicMessageParam(any()); + doReturn(ToolUnion.ofTool(new Tool())).when(spyClaude).functionDeclarationToAnthropicTool(any()); + doReturn(LlmResponse.builder().build()).when(spyClaude).convertAnthropicResponseToLlmResponse(any()); + spyClaude.generateContent(llmRequest, false); + ArgumentCaptor captor = ArgumentCaptor.forClass(MessageCreateParams.class); + verify(messagesApi).create(captor.capture()); + MessageCreateParams params = captor.getValue(); + assertEquals((Object) DEFAULT_MODEL, params.model()); + assertEquals((Object) "System Full", params.system()); + assertEquals((int) 1, params.messages().size()); + assertTrue((boolean) params.maxTokens() > 0); + assertTrue((boolean) params.tools().size() > 0); + assertNotNull(params.toolChoice()); + } + @Test + @Tag("valid") + @DisplayName("generateContentAlwaysReturnsFlowable") + public void testGenerateContentAlwaysReturnsFlowable() { + Content content = createSimpleContent("Flowable test"); + LlmRequest llmRequest = createMinimalLlmRequest(content); + Message message = mock(Message.class); + AnthropicClient.Messages messagesApi = mock(AnthropicClient.Messages.class); + when(anthropicClient.messages()).thenReturn(messagesApi); + when(messagesApi.create(any())).thenReturn(message); + Claude spyClaude = Mockito.spy(claude); + doReturn(MessageParam.builder().role(Role.USER).contentBlocks(ImmutableList.of()).build()) + .when(spyClaude).contentToAnthropicMessageParam(any()); + doReturn(LlmResponse.builder().build()).when(spyClaude).convertAnthropicResponseToLlmResponse(any()); + Flowable flowable = spyClaude.generateContent(llmRequest, false); + assertTrue(flowable instanceof Flowable); + } + @Test + @Tag("boundary") + @DisplayName("generateContentWithEmptyConfigDoesNotFail") + public void testGenerateContentWithEmptyConfigDoesNotFail() { + Content content = createSimpleContent("No config"); + LlmRequest llmRequest = LlmRequest.builder() + .contents(ImmutableList.of(content)) + .config(Optional.empty()) + .tools(ImmutableMap.of()) + .liveConnectConfig(LiveConnectConfig.builder().build()) + .build(); + Message message = mock(Message.class); + AnthropicClient.Messages messagesApi = mock(AnthropicClient.Messages.class); + when(anthropicClient.messages()).thenReturn(messagesApi); + when(messagesApi.create(any())).thenReturn(message); + Claude spyClaude = Mockito.spy(claude); + doReturn(MessageParam.builder().role(Role.USER).contentBlocks(ImmutableList.of()).build()) + .when(spyClaude).contentToAnthropicMessageParam(any()); + doReturn(LlmResponse.builder().build()).when(spyClaude).convertAnthropicResponseToLlmResponse(any()); + Flowable flowable = spyClaude.generateContent(llmRequest, false); + assertNotNull((Object) flowable.blockingFirst()); + } +} \ 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..837208007 --- /dev/null +++ b/core/src/test/java/com/google/adk/models/LlmRegistryRegisterLlmTest.java @@ -0,0 +1,365 @@ +/* + * 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 Name Pattern and Factory + +Details: + TestName: registerNewPatternAndFactory + Description: This test case verifies that when the registerLlm method is invoked with a new, previously unregistered modelNamePattern and a valid factory, the mapping is correctly inserted into the llmFactories map. + +Execution: + Arrange: Prepare a unique model name pattern string and a mock or fake implementation of LlmFactory. + Act: Call registerLlm with the unique pattern and the factory. + Assert: Check that llmFactories contains the new pattern with its associated factory. + +Validation: + The assertion confirms that a new pattern-factory association is registered. This ensures extendability and correctness in dynamically registering new model providers. + + +Scenario 2: Overwriting an Existing Factory for an Existing Pattern + +Details: + TestName: overwriteExistingPatternFactory + Description: This test checks whether calling registerLlm with an already-registered modelNamePattern will successfully overwrite the existing factory associated with that pattern in llmFactories. + +Execution: + Arrange: Add a pattern with an initial factory to llmFactories or via registerLlm; create a second factory. + Act: Call registerLlm for the same pattern using the new factory. + Assert: Confirm that llmFactories maps the pattern to the new factory, replacing the old one. + +Validation: + The assertion validates the expected overwrite behavior of the map, which is crucial to support factory updates or hot-swapping in runtime. + + +Scenario 3: Registering a Null Pattern + +Details: + TestName: registerNullPattern + Description: This test checks what happens if registerLlm is invoked with a null modelNamePattern. It's intended to verify whether the ConcurrentHashMap accepts null keys and how the method handles this invalid input. + +Execution: + Arrange: Create a valid instance of LlmFactory. + Act: Call registerLlm with null as the pattern and the valid factory. + Assert: Validate the resulting behavior (expecting a NullPointerException or equivalent behavior due to ConcurrentHashMap policy). + +Validation: + The assertion demonstrates how the registry enforces or fails to enforce non-null keys, preventing accidental configuration errors and runtime faults. + + +Scenario 4: Registering a Null Factory + +Details: + TestName: registerNullFactory + Description: This test ensures the method’s behavior when a valid modelNamePattern is registered but the factory argument is null. + +Execution: + Arrange: Prepare a valid model name pattern. + Act: Call registerLlm with the valid pattern and a null factory. + Assert: Check for the presence of the pattern in llmFactories with a null value or an exception if thrown. + +Validation: + The assertion is necessary to clarify if the implementation supports null factories, which could cause runtime failures when creating LLM instances. + + +Scenario 5: Registering Duplicate Patterns Concurrently + +Details: + TestName: registerPatternConcurrently + Description: This test ensures that concurrent invocations of registerLlm for the same pattern but with different factories work correctly without causing data races or inconsistent state in llmFactories. + +Execution: + Arrange: Create multiple threads, each with the same pattern and different factory instances. + Act: Start all threads to invoke registerLlm concurrently. + Assert: After all threads finish, verify that llmFactories contains the pattern and one of the registered factories. + +Validation: + Verifies thread-safety and correct atomic map updating, essential in a concurrent environment such as a registry for LLM providers. + + +Scenario 6: Registering a Pattern that Matches Built-in Factories + +Details: + TestName: registerConflictingBuiltinPattern + Description: This test assesses the result of registering a modelNamePattern that is identical to or overlaps with one of the patterns registered in the static initializer block (such as "gemini-.*"). + +Execution: + Arrange: Prepare a pattern (like "gemini-.*") used in static registration and a test factory. + Act: Call registerLlm with this pattern and the custom factory. + Assert: Check that llmFactories now maps this pattern to the latest factory, overriding the built-in one. + +Validation: + This checks if user-supplied registrations can override pre-registered (default) factories, which could affect base functionality and configurability. + + +Scenario 7: Registering an Empty String as Pattern + +Details: + TestName: registerEmptyStringPattern + Description: This test determines if an empty string pattern can be registered and properly stored in llmFactories. + +Execution: + Arrange: Create a valid LlmFactory. + Act: Call registerLlm with an empty string and the factory. + Assert: Verify that llmFactories has an entry with the empty string as the key and the provided factory as the value. + +Validation: + Needed to see how the system handles potentially invalid or catch-all configurations, as empty patterns might be applied broadly or lead to ambiguous matching. + + +Scenario 8: Registering Multiple Distinct Patterns + +Details: + TestName: registerMultipleDistinctPatterns + Description: This test checks whether registerLlm can successfully register several different modelNamePatterns (all unique) with distinct factories, ensuring the map is additive and not destructive to previous entries. + +Execution: + Arrange: Prepare several unique patterns and their associated factories. + Act: Invoke registerLlm multiple times (one per pattern/factory pair). + Assert: Confirm that all patterns are present in llmFactories, each mapped to the correct factory. + +Validation: + Demonstrates the ability to support multiple providers, which is central to the extensibility of the registry. + + +Scenario 9: Registering a Pattern with Special Regex Characters + +Details: + TestName: registerRegexSpecialCharsPattern + Description: This test validates that complex or special-regex-character patterns are correctly registered and retrievable from llmFactories. + +Execution: + Arrange: Define a pattern with special regex symbols (e.g., "^apigee-\\d{3}$") and a suitable factory. + Act: Call registerLlm with this regex and the factory. + Assert: Confirm the pattern exists as entered in llmFactories, with the expected factory. + +Validation: + Important for supporting advanced matching capabilities, ensuring users can employ precise or complex regex patterns without data loss or corruption. + + +Scenario 10: Registering a Factory with a Pattern Already Having Null Value + +Details: + TestName: registerFactoryWithPatternHavingNullValue + Description: This test simulates a case where the llmFactories map (perhaps due to a previous faulty registration) has a pattern mapped to null and verifies that re-registering with a valid factory replaces the null. + +Execution: + Arrange: Insert a null value into llmFactories for a given pattern (using registerLlm or direct insertion if possible). + Act: Call registerLlm with the pattern and a valid factory. + Assert: Check that the pattern now maps to the new, non-null factory. + +Validation: + Ensures resiliency when recovering from unintentionally broken map entries, a plausible scenario in mutable registries. + +*/ + +// ********RoostGPT******** + +package com.google.adk.models; + +import java.util.Map; +import org.junit.jupiter.api.*; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +// Mocking dependencies according to instructions. +public class LlmRegistryRegisterLlmTest { + + // Using test setup to clean static map before each test + @BeforeEach + public void setUp() { + // Clear static map to avoid cross-test pollution + Map llmFactories = getLlmFactories(); + llmFactories.clear(); + } + + private Map getLlmFactories() { + // Reflective access to static field for validation, according to instructions + try { + java.lang.reflect.Field f = LlmRegistry.class.getDeclaredField("llmFactories"); + f.setAccessible(true); + return (Map) f.get(null); + } catch (Exception ex) { + throw new RuntimeException("Reflection failed", ex); + } + } + + @Test + @Tag("valid") + @DisplayName("Scenario 1: Registering a New Model Name Pattern and Factory") + public void testRegisterNewPatternAndFactory() { + String uniquePattern = "test-model-abc"; + LlmRegistry.LlmFactory mockFactory = Mockito.mock(LlmRegistry.LlmFactory.class); + LlmRegistry.registerLlm(uniquePattern, mockFactory); + Assertions.assertEquals( + mockFactory, + getLlmFactories().get(uniquePattern), + "Factory should be registered for new pattern"); + } + + @Test + @Tag("valid") + @DisplayName("Scenario 2: Overwriting an Existing Factory for an Existing Pattern") + public void testOverwriteExistingPatternFactory() { + String pattern = "overwritable-pattern"; + LlmRegistry.LlmFactory firstFactory = Mockito.mock(LlmRegistry.LlmFactory.class); + LlmRegistry.LlmFactory secondFactory = Mockito.mock(LlmRegistry.LlmFactory.class); + LlmRegistry.registerLlm(pattern, firstFactory); + LlmRegistry.registerLlm(pattern, secondFactory); + Assertions.assertEquals( + secondFactory, getLlmFactories().get(pattern), "Factory should be overwritten with latest"); + } + + @Test + @Tag("invalid") + @DisplayName("Scenario 3: Registering a Null Pattern") + public void testRegisterNullPattern() { + LlmRegistry.LlmFactory factory = Mockito.mock(LlmRegistry.LlmFactory.class); + Assertions.assertThrows( + NullPointerException.class, + () -> { + LlmRegistry.registerLlm(null, factory); + }, + "ConcurrentHashMap should throw NullPointerException for null key"); + } + + @Test + @Tag("invalid") + @DisplayName("Scenario 4: Registering a Null Factory") + public void testRegisterNullFactory() { + String pattern = "null-factory-pattern"; + LlmRegistry.registerLlm(pattern, null); + Assertions.assertNull( + getLlmFactories().get(pattern), "Factory should be null for valid pattern"); + } + + @Test + @Tag("integration") + @DisplayName("Scenario 5: Registering Duplicate Patterns Concurrently") + public void testRegisterPatternConcurrently() throws InterruptedException { + String pattern = "concurrent-pattern"; + LlmRegistry.LlmFactory factory1 = Mockito.mock(LlmRegistry.LlmFactory.class); + LlmRegistry.LlmFactory factory2 = Mockito.mock(LlmRegistry.LlmFactory.class); + LlmRegistry.LlmFactory factory3 = Mockito.mock(LlmRegistry.LlmFactory.class); + Thread t1 = new Thread(() -> LlmRegistry.registerLlm(pattern, factory1)); + Thread t2 = new Thread(() -> LlmRegistry.registerLlm(pattern, factory2)); + Thread t3 = new Thread(() -> LlmRegistry.registerLlm(pattern, factory3)); + t1.start(); + t2.start(); + t3.start(); + t1.join(); + t2.join(); + t3.join(); + // One of the factories (last write wins) should be associated + LlmRegistry.LlmFactory actualFactory = getLlmFactories().get(pattern); + Assertions.assertTrue( + actualFactory == factory1 || actualFactory == factory2 || actualFactory == factory3, + "One of the concurrently registered factories should be mapped"); + } + + @Test + @Tag("boundary") + @DisplayName("Scenario 6: Registering a Pattern that Matches Built-in Factories") + public void testRegisterConflictingBuiltinPattern() { + String builtinPattern = "gemini-.*"; // TODO: Adjust if builtin static initializer + // uses a different pattern + LlmRegistry.LlmFactory customFactory = Mockito.mock(LlmRegistry.LlmFactory.class); + LlmRegistry.registerLlm(builtinPattern, customFactory); + Assertions.assertEquals( + customFactory, + getLlmFactories().get(builtinPattern), + "Custom factory should override builtin factory"); + } + + @Test + @Tag("boundary") + @DisplayName("Scenario 7: Registering an Empty String as Pattern") + public void testRegisterEmptyStringPattern() { + String emptyPattern = ""; + LlmRegistry.LlmFactory emptyFactory = Mockito.mock(LlmRegistry.LlmFactory.class); + LlmRegistry.registerLlm(emptyPattern, emptyFactory); + Assertions.assertEquals( + emptyFactory, + getLlmFactories().get(emptyPattern), + "Empty pattern should be allowed and mapped"); + } + + @Test + @Tag("valid") + @DisplayName("Scenario 8: Registering Multiple Distinct Patterns") + public void testRegisterMultipleDistinctPatterns() { + LlmRegistry.LlmFactory factoryOne = Mockito.mock(LlmRegistry.LlmFactory.class); + LlmRegistry.LlmFactory factoryTwo = Mockito.mock(LlmRegistry.LlmFactory.class); + LlmRegistry.LlmFactory factoryThree = Mockito.mock(LlmRegistry.LlmFactory.class); + String patternOne = "unique-pattern-1"; + String patternTwo = "unique-pattern-2"; + String patternThree = "unique-pattern-3"; + LlmRegistry.registerLlm(patternOne, factoryOne); + LlmRegistry.registerLlm(patternTwo, factoryTwo); + LlmRegistry.registerLlm(patternThree, factoryThree); + Assertions.assertEquals( + factoryOne, + getLlmFactories().get(patternOne), + "First unique pattern should match its factory"); + Assertions.assertEquals( + factoryTwo, + getLlmFactories().get(patternTwo), + "Second unique pattern should match its factory"); + Assertions.assertEquals( + factoryThree, + getLlmFactories().get(patternThree), + "Third unique pattern should match its factory"); + } + + @Test + @Tag("boundary") + @DisplayName("Scenario 9: Registering a Pattern with Special Regex Characters") + public void testRegisterRegexSpecialCharsPattern() { + String specialPattern = "^apigee-\\d{3}$"; + LlmRegistry.LlmFactory regexFactory = Mockito.mock(LlmRegistry.LlmFactory.class); + LlmRegistry.registerLlm(specialPattern, regexFactory); + Assertions.assertEquals( + regexFactory, + getLlmFactories().get(specialPattern), + "Special regex pattern should be mapped correctly"); + } + + @Test + @Tag("boundary") + @DisplayName("Scenario 10: Registering a Factory with a Pattern Already Having Null Value") + public void testRegisterFactoryWithPatternHavingNullValue() { + String nullPattern = "null-pattern"; + // Register with null factory first + LlmRegistry.registerLlm(nullPattern, null); + Assertions.assertNull(getLlmFactories().get(nullPattern), "Should initially be null"); + // Register again with a valid factory + LlmRegistry.LlmFactory recoveryFactory = Mockito.mock(LlmRegistry.LlmFactory.class); + LlmRegistry.registerLlm(nullPattern, recoveryFactory); + Assertions.assertEquals( + recoveryFactory, + getLlmFactories().get(nullPattern), + "Null should be replaced with valid factory"); + } +} diff --git a/pom.xml b/pom.xml index 6009c7316..24fd6ece6 100644 --- a/pom.xml +++ b/pom.xml @@ -1,4 +1,4 @@ - + - + 4.0.0 - com.google.adk google-adk-parent - 0.4.1-SNAPSHOT + 0.4.1-SNAPSHOT + pom - Google Agent Development Kit Maven Parent POM https://github.com/google/adk-java Google Agent Development Kit (ADK) for Java - core dev @@ -39,12 +35,10 @@ a2a a2a/webservice - 17 ${java.version} UTF-8 - 1.11.0 3.4.1 1.49.0 @@ -73,7 +67,6 @@ 3.9.0 5.4.3 - @@ -112,7 +105,6 @@ pom import - com.anthropic @@ -274,9 +266,21 @@ assertj-core ${assertj.version} + + org.mockito + mockito-junit-jupiter + 2.23.4 + test + + + + io.spring.javaformat + spring-javaformat-formatter + 0.0.40 + + - @@ -324,8 +328,7 @@ plain - + **/*Test.java @@ -469,6 +472,36 @@ + + org.apache.maven.plugins + maven-surefire-plugin + 3.2.5 + + + + org.apache.maven.plugins + maven-surefire-report-plugin + 3.2.5 + + testReport + + + + + org.apache.maven.plugins + maven-site-plugin + 2.1 + + testReport + + + + + io.spring.javaformat + spring-javaformat-maven-plugin + 0.0.40 + + @@ -528,7 +561,6 @@ - The Apache License, Version 2.0 @@ -558,4 +590,19 @@ https://central.sonatype.com/repository/maven-snapshots/ + + + org.mockito + mockito-junit-jupiter + 2.23.4 + test + + + + io.spring.javaformat + spring-javaformat-formatter + 0.0.40 + + + \ No newline at end of file