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/LlmRegistryRegisterTestLlmTest.java b/core/src/test/java/com/google/adk/models/LlmRegistryRegisterTestLlmTest.java new file mode 100644 index 000000000..d92b7493d --- /dev/null +++ b/core/src/test/java/com/google/adk/models/LlmRegistryRegisterTestLlmTest.java @@ -0,0 +1,946 @@ +/* + * 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 unit-java-adk using AI Type Azure Open AI and AI Model gpt-5 + +ROOST_METHOD_HASH=registerTestLlm_14ec6c33f6 +ROOST_METHOD_SIG_HASH=registerTestLlm_b0e327d7c8 + +Scenario 1: Registers a new non-overlapping pattern and uses it for future lookups + +Details: + TestName: registersNewPatternAndCreatesInstancesViaNewFactory + Description: Verifies that when registerTestLlm is called with a new regex pattern that does not overlap with existing defaults, the factory is stored and subsequent getLlm calls for matching model names create and return instances from that factory. + +Execution: + Arrange: Prepare a test LlmFactory that returns a distinct, easily identifiable BaseLlm test double. Ensure no prior instances exist for the chosen model name (e.g., "custom-foo"). + Act: Invoke LlmRegistry.registerTestLlm("custom-.*", testFactory) and then call LlmRegistry.getLlm("custom-foo"). + Assert: Use JUnit assertions to verify that the returned BaseLlm is the same instance produced by testFactory for "custom-foo" (e.g., assertSame). + +Validation: + Confirms that registerTestLlm inserts the factory into the registry and that getLlm routes creation through the newly registered factory for matching names. This ensures extensibility for custom model name families during tests. + + +Scenario 2: Re-registering an existing pattern replaces the factory and clears matching cached instances + +Details: + TestName: reRegisterPatternReplacesFactoryAndInvalidatesCache + Description: Ensures that calling registerTestLlm with a pattern already present replaces the factory and that any cached instances whose names match the pattern are removed. + +Execution: + Arrange: + - Register a pattern "custom-.*" with factoryA and call getLlm("custom-1") to populate the cache with an instance from factoryA. + - Prepare factoryB that returns a different identifiable BaseLlm. + Act: Invoke LlmRegistry.registerTestLlm("custom-.*", factoryB) and then call getLlm("custom-1") again. + Assert: + - Verify the new returned instance is not the same as the previously cached one (assertNotSame). + - Verify it is the instance produced by factoryB for "custom-1" (assertSame with factoryB’s product). + +Validation: + Confirms that the method’s removeIf logic clears cached matches to ensure test isolation, and that the factory mapping is updated atomically to the new factory. + + +Scenario 3: Non-matching cached entries remain unaffected + +Details: + TestName: nonMatchingCacheEntriesArePreserved + Description: Verifies that registerTestLlm only removes cached instances whose model names match the given pattern and leaves others intact. + +Execution: + Arrange: + - Pre-populate the cache by calling getLlm("gemini-pro") (uses default gemini factory) and getLlm("apigee/service-1") (uses default apigee factory). + Act: Invoke LlmRegistry.registerTestLlm("custom-.*", someTestFactory). + Assert: + - Call getLlm("gemini-pro") again and assert it returns the same instance as before (assertSame). + - Call getLlm("apigee/service-1") again and assert it returns the same instance as before. + +Validation: + Confirms selective cache invalidation and that unrelated cached entries are not disturbed by registering a new, non-overlapping pattern. + + +Scenario 4: Pattern matches multiple cached entries and removes them all + +Details: + TestName: clearsAllCachedInstancesThatMatchPattern + Description: Ensures that when the pattern matches multiple cached model names, all those entries are removed. + +Execution: + Arrange: + - Register test factoryA for "batch-.*" and call getLlm for "batch-a", "batch-b", and "batch-c" to populate the cache with factoryA instances. + - Register a different factoryB to be used after cache clearing. + Act: Invoke LlmRegistry.registerTestLlm("batch-.*", factoryB). Then call getLlm("batch-a"), "batch-b", "batch-c" again. + Assert: + - For each model name, assert the returned instance is not the previously cached object and matches factoryB’s created objects (assertNotSame previous; assertSame to new). + Validation: + Confirms that removeIf removes every matching key and that subsequent getLlm calls use the newly registered factory for those names. + + +Scenario 5: Using a catch-all regex clears the entire cache + +Details: + TestName: catchAllPatternClearsAllCachedInstances + Description: Verifies that a broad pattern like ".*" removes every cached instance to guarantee a clean slate. + +Execution: + Arrange: + - Populate the cache with several diverse model names (e.g., "gemini-pro", "apigee/service-x", "misc-123") by calling getLlm. + - Prepare a test factory to re-create instances after clearing. + Act: Invoke LlmRegistry.registerTestLlm(".*", testFactory). + Assert: + - Subsequent getLlm calls for any previously cached model name should return newly created instances from testFactory (assertNotSame vs prior; assertSame to new). + Validation: + Ensures that the method supports fully resetting the cache, which is valuable for test isolation across suites. + + +Scenario 6: Empty pattern behaves as a strict empty-string match and does not remove typical entries + +Details: + TestName: emptyPatternDoesNotAffectNonEmptyModelNames + Description: Confirms that the empty regex "" only matches an empty model name and does not remove or affect normal cached entries. + +Execution: + Arrange: + - Pre-populate cache with a normal model name like "custom-1". + - Create a factory for the empty-string pattern. + Act: Invoke LlmRegistry.registerTestLlm("", emptyPatternFactory). + Assert: + - Verify that calling getLlm("custom-1") returns the same cached instance (assertSame). + - Optionally, verify that getLlm("") returns an instance created by emptyPatternFactory (assertSame). + Validation: + Demonstrates that registerTestLlm uses Java regex semantics via String.matches, and "" only matches empty inputs, leaving typical names unaffected. + + +Scenario 7: Null pattern results in a NullPointerException due to ConcurrentHashMap restrictions + +Details: + TestName: nullPatternThrowsNullPointerException + Description: Ensures that passing a null modelNamePattern causes a NullPointerException during the llmFactories.put operation. + +Execution: + Arrange: Have a valid non-null factory ready. + Act: Call LlmRegistry.registerTestLlm(null, validFactory) and expect an exception. + Assert: Use assertThrows to check for NullPointerException. + Validation: + Confirms defensive behavior of ConcurrentHashMap and aligns with the method’s lack of null checks. The test protects against accidental null keys. + + +Scenario 8: Null factory results in a NullPointerException due to ConcurrentHashMap restrictions + +Details: + TestName: nullFactoryThrowsNullPointerException + Description: Ensures that passing a null LlmFactory causes a NullPointerException when inserting into the ConcurrentHashMap. + +Execution: + Arrange: Choose any valid non-null pattern. + Act: Call LlmRegistry.registerTestLlm("custom-.*", null) and expect an exception. + Assert: Use assertThrows to check for NullPointerException. + Validation: + Confirms that null values are disallowed and the method behaves accordingly by bubbling up the NPE. + + +Scenario 9: Invalid regex pattern triggers a PatternSyntaxException during removal + +Details: + TestName: invalidRegexPatternThrowsPatternSyntaxException + Description: Verifies that if an invalid regex (e.g., "(") is provided, the removal predicate throws PatternSyntaxException when evaluating matches. + +Execution: + Arrange: + - Pre-populate the cache with one or more model names (e.g., "gemini-pro") so that removeIf attempts evaluation. + Act: Invoke LlmRegistry.registerTestLlm("(", someFactory) and expect an exception. + Assert: Use assertThrows to verify PatternSyntaxException is thrown. + Validation: + Confirms that the method does not suppress regex compilation errors and surfaces configuration mistakes early, even though the factory mapping is attempted first. + + +Scenario 10: The factory is not invoked during registration, only upon later getLlm + +Details: + TestName: factoryNotInvokedDuringRegistration + Description: Ensures that registerTestLlm only stores the factory and clears the cache, but does not call LlmFactory.create until getLlm is invoked. + +Execution: + Arrange: + - Prepare a spy or counter-enabled LlmFactory that increments a counter each time create is called. + Act: + - Call LlmRegistry.registerTestLlm("custom-.*", countingFactory) without calling getLlm. + Assert: + - Assert that the factory invocation counter remains zero. + - After calling getLlm("custom-1"), assert that the counter increments to 1. + Validation: + Confirms lazy instantiation behavior: registration is side-effect free regarding object creation and creation occurs only on demand. + + +Scenario 11: Case sensitivity in regex matching prevents unintended removals + +Details: + TestName: regexIsCaseSensitiveForRemoval + Description: Validates that the removal predicate is case-sensitive (per String.matches) and does not clear entries with different case. + +Execution: + Arrange: + - Cache an instance under "gemini-pro" via getLlm. + Act: + - Call LlmRegistry.registerTestLlm("GEMINI-.*", someFactory). + Assert: + - Call getLlm("gemini-pro") again and assert it is the same cached instance as before (assertSame), indicating it was not removed. + Validation: + Ensures predictable behavior regarding case sensitivity in regex processing. + + +Scenario 12: Overriding the default gemini pattern clears its cached instances + +Details: + TestName: overridingDefaultGeminiClearsCachedGemini + Description: Ensures that when the default "gemini-.*" pattern is overridden for testing, any previously cached gemini instances are cleared. + +Execution: + Arrange: + - Call getLlm("gemini-pro") to create and cache a default gemini instance. + - Prepare a test factory that creates a distinct test double for gemini names. + Act: + - Call LlmRegistry.registerTestLlm("gemini-.*", testFactory), then call getLlm("gemini-pro"). + Assert: + - Verify the new instance differs from the previous cached one and matches the instance from testFactory (assertNotSame previous; assertSame to new). + Validation: + Confirms that registerTestLlm ensures test isolation by invalidating default-cached instances for overridden patterns. + + +Scenario 13: Concurrent registration and cache population does not cause concurrent modification errors + +Details: + TestName: concurrentRegistrationDoesNotThrowAndEndsInConsistentState + Description: Verifies thread-safety under concurrency by invoking getLlm for matching names while calling registerTestLlm, ensuring no ConcurrentModificationException and eventual consistency. + +Execution: + Arrange: + - Spawn one thread repeatedly calling getLlm("custom-1") using an initial factory registered for "custom-.*". + - Prepare a second factory to replace the first. + - Coordinate action with latches to overlap operations. + Act: + - In parallel, call LlmRegistry.registerTestLlm("custom-.*", secondFactory). + Assert: + - Assert no exceptions are thrown from either thread. + - After registration completes, verify a subsequent getLlm("custom-1") returns an instance created by secondFactory. + Validation: + Confirms that the use of ConcurrentHashMap and keySet().removeIf is safe under concurrent access and results in the expected final behavior. + + +Scenario 14: Latest registration wins for the same pattern + +Details: + TestName: lastRegisteredFactoryForPatternIsUsed + Description: Ensures that when the same regex pattern is registered multiple times sequentially, the last factory overwrites earlier ones for subsequent getLlm calls. + +Execution: + Arrange: + - Register "custom-.*" with factoryA. + - Register "custom-.*" again with factoryB. + Act: + - Call getLlm("custom-xyz"). + Assert: + - Verify the returned instance is the one created by factoryB (assertSame), not by factoryA. + Validation: + Confirms deterministic overwrite behavior in the factory map for duplicate keys. + + +Scenario 15: Overriding apigee pattern does not affect gemini cached entries + +Details: + TestName: overridingApigeeDoesNotAffectGeminiCache + Description: Ensures that when "apigee/.*" is overridden, only apigee entries are cleared and gemini entries remain cached. + +Execution: + Arrange: + - Cache instances for "apigee/service-1" and "gemini-pro" using getLlm. + - Prepare a test factory for "apigee/.*". + Act: + - Call LlmRegistry.registerTestLlm("apigee/.*", testFactory). + Assert: + - getLlm("apigee/service-1") returns a new instance from testFactory (assertNotSame previous; assertSame to new). + - getLlm("gemini-pro") returns the same instance as before (assertSame). + Validation: + Confirms pattern-specific cache invalidation without collateral impact on other families. + + +Scenario 16: Exact match pattern only removes the exact model name, not similar prefixes + +Details: + TestName: exactPatternRemovesOnlyExactModelName + Description: Verifies that a literal pattern like "custom-1" removes only "custom-1" and not "custom-10" or "custom-11", because String.matches requires a full-string match. + +Execution: + Arrange: + - Register a factory for "custom-.*" and cache "custom-1" and "custom-10" via getLlm. + - Prepare a replacement factory. + Act: + - Call LlmRegistry.registerTestLlm("custom-1", replacementFactory). + Assert: + - getLlm("custom-1") returns an instance from replacementFactory (new). + - getLlm("custom-10") returns the same previously cached instance (unchanged). + Validation: + Demonstrates correct full-string regex matching semantics in removal behavior. + + +Scenario 17: Bulk cache clearing for many matching keys + +Details: + TestName: clearsLargeNumberOfMatchingCachedEntriesEfficiently + Description: Ensures correctness when many keys need removal by verifying all matching entries are cleared and recreated via the new factory. + +Execution: + Arrange: + - Register factoryA for "bulk-.*" and cache a large set of names: "bulk-1" through "bulk-100" using getLlm. + - Prepare factoryB to replace factoryA. + Act: + - Call LlmRegistry.registerTestLlm("bulk-.*", factoryB). Then call getLlm on several of the bulk keys (e.g., first, middle, last). + Assert: + - For sampled keys, assert new instances are returned from factoryB and are different from previously cached ones (assertNotSame previous; assertSame to new). + Validation: + Confirms correctness at scale and that removal impacts all matching keys. + + +Scenario 18: Pattern with escaped special characters works as a regex + +Details: + TestName: escapedCharactersInPatternAreHandledCorrectly + Description: Validates that regex metacharacters must be escaped to match literal characters and that removal respects these escapes. + +Execution: + Arrange: + - Register a factory for "custom\\+model-.*" and cache "custom+model-1" via getLlm (ensure an earlier factory supports this name). + - Prepare a replacement factory. + Act: + - Call LlmRegistry.registerTestLlm("custom\\+model-.*", replacementFactory) and then getLlm("custom+model-1"). + Assert: + - Assert that the cached instance for "custom+model-1" was cleared and a new one from replacementFactory is returned (assertNotSame previous; assertSame to new). + Validation: + Confirms adherence to Java regex syntax in matching and cache invalidation. + + +Scenario 19: Registering a pattern that matches no current entries does not change existing cache + +Details: + TestName: nonMatchingPatternDoesNotClearExistingCache + Description: Ensures that when no cached keys match the provided pattern, the cache remains unchanged while the factory is still registered for future use. + +Execution: + Arrange: + - Cache a model like "gemini-pro" via getLlm. + Act: + - Call LlmRegistry.registerTestLlm("custom-.*", someFactory) where no "custom-.*" items are cached. + Assert: + - getLlm("gemini-pro") returns the same instance as before (assertSame). + - getLlm("custom-1") afterward returns an instance produced by someFactory (assertSame to new), demonstrating the factory was registered. + Validation: + Confirms that cache clearing is conditional on matches and that future matching lookups leverage the new factory. + + +Scenario 20: After overriding pattern, subsequent creations use the new factory even for names created concurrently before registration completes + +Details: + TestName: postOverrideLookupsUseNewFactoryDespiteConcurrentActivity + Description: Ensures that once registerTestLlm completes, any subsequent calls to getLlm for matching names return instances created by the newly registered factory, regardless of concurrent pre-registration cache state. + +Execution: + Arrange: + - Start a thread that calls getLlm("override-1") using an initially registered factory (e.g., default or prior test factory) and pauses. + - Prepare a new factory to override "override-.*". + Act: + - Call LlmRegistry.registerTestLlm("override-.*", newFactory) and then, after ensuring it completes, call getLlm("override-1"). + Assert: + - Assert that getLlm("override-1") returns an instance from newFactory (assertSame to new and assertNotSame to any earlier instance, if present). + Validation: + Confirms the final consistency guarantee: after registration, future lookups use the latest factory and any matching cached entries have been purged. + + +*/ + +// ********RoostGPT******** + +package com.google.adk.models; + +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import com.google.adk.models.LlmRegistry.LlmFactory; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; +import java.util.regex.PatternSyntaxException; +import org.junit.jupiter.api.*; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.MethodOrderer.OrderAnnotation; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.mockito.Mockito; + +@TestMethodOrder(OrderAnnotation.class) +public class LlmRegistryRegisterTestLlmTest { + + private static LlmFactory factoryFromMap(Map mapping) { + return modelName -> mapping.get(modelName); + } + + private static class CountingFactory implements LlmFactory { + + private final AtomicInteger counter = new AtomicInteger(0); + + private final Map mapping; + + CountingFactory(Map mapping) { + this.mapping = mapping; + } + + @Override + public BaseLlm create(String modelName) { + counter.incrementAndGet(); + return mapping.get(modelName); + } + + int getCount() { + return counter.get(); + } + } + + @Test + @Order(1) + @Tag("valid") + @DisplayName("Scenario 1: Registers a new non-overlapping pattern and uses it for future lookups") + public void testRegistersNewPatternAndCreatesInstancesViaNewFactory() { + Map map = new ConcurrentHashMap<>(); + BaseLlm customFoo = Mockito.mock(BaseLlm.class); + map.put("custom-foo", customFoo); + LlmFactory testFactory = factoryFromMap(map); + LlmRegistry.registerTestLlm("custom-.*", testFactory); + BaseLlm actual = LlmRegistry.getLlm("custom-foo"); + assertSame((BaseLlm) customFoo, (BaseLlm) actual); + } + + @Test + @Order(2) + @Tag("valid") + @DisplayName( + "Scenario 2: Re-registering an existing pattern replaces the factory and clears matching cached instances") + public void testReRegisterPatternReplacesFactoryAndInvalidatesCache() { + Map aMap = new ConcurrentHashMap<>(); + BaseLlm a1 = Mockito.mock(BaseLlm.class); + aMap.put("custom-1", a1); + LlmFactory factoryA = factoryFromMap(aMap); + LlmRegistry.registerTestLlm("custom-.*", factoryA); + BaseLlm first = LlmRegistry.getLlm("custom-1"); + Map bMap = new ConcurrentHashMap<>(); + BaseLlm b1 = Mockito.mock(BaseLlm.class); + bMap.put("custom-1", b1); + LlmFactory factoryB = factoryFromMap(bMap); + LlmRegistry.registerTestLlm("custom-.*", factoryB); + BaseLlm second = LlmRegistry.getLlm("custom-1"); + assertNotSame((BaseLlm) first, (BaseLlm) second); + assertSame((BaseLlm) b1, (BaseLlm) second); + } + + @Test + @Order(3) + @Tag("valid") + @DisplayName("Scenario 3: Non-matching cached entries remain unaffected") + public void testNonMatchingCacheEntriesArePreserved() { + // gemini factory and instance + Map geminiMap = new ConcurrentHashMap<>(); + BaseLlm geminiInst = Mockito.mock(BaseLlm.class); + geminiMap.put("gemini-pro", geminiInst); + LlmFactory geminiFactory = factoryFromMap(geminiMap); + LlmRegistry.registerTestLlm("gemini-.*", geminiFactory); + BaseLlm cachedGemini = LlmRegistry.getLlm("gemini-pro"); + // apigee factory and instance + Map apigeeMap = new ConcurrentHashMap<>(); + BaseLlm apigeeInst = Mockito.mock(BaseLlm.class); + apigeeMap.put("apigee/service-1", apigeeInst); + LlmFactory apigeeFactory = factoryFromMap(apigeeMap); + LlmRegistry.registerTestLlm("apigee/.*", apigeeFactory); + BaseLlm cachedApigee = LlmRegistry.getLlm("apigee/service-1"); + // Register non-overlapping pattern + Map customMap = new ConcurrentHashMap<>(); + BaseLlm customInst = Mockito.mock(BaseLlm.class); + customMap.put("custom-foo", customInst); + LlmFactory customFactory = factoryFromMap(customMap); + LlmRegistry.registerTestLlm("custom-.*", customFactory); + BaseLlm geminiAfter = LlmRegistry.getLlm("gemini-pro"); + BaseLlm apigeeAfter = LlmRegistry.getLlm("apigee/service-1"); + assertSame((BaseLlm) cachedGemini, (BaseLlm) geminiAfter); + assertSame((BaseLlm) cachedApigee, (BaseLlm) apigeeAfter); + } + + @Test + @Order(4) + @Tag("valid") + @DisplayName("Scenario 4: Pattern matches multiple cached entries and removes them all") + public void testClearsAllCachedInstancesThatMatchPattern() { + Map aMap = new ConcurrentHashMap<>(); + BaseLlm aA = Mockito.mock(BaseLlm.class); + BaseLlm aB = Mockito.mock(BaseLlm.class); + BaseLlm aC = Mockito.mock(BaseLlm.class); + aMap.put("batch-a", aA); + aMap.put("batch-b", aB); + aMap.put("batch-c", aC); + LlmFactory factoryA = factoryFromMap(aMap); + LlmRegistry.registerTestLlm("batch-.*", factoryA); + BaseLlm prevA = LlmRegistry.getLlm("batch-a"); + BaseLlm prevB = LlmRegistry.getLlm("batch-b"); + BaseLlm prevC = LlmRegistry.getLlm("batch-c"); + Map bMap = new ConcurrentHashMap<>(); + BaseLlm bA = Mockito.mock(BaseLlm.class); + BaseLlm bB = Mockito.mock(BaseLlm.class); + BaseLlm bC = Mockito.mock(BaseLlm.class); + bMap.put("batch-a", bA); + bMap.put("batch-b", bB); + bMap.put("batch-c", bC); + LlmFactory factoryB = factoryFromMap(bMap); + LlmRegistry.registerTestLlm("batch-.*", factoryB); + BaseLlm newA = LlmRegistry.getLlm("batch-a"); + BaseLlm newB = LlmRegistry.getLlm("batch-b"); + BaseLlm newC = LlmRegistry.getLlm("batch-c"); + assertNotSame((BaseLlm) prevA, (BaseLlm) newA); + assertNotSame((BaseLlm) prevB, (BaseLlm) newB); + assertNotSame((BaseLlm) prevC, (BaseLlm) newC); + assertSame((BaseLlm) bA, (BaseLlm) newA); + assertSame((BaseLlm) bB, (BaseLlm) newB); + assertSame((BaseLlm) bC, (BaseLlm) newC); + } + + @Test + @Order(5) + @Tag("boundary") + @DisplayName( + "Scenario 6: Empty pattern behaves as a strict empty-string match and does not remove typical entries") + public void testEmptyPatternDoesNotAffectNonEmptyModelNames() { + Map customMap = new ConcurrentHashMap<>(); + BaseLlm c1 = Mockito.mock(BaseLlm.class); + customMap.put("custom-1", c1); + LlmFactory customFactory = factoryFromMap(customMap); + LlmRegistry.registerTestLlm("custom-.*", customFactory); + BaseLlm prev = LlmRegistry.getLlm("custom-1"); + Map emptyMap = new ConcurrentHashMap<>(); + BaseLlm emptyInst = Mockito.mock(BaseLlm.class); + emptyMap.put("", emptyInst); + LlmFactory emptyFactory = factoryFromMap(emptyMap); + LlmRegistry.registerTestLlm("", emptyFactory); + BaseLlm after = LlmRegistry.getLlm("custom-1"); + assertSame((BaseLlm) prev, (BaseLlm) after); + BaseLlm empty = LlmRegistry.getLlm(""); + assertSame((BaseLlm) emptyInst, (BaseLlm) empty); + } + + @Test + @Order(6) + @Tag("boundary") + @DisplayName("Scenario 11: Case sensitivity in regex matching prevents unintended removals") + public void testRegexIsCaseSensitiveForRemoval() { + Map gMap = new ConcurrentHashMap<>(); + BaseLlm g = Mockito.mock(BaseLlm.class); + gMap.put("gemini-pro", g); + LlmFactory gFactory = factoryFromMap(gMap); + LlmRegistry.registerTestLlm("gemini-.*", gFactory); + BaseLlm prev = LlmRegistry.getLlm("gemini-pro"); + Map upperMap = new ConcurrentHashMap<>(); + LlmFactory upperFactory = factoryFromMap(upperMap); + LlmRegistry.registerTestLlm("GEMINI-.*", upperFactory); + BaseLlm after = LlmRegistry.getLlm("gemini-pro"); + assertSame((BaseLlm) prev, (BaseLlm) after); + } + + @Test + @Order(7) + @Tag("valid") + @DisplayName("Scenario 12: Overriding the default gemini pattern clears its cached instances") + public void testOverridingDefaultGeminiClearsCachedGemini() { + Map oldMap = new ConcurrentHashMap<>(); + BaseLlm gOld = Mockito.mock(BaseLlm.class); + oldMap.put("gemini-pro", gOld); + LlmFactory oldFactory = factoryFromMap(oldMap); + LlmRegistry.registerTestLlm("gemini-.*", oldFactory); + BaseLlm prev = LlmRegistry.getLlm("gemini-pro"); + Map newMap = new ConcurrentHashMap<>(); + BaseLlm gNew = Mockito.mock(BaseLlm.class); + newMap.put("gemini-pro", gNew); + LlmFactory newFactory = factoryFromMap(newMap); + LlmRegistry.registerTestLlm("gemini-.*", newFactory); + BaseLlm after = LlmRegistry.getLlm("gemini-pro"); + assertNotSame((BaseLlm) prev, (BaseLlm) after); + assertSame((BaseLlm) gNew, (BaseLlm) after); + } + + @Test + @Order(8) + @Tag("valid") + @DisplayName("Scenario 14: Latest registration wins for the same pattern") + public void testLastRegisteredFactoryForPatternIsUsed() { + Map aMap = new ConcurrentHashMap<>(); + BaseLlm aX = Mockito.mock(BaseLlm.class); + aMap.put("custom-xyz", aX); + LlmFactory factoryA = factoryFromMap(aMap); + LlmRegistry.registerTestLlm("custom-.*", factoryA); + Map bMap = new ConcurrentHashMap<>(); + BaseLlm bX = Mockito.mock(BaseLlm.class); + bMap.put("custom-xyz", bX); + LlmFactory factoryB = factoryFromMap(bMap); + LlmRegistry.registerTestLlm("custom-.*", factoryB); + BaseLlm actual = LlmRegistry.getLlm("custom-xyz"); + assertSame((BaseLlm) bX, (BaseLlm) actual); + } + + @Test + @Order(9) + @Tag("boundary") + @DisplayName( + "Scenario 16: Exact match pattern only removes the exact model name, not similar prefixes") + public void testExactPatternRemovesOnlyExactModelName() { + Map initial = new ConcurrentHashMap<>(); + BaseLlm c1 = Mockito.mock(BaseLlm.class); + BaseLlm c10 = Mockito.mock(BaseLlm.class); + initial.put("custom-1", c1); + initial.put("custom-10", c10); + LlmFactory initialFactory = factoryFromMap(initial); + LlmRegistry.registerTestLlm("custom-.*", initialFactory); + BaseLlm prev1 = LlmRegistry.getLlm("custom-1"); + BaseLlm prev10 = LlmRegistry.getLlm("custom-10"); + Map repl = new ConcurrentHashMap<>(); + BaseLlm new1 = Mockito.mock(BaseLlm.class); + repl.put("custom-1", new1); + LlmFactory replFactory = factoryFromMap(repl); + LlmRegistry.registerTestLlm("custom-1", replFactory); + BaseLlm after1 = LlmRegistry.getLlm("custom-1"); + BaseLlm after10 = LlmRegistry.getLlm("custom-10"); + assertSame((BaseLlm) new1, (BaseLlm) after1); + assertSame((BaseLlm) prev10, (BaseLlm) after10); + } + + @Test + @Order(10) + @Tag("valid") + @DisplayName( + "Scenario 19: Registering a pattern that matches no current entries does not change existing cache") + public void testNonMatchingPatternDoesNotClearExistingCache() { + Map gMap = new ConcurrentHashMap<>(); + BaseLlm g = Mockito.mock(BaseLlm.class); + gMap.put("gemini-pro", g); + LlmFactory gFactory = factoryFromMap(gMap); + LlmRegistry.registerTestLlm("gemini-.*", gFactory); + BaseLlm prevGemini = LlmRegistry.getLlm("gemini-pro"); + Map customMap = new ConcurrentHashMap<>(); + BaseLlm c = Mockito.mock(BaseLlm.class); + customMap.put("custom-1", c); + LlmFactory cFactory = factoryFromMap(customMap); + LlmRegistry.registerTestLlm("custom-.*", cFactory); + BaseLlm geminiAfter = LlmRegistry.getLlm("gemini-pro"); + BaseLlm customAfter = LlmRegistry.getLlm("custom-1"); + assertSame((BaseLlm) prevGemini, (BaseLlm) geminiAfter); + assertSame((BaseLlm) c, (BaseLlm) customAfter); + } + + @Test + @Order(11) + @Tag("boundary") + @DisplayName("Scenario 18: Pattern with escaped special characters works as a regex") + public void testEscapedCharactersInPatternAreHandledCorrectly() { + Map aMap = new ConcurrentHashMap<>(); + BaseLlm old = Mockito.mock(BaseLlm.class); + aMap.put("custom+model-1", old); + LlmFactory aFactory = factoryFromMap(aMap); + LlmRegistry.registerTestLlm("custom\\+model-.*", aFactory); + BaseLlm prev = LlmRegistry.getLlm("custom+model-1"); + Map bMap = new ConcurrentHashMap<>(); + BaseLlm newer = Mockito.mock(BaseLlm.class); + bMap.put("custom+model-1", newer); + LlmFactory bFactory = factoryFromMap(bMap); + LlmRegistry.registerTestLlm("custom\\+model-.*", bFactory); + BaseLlm after = LlmRegistry.getLlm("custom+model-1"); + assertNotSame((BaseLlm) prev, (BaseLlm) after); + assertSame((BaseLlm) newer, (BaseLlm) after); + } + + @Test + @Order(12) + @Tag("integration") + @DisplayName("Scenario 17: Bulk cache clearing for many matching keys") + public void testClearsLargeNumberOfMatchingCachedEntriesEfficiently() { + Map aMap = new ConcurrentHashMap<>(); + for (int i = 1; i <= 100; i++) { + aMap.put("bulk-" + i, Mockito.mock(BaseLlm.class)); + } + LlmFactory aFactory = factoryFromMap(aMap); + LlmRegistry.registerTestLlm("bulk-.*", aFactory); + BaseLlm prev1 = LlmRegistry.getLlm("bulk-1"); + BaseLlm prev50 = LlmRegistry.getLlm("bulk-50"); + BaseLlm prev100 = LlmRegistry.getLlm("bulk-100"); + Map bMap = new ConcurrentHashMap<>(); + for (int i = 1; i <= 100; i++) { + bMap.put("bulk-" + i, Mockito.mock(BaseLlm.class)); + } + LlmFactory bFactory = factoryFromMap(bMap); + LlmRegistry.registerTestLlm("bulk-.*", bFactory); + BaseLlm after1 = LlmRegistry.getLlm("bulk-1"); + BaseLlm after50 = LlmRegistry.getLlm("bulk-50"); + BaseLlm after100 = LlmRegistry.getLlm("bulk-100"); + assertNotSame((BaseLlm) prev1, (BaseLlm) after1); + assertNotSame((BaseLlm) prev50, (BaseLlm) after50); + assertNotSame((BaseLlm) prev100, (BaseLlm) after100); + assertSame((BaseLlm) bMap.get("bulk-1"), (BaseLlm) after1); + assertSame((BaseLlm) bMap.get("bulk-50"), (BaseLlm) after50); + assertSame((BaseLlm) bMap.get("bulk-100"), (BaseLlm) after100); + } + + @Test + @Order(13) + @Tag("integration") + @DisplayName( + "Scenario 13: Concurrent registration and cache population does not cause concurrent modification errors") + public void testConcurrentRegistrationDoesNotThrowAndEndsInConsistentState() + throws InterruptedException { + Map aMap = new ConcurrentHashMap<>(); + BaseLlm aInst = Mockito.mock(BaseLlm.class); + aMap.put("custom-1", aInst); + LlmFactory aFactory = factoryFromMap(aMap); + LlmRegistry.registerTestLlm("custom-.*", aFactory); + BaseLlm initial = LlmRegistry.getLlm("custom-1"); + Map bMap = new ConcurrentHashMap<>(); + BaseLlm bInst = Mockito.mock(BaseLlm.class); + bMap.put("custom-1", bInst); + LlmFactory bFactory = factoryFromMap(bMap); + CountDownLatch startLatch = new CountDownLatch(1); + CountDownLatch doneLatch = new CountDownLatch(2); + AtomicReference errorRef = new AtomicReference<>(null); + ExecutorService pool = Executors.newFixedThreadPool(2); + pool.submit( + () -> { + try { + startLatch.await(5, TimeUnit.SECONDS); + for (int i = 0; i < 1000; i++) { + BaseLlm x = LlmRegistry.getLlm("custom-1"); + if (x == null) { + throw new AssertionError("Instance should not be null"); + } + } + } catch (Throwable t) { + errorRef.compareAndSet(null, t); + } finally { + doneLatch.countDown(); + } + }); + pool.submit( + () -> { + try { + startLatch.await(5, TimeUnit.SECONDS); + LlmRegistry.registerTestLlm("custom-.*", bFactory); + } catch (Throwable t) { + errorRef.compareAndSet(null, t); + } finally { + doneLatch.countDown(); + } + }); + startLatch.countDown(); + doneLatch.await(10, TimeUnit.SECONDS); + pool.shutdownNow(); + if (errorRef.get() != null) { + throw new AssertionError("Unexpected error in concurrent operations", errorRef.get()); + } + BaseLlm finalInst = LlmRegistry.getLlm("custom-1"); + assertNotSame((BaseLlm) initial, (BaseLlm) finalInst); + assertSame((BaseLlm) bInst, (BaseLlm) finalInst); + } + + @Test + @Order(14) + @Tag("integration") + @DisplayName( + "Scenario 20: After overriding pattern, subsequent creations use the new factory even for names created concurrently before registration completes") + public void testPostOverrideLookupsUseNewFactoryDespiteConcurrentActivity() + throws InterruptedException { + Map oldMap = new ConcurrentHashMap<>(); + BaseLlm oldInst = Mockito.mock(BaseLlm.class); + oldMap.put("override-1", oldInst); + LlmFactory oldFactory = factoryFromMap(oldMap); + LlmRegistry.registerTestLlm("override-.*", oldFactory); + CountDownLatch ready = new CountDownLatch(1); + CountDownLatch proceed = new CountDownLatch(1); + AtomicReference previousRef = new AtomicReference<>(); + ExecutorService pool = Executors.newSingleThreadExecutor(); + pool.submit( + () -> { + try { + ready.countDown(); + proceed.await(5, TimeUnit.SECONDS); + BaseLlm prev = LlmRegistry.getLlm("override-1"); + previousRef.set(prev); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + }); + ready.await(5, TimeUnit.SECONDS); + proceed.countDown(); + // Register new factory + Map newMap = new ConcurrentHashMap<>(); + BaseLlm newInst = Mockito.mock(BaseLlm.class); + newMap.put("override-1", newInst); + LlmFactory newFactory = factoryFromMap(newMap); + LlmRegistry.registerTestLlm("override-.*", newFactory); + pool.shutdown(); + pool.awaitTermination(5, TimeUnit.SECONDS); + BaseLlm prev = previousRef.get(); + BaseLlm after = LlmRegistry.getLlm("override-1"); + assertNotSame((BaseLlm) prev, (BaseLlm) after); + assertSame((BaseLlm) newInst, (BaseLlm) after); + } + + @Test + @Order(15) + @Tag("valid") + @DisplayName("Scenario 15: Overriding apigee pattern does not affect gemini cached entries") + public void testOverridingApigeeDoesNotAffectGeminiCache() { + Map apigeeOldMap = new ConcurrentHashMap<>(); + BaseLlm apigeeOld = Mockito.mock(BaseLlm.class); + apigeeOldMap.put("apigee/service-1", apigeeOld); + LlmFactory apigeeOldFactory = factoryFromMap(apigeeOldMap); + LlmRegistry.registerTestLlm("apigee/.*", apigeeOldFactory); + BaseLlm prevApigee = LlmRegistry.getLlm("apigee/service-1"); + Map geminiMap = new ConcurrentHashMap<>(); + BaseLlm geminiInst = Mockito.mock(BaseLlm.class); + geminiMap.put("gemini-pro", geminiInst); + LlmFactory geminiFactory = factoryFromMap(geminiMap); + LlmRegistry.registerTestLlm("gemini-.*", geminiFactory); + BaseLlm prevGemini = LlmRegistry.getLlm("gemini-pro"); + Map apigeeNewMap = new ConcurrentHashMap<>(); + BaseLlm apigeeNew = Mockito.mock(BaseLlm.class); + apigeeNewMap.put("apigee/service-1", apigeeNew); + LlmFactory apigeeNewFactory = factoryFromMap(apigeeNewMap); + LlmRegistry.registerTestLlm("apigee/.*", apigeeNewFactory); + BaseLlm afterApigee = LlmRegistry.getLlm("apigee/service-1"); + BaseLlm afterGemini = LlmRegistry.getLlm("gemini-pro"); + assertNotSame((BaseLlm) prevApigee, (BaseLlm) afterApigee); + assertSame((BaseLlm) apigeeNew, (BaseLlm) afterApigee); + assertSame((BaseLlm) prevGemini, (BaseLlm) afterGemini); + } + + @Test + @Order(16) + @Tag("valid") + @DisplayName( + "Scenario 10: The factory is not invoked during registration, only upon later getLlm") + public void testFactoryNotInvokedDuringRegistration() { + Map map = new ConcurrentHashMap<>(); + BaseLlm inst = Mockito.mock(BaseLlm.class); + map.put("custom-1", inst); + CountingFactory countingFactory = new CountingFactory(map); + LlmRegistry.registerTestLlm("custom-.*", countingFactory); + // ensure not invoked during registration + assertSame((int) 0, (int) countingFactory.getCount()); + BaseLlm after = LlmRegistry.getLlm("custom-1"); + assertSame((int) 1, (int) countingFactory.getCount()); + assertSame((BaseLlm) inst, (BaseLlm) after); + } + + @Test + @Order(17) + @Tag("invalid") + @DisplayName( + "Scenario 7: Null pattern results in a NullPointerException due to ConcurrentHashMap restrictions") + public void testNullPatternThrowsNullPointerException() { + Map map = new ConcurrentHashMap<>(); + BaseLlm inst = Mockito.mock(BaseLlm.class); + map.put("x", inst); + LlmFactory validFactory = factoryFromMap(map); + assertThrows(NullPointerException.class, () -> LlmRegistry.registerTestLlm(null, validFactory)); + } + + @Test + @Order(18) + @Tag("invalid") + @DisplayName( + "Scenario 8: Null factory results in a NullPointerException due to ConcurrentHashMap restrictions") + public void testNullFactoryThrowsNullPointerException() { + assertThrows(NullPointerException.class, () -> LlmRegistry.registerTestLlm("custom-.*", null)); + } + + @Test + @Order(19) + @Tag("boundary") + @DisplayName("Scenario 5: Using a catch-all regex clears the entire cache") + public void testCatchAllPatternClearsAllCachedInstances() { + // First register catch-all with an initial factory and populate cache + Map oldMap = new ConcurrentHashMap<>(); + BaseLlm i1 = Mockito.mock(BaseLlm.class); + BaseLlm i2 = Mockito.mock(BaseLlm.class); + BaseLlm i3 = Mockito.mock(BaseLlm.class); + oldMap.put("zzcatch-1", i1); + oldMap.put("zzcatch-2", i2); + oldMap.put("zzcatch-3", i3); + LlmFactory oldFactory = factoryFromMap(oldMap); + LlmRegistry.registerTestLlm(".*", oldFactory); + BaseLlm prev1 = LlmRegistry.getLlm("zzcatch-1"); + BaseLlm prev2 = LlmRegistry.getLlm("zzcatch-2"); + BaseLlm prev3 = LlmRegistry.getLlm("zzcatch-3"); + // Now override catch-all with a new factory to clear and recreate + Map newMap = new ConcurrentHashMap<>(); + BaseLlm n1 = Mockito.mock(BaseLlm.class); + BaseLlm n2 = Mockito.mock(BaseLlm.class); + BaseLlm n3 = Mockito.mock(BaseLlm.class); + newMap.put("zzcatch-1", n1); + newMap.put("zzcatch-2", n2); + newMap.put("zzcatch-3", n3); + LlmFactory newFactory = factoryFromMap(newMap); + LlmRegistry.registerTestLlm(".*", newFactory); + BaseLlm after1 = LlmRegistry.getLlm("zzcatch-1"); + BaseLlm after2 = LlmRegistry.getLlm("zzcatch-2"); + BaseLlm after3 = LlmRegistry.getLlm("zzcatch-3"); + assertNotSame((BaseLlm) prev1, (BaseLlm) after1); + assertNotSame((BaseLlm) prev2, (BaseLlm) after2); + assertNotSame((BaseLlm) prev3, (BaseLlm) after3); + assertSame((BaseLlm) n1, (BaseLlm) after1); + assertSame((BaseLlm) n2, (BaseLlm) after2); + assertSame((BaseLlm) n3, (BaseLlm) after3); + } + + @Test + @Order(20) + @Tag("invalid") + @DisplayName("Scenario 9: Invalid regex pattern triggers a PatternSyntaxException during removal") + public void testInvalidRegexPatternThrowsPatternSyntaxException() { + // Pre-populate to ensure removeIf predicate is evaluated + Map preMap = new ConcurrentHashMap<>(); + BaseLlm g = Mockito.mock(BaseLlm.class); + preMap.put("gamma-1", g); + LlmFactory preFactory = factoryFromMap(preMap); + LlmRegistry.registerTestLlm("gamma-.*", preFactory); + BaseLlm prev = LlmRegistry.getLlm("gamma-1"); + assertSame((BaseLlm) g, (BaseLlm) prev); + Map someMap = new ConcurrentHashMap<>(); + BaseLlm any = Mockito.mock(BaseLlm.class); + someMap.put("any", any); + LlmFactory someFactory = factoryFromMap(someMap); + assertThrows(PatternSyntaxException.class, () -> LlmRegistry.registerTestLlm("(", someFactory)); + } + // TODO: Adjust model names or patterns if repository introduces new default patterns + // that could overlap unexpectedly. + +} 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