From 15199c7423fc92a14f7d792b9eb86950a604d63a Mon Sep 17 00:00:00 2001 From: Marcos Date: Wed, 1 Jul 2026 11:47:57 +0200 Subject: [PATCH 1/3] Make LLM providers pluggable via auto-configuration Register each provider's AgentOnboardingProvider through a Spring Boot @AutoConfiguration class listed in the module's AutoConfiguration.imports, mirroring the plugin modules. Providers now self-register from the classpath independently of the app's component scan, instead of relying on @Component being reachable under the shared ai.javaclaw package. - anthropic: AnthropticClaudeCodeConfiguration @Configuration -> @AutoConfiguration; onboarding provider moved to a @Bean in AnthropicAgentAutoConfiguration - openai/ollama/google: drop @Component, add per-module @AutoConfiguration + imports - Remove dead AgentProvider (unused; isConfigured stub returned false and getDefaultChatModel would throw). Model selection is handled centrally by spring.ai.model.chat. - Add per-provider tests asserting the bean registers and the imports file names the auto-config (the only guard for provider discovery, since OnboardingControllerTest mocks AgentOnboardingProviders) - Update AGENTS.md wording --- AGENTS.md | 4 +- .../ai/javaclaw/providers/AgentProvider.java | 30 ----------- .../AnthropicAgentAutoConfiguration.java | 14 +++++ .../AnthropicAgentOnboardingProvider.java | 2 - .../AnthropticClaudeCodeConfiguration.java | 4 +- ...ot.autoconfigure.AutoConfiguration.imports | 2 + .../AnthropicAgentAutoConfigurationTest.java | 51 +++++++++++++++++++ .../GoogleGenAIAgentAutoConfiguration.java | 14 +++++ .../GoogleGenAIAgentOnboardingProvider.java | 2 - ...ot.autoconfigure.AutoConfiguration.imports | 1 + ...GoogleGenAIAgentAutoConfigurationTest.java | 50 ++++++++++++++++++ .../ollama/OllamaAgentAutoConfiguration.java | 14 +++++ .../ollama/OllamaAgentOnboardingProvider.java | 2 - ...ot.autoconfigure.AutoConfiguration.imports | 1 + .../OllamaAgentAutoConfigurationTest.java | 50 ++++++++++++++++++ .../openai/OpenAIAgentAutoConfiguration.java | 14 +++++ .../openai/OpenAIAgentOnboardingProvider.java | 2 - ...ot.autoconfigure.AutoConfiguration.imports | 1 + .../OpenAIAgentAutoConfigurationTest.java | 50 ++++++++++++++++++ 19 files changed, 266 insertions(+), 42 deletions(-) delete mode 100644 base/src/main/java/ai/javaclaw/providers/AgentProvider.java create mode 100644 providers/anthropic/src/main/java/ai/javaclaw/providers/anthropic/AnthropicAgentAutoConfiguration.java create mode 100644 providers/anthropic/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports create mode 100644 providers/anthropic/src/test/java/ai/javaclaw/providers/anthropic/AnthropicAgentAutoConfigurationTest.java create mode 100644 providers/google/src/main/java/ai/javaclaw/providers/google/genai/GoogleGenAIAgentAutoConfiguration.java create mode 100644 providers/google/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports create mode 100644 providers/google/src/test/java/ai/javaclaw/providers/google/genai/GoogleGenAIAgentAutoConfigurationTest.java create mode 100644 providers/ollama/src/main/java/ai/javaclaw/providers/ollama/OllamaAgentAutoConfiguration.java create mode 100644 providers/ollama/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports create mode 100644 providers/ollama/src/test/java/ai/javaclaw/providers/ollama/OllamaAgentAutoConfigurationTest.java create mode 100644 providers/openai/src/main/java/ai/javaclaw/providers/openai/OpenAIAgentAutoConfiguration.java create mode 100644 providers/openai/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports create mode 100644 providers/openai/src/test/java/ai/javaclaw/providers/openai/OpenAIAgentAutoConfigurationTest.java diff --git a/AGENTS.md b/AGENTS.md index 9f936f64..554879db 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -43,7 +43,7 @@ root `app` depends on `base` + all `providers/` + all `plugins/`. `ChatChannel` lives inside `app/`. -Each **provider** implements `AgentOnboardingProvider` (in `base`) and is auto-discovered by Spring. Each **plugin** is an optional Spring Boot auto-configuration module that contributes tools or channels. +Each **provider** implements `AgentOnboardingProvider` (in `base`) and self-registers via Spring Boot auto-configuration (`META-INF/spring/…AutoConfiguration.imports`) — same drop-in mechanism as plugins, independent of the app's component scan. Each **plugin** is an optional Spring Boot auto-configuration module that contributes tools or channels. --- @@ -107,7 +107,7 @@ User/Agent → TaskManager.create() | `MCP Tools` | `SyncMcpToolCallbackProvider` | | `BraveWebSearchTool` | 15 results (only if Brave API key configured) | -**Supported LLM Providers** — each lives in its own `providers//` module and implements `AgentOnboardingProvider`: +**Supported LLM Providers** — each lives in its own `providers//` module and contributes its `AgentOnboardingProvider` through an `@AutoConfiguration` class registered in the module's `AutoConfiguration.imports`. The active model is selected centrally by `spring.ai.model.chat` (Spring AI), not per-provider; the default `unknown` disables all of them until onboarding sets it: | Provider | Module | Default Model | API Key | |---|---|---|---| diff --git a/base/src/main/java/ai/javaclaw/providers/AgentProvider.java b/base/src/main/java/ai/javaclaw/providers/AgentProvider.java deleted file mode 100644 index 793fa60d..00000000 --- a/base/src/main/java/ai/javaclaw/providers/AgentProvider.java +++ /dev/null @@ -1,30 +0,0 @@ -package ai.javaclaw.providers; - -import ai.javaclaw.onboarding.AgentOnboardingProvider; -import org.springframework.ai.chat.model.ChatModel; -import org.springframework.core.env.Environment; -import org.springframework.stereotype.Component; - -import java.util.SequencedCollection; - -@Component -public class AgentProvider { - - private final Environment environment; - private final SequencedCollection agentOnboardingProviders; - private final SequencedCollection chatModelProviders; - - public AgentProvider(Environment environment, SequencedCollection agentOnboardingProviders, SequencedCollection chatModelProviders) { - this.environment = environment; - this.agentOnboardingProviders = agentOnboardingProviders; - this.chatModelProviders = chatModelProviders.stream().filter(this::isConfigured).toList(); - } - - private boolean isConfigured(ChatModel chatModel) { - return false; - } - - public ChatModel getDefaultChatModel() { - return chatModelProviders.getFirst(); - } -} diff --git a/providers/anthropic/src/main/java/ai/javaclaw/providers/anthropic/AnthropicAgentAutoConfiguration.java b/providers/anthropic/src/main/java/ai/javaclaw/providers/anthropic/AnthropicAgentAutoConfiguration.java new file mode 100644 index 00000000..91b3ff87 --- /dev/null +++ b/providers/anthropic/src/main/java/ai/javaclaw/providers/anthropic/AnthropicAgentAutoConfiguration.java @@ -0,0 +1,14 @@ +package ai.javaclaw.providers.anthropic; + +import ai.javaclaw.onboarding.AgentOnboardingProvider; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.context.annotation.Bean; + +@AutoConfiguration +public class AnthropicAgentAutoConfiguration { + + @Bean + public AgentOnboardingProvider anthropicAgentOnboardingProvider() { + return new AnthropicAgentOnboardingProvider(); + } +} \ No newline at end of file diff --git a/providers/anthropic/src/main/java/ai/javaclaw/providers/anthropic/AnthropicAgentOnboardingProvider.java b/providers/anthropic/src/main/java/ai/javaclaw/providers/anthropic/AnthropicAgentOnboardingProvider.java index 8c7ea5e0..feb13910 100644 --- a/providers/anthropic/src/main/java/ai/javaclaw/providers/anthropic/AnthropicAgentOnboardingProvider.java +++ b/providers/anthropic/src/main/java/ai/javaclaw/providers/anthropic/AnthropicAgentOnboardingProvider.java @@ -1,13 +1,11 @@ package ai.javaclaw.providers.anthropic; import ai.javaclaw.onboarding.AgentOnboardingProvider; -import org.springframework.stereotype.Component; import java.util.Optional; import static ai.javaclaw.providers.anthropic.AnthropticClaudeCodeConfiguration.CLAUDE_CODE_OATH_TOKEN_PLACEHOLDER; -@Component public class AnthropicAgentOnboardingProvider implements AgentOnboardingProvider { @Override public String getId() { diff --git a/providers/anthropic/src/main/java/ai/javaclaw/providers/anthropic/AnthropticClaudeCodeConfiguration.java b/providers/anthropic/src/main/java/ai/javaclaw/providers/anthropic/AnthropticClaudeCodeConfiguration.java index 8c4db32b..95f53528 100644 --- a/providers/anthropic/src/main/java/ai/javaclaw/providers/anthropic/AnthropticClaudeCodeConfiguration.java +++ b/providers/anthropic/src/main/java/ai/javaclaw/providers/anthropic/AnthropticClaudeCodeConfiguration.java @@ -11,11 +11,11 @@ import org.springframework.ai.model.anthropic.autoconfigure.AnthropicChatProperties; import org.springframework.ai.model.anthropic.autoconfigure.AnthropicConnectionProperties; import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -@Configuration +@AutoConfiguration @ConditionalOnProperty(name = "spring.ai.anthropic.api-key", havingValue = AnthropticClaudeCodeConfiguration.CLAUDE_CODE_OATH_TOKEN_PLACEHOLDER) public class AnthropticClaudeCodeConfiguration { diff --git a/providers/anthropic/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/providers/anthropic/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 00000000..5b807c20 --- /dev/null +++ b/providers/anthropic/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,2 @@ +ai.javaclaw.providers.anthropic.AnthropicAgentAutoConfiguration +ai.javaclaw.providers.anthropic.AnthropticClaudeCodeConfiguration diff --git a/providers/anthropic/src/test/java/ai/javaclaw/providers/anthropic/AnthropicAgentAutoConfigurationTest.java b/providers/anthropic/src/test/java/ai/javaclaw/providers/anthropic/AnthropicAgentAutoConfigurationTest.java new file mode 100644 index 00000000..03243549 --- /dev/null +++ b/providers/anthropic/src/test/java/ai/javaclaw/providers/anthropic/AnthropicAgentAutoConfigurationTest.java @@ -0,0 +1,51 @@ +package ai.javaclaw.providers.anthropic; + +import ai.javaclaw.onboarding.AgentOnboardingProvider; +import org.junit.jupiter.api.Test; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; + +import java.io.IOException; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +class AnthropicAgentAutoConfigurationTest { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(AnthropicAgentAutoConfiguration.class)); + + @Test + void registersOnboardingProvider() { + contextRunner.run(context -> { + assertThat(context).hasSingleBean(AgentOnboardingProvider.class); + assertThat(context.getBean(AgentOnboardingProvider.class).getId()).isEqualTo("anthropic"); + }); + } + + @Test + void bothAutoConfigurationsAreRegisteredViaImportsFile() throws IOException { + assertThat(importedAutoConfigurations()).contains( + AnthropicAgentAutoConfiguration.class.getName(), + AnthropticClaudeCodeConfiguration.class.getName()); + } + + static List importedAutoConfigurations() throws IOException { + List classNames = new ArrayList<>(); + Enumeration resources = Thread.currentThread().getContextClassLoader() + .getResources("META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports"); + while (resources.hasMoreElements()) { + try (var in = resources.nextElement().openStream()) { + new String(in.readAllBytes(), StandardCharsets.UTF_8).lines() + .map(String::trim) + .filter(line -> !line.isEmpty()) + .forEach(classNames::add); + } + } + return classNames; + } +} \ No newline at end of file diff --git a/providers/google/src/main/java/ai/javaclaw/providers/google/genai/GoogleGenAIAgentAutoConfiguration.java b/providers/google/src/main/java/ai/javaclaw/providers/google/genai/GoogleGenAIAgentAutoConfiguration.java new file mode 100644 index 00000000..46986756 --- /dev/null +++ b/providers/google/src/main/java/ai/javaclaw/providers/google/genai/GoogleGenAIAgentAutoConfiguration.java @@ -0,0 +1,14 @@ +package ai.javaclaw.providers.google.genai; + +import ai.javaclaw.onboarding.AgentOnboardingProvider; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.context.annotation.Bean; + +@AutoConfiguration +public class GoogleGenAIAgentAutoConfiguration { + + @Bean + public AgentOnboardingProvider googleGenAIAgentOnboardingProvider() { + return new GoogleGenAIAgentOnboardingProvider(); + } +} \ No newline at end of file diff --git a/providers/google/src/main/java/ai/javaclaw/providers/google/genai/GoogleGenAIAgentOnboardingProvider.java b/providers/google/src/main/java/ai/javaclaw/providers/google/genai/GoogleGenAIAgentOnboardingProvider.java index 8f656b2f..d577649b 100644 --- a/providers/google/src/main/java/ai/javaclaw/providers/google/genai/GoogleGenAIAgentOnboardingProvider.java +++ b/providers/google/src/main/java/ai/javaclaw/providers/google/genai/GoogleGenAIAgentOnboardingProvider.java @@ -1,9 +1,7 @@ package ai.javaclaw.providers.google.genai; import ai.javaclaw.onboarding.AgentOnboardingProvider; -import org.springframework.stereotype.Component; -@Component public class GoogleGenAIAgentOnboardingProvider implements AgentOnboardingProvider { @Override diff --git a/providers/google/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/providers/google/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 00000000..1dab55d7 --- /dev/null +++ b/providers/google/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +ai.javaclaw.providers.google.genai.GoogleGenAIAgentAutoConfiguration \ No newline at end of file diff --git a/providers/google/src/test/java/ai/javaclaw/providers/google/genai/GoogleGenAIAgentAutoConfigurationTest.java b/providers/google/src/test/java/ai/javaclaw/providers/google/genai/GoogleGenAIAgentAutoConfigurationTest.java new file mode 100644 index 00000000..d1882155 --- /dev/null +++ b/providers/google/src/test/java/ai/javaclaw/providers/google/genai/GoogleGenAIAgentAutoConfigurationTest.java @@ -0,0 +1,50 @@ +package ai.javaclaw.providers.google.genai; + +import ai.javaclaw.onboarding.AgentOnboardingProvider; +import org.junit.jupiter.api.Test; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; + +import java.io.IOException; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +class GoogleGenAIAgentAutoConfigurationTest { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(GoogleGenAIAgentAutoConfiguration.class)); + + @Test + void registersOnboardingProvider() { + contextRunner.run(context -> { + assertThat(context).hasSingleBean(AgentOnboardingProvider.class); + assertThat(context.getBean(AgentOnboardingProvider.class).getId()).isEqualTo("google.genai"); + }); + } + + @Test + void autoConfigurationIsRegisteredViaImportsFile() throws IOException { + assertThat(importedAutoConfigurations()) + .contains(GoogleGenAIAgentAutoConfiguration.class.getName()); + } + + static List importedAutoConfigurations() throws IOException { + List classNames = new ArrayList<>(); + Enumeration resources = Thread.currentThread().getContextClassLoader() + .getResources("META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports"); + while (resources.hasMoreElements()) { + try (var in = resources.nextElement().openStream()) { + new String(in.readAllBytes(), StandardCharsets.UTF_8).lines() + .map(String::trim) + .filter(line -> !line.isEmpty()) + .forEach(classNames::add); + } + } + return classNames; + } +} \ No newline at end of file diff --git a/providers/ollama/src/main/java/ai/javaclaw/providers/ollama/OllamaAgentAutoConfiguration.java b/providers/ollama/src/main/java/ai/javaclaw/providers/ollama/OllamaAgentAutoConfiguration.java new file mode 100644 index 00000000..2335410d --- /dev/null +++ b/providers/ollama/src/main/java/ai/javaclaw/providers/ollama/OllamaAgentAutoConfiguration.java @@ -0,0 +1,14 @@ +package ai.javaclaw.providers.ollama; + +import ai.javaclaw.onboarding.AgentOnboardingProvider; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.context.annotation.Bean; + +@AutoConfiguration +public class OllamaAgentAutoConfiguration { + + @Bean + public AgentOnboardingProvider ollamaAgentOnboardingProvider() { + return new OllamaAgentOnboardingProvider(); + } +} \ No newline at end of file diff --git a/providers/ollama/src/main/java/ai/javaclaw/providers/ollama/OllamaAgentOnboardingProvider.java b/providers/ollama/src/main/java/ai/javaclaw/providers/ollama/OllamaAgentOnboardingProvider.java index 155d81a4..ceb3dbb3 100644 --- a/providers/ollama/src/main/java/ai/javaclaw/providers/ollama/OllamaAgentOnboardingProvider.java +++ b/providers/ollama/src/main/java/ai/javaclaw/providers/ollama/OllamaAgentOnboardingProvider.java @@ -1,9 +1,7 @@ package ai.javaclaw.providers.ollama; import ai.javaclaw.onboarding.AgentOnboardingProvider; -import org.springframework.stereotype.Component; -@Component public class OllamaAgentOnboardingProvider implements AgentOnboardingProvider { @Override diff --git a/providers/ollama/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/providers/ollama/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 00000000..949cb8fa --- /dev/null +++ b/providers/ollama/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +ai.javaclaw.providers.ollama.OllamaAgentAutoConfiguration \ No newline at end of file diff --git a/providers/ollama/src/test/java/ai/javaclaw/providers/ollama/OllamaAgentAutoConfigurationTest.java b/providers/ollama/src/test/java/ai/javaclaw/providers/ollama/OllamaAgentAutoConfigurationTest.java new file mode 100644 index 00000000..e3bd3b7f --- /dev/null +++ b/providers/ollama/src/test/java/ai/javaclaw/providers/ollama/OllamaAgentAutoConfigurationTest.java @@ -0,0 +1,50 @@ +package ai.javaclaw.providers.ollama; + +import ai.javaclaw.onboarding.AgentOnboardingProvider; +import org.junit.jupiter.api.Test; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; + +import java.io.IOException; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +class OllamaAgentAutoConfigurationTest { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(OllamaAgentAutoConfiguration.class)); + + @Test + void registersOnboardingProvider() { + contextRunner.run(context -> { + assertThat(context).hasSingleBean(AgentOnboardingProvider.class); + assertThat(context.getBean(AgentOnboardingProvider.class).getId()).isEqualTo("ollama"); + }); + } + + @Test + void autoConfigurationIsRegisteredViaImportsFile() throws IOException { + assertThat(importedAutoConfigurations()) + .contains(OllamaAgentAutoConfiguration.class.getName()); + } + + static List importedAutoConfigurations() throws IOException { + List classNames = new ArrayList<>(); + Enumeration resources = Thread.currentThread().getContextClassLoader() + .getResources("META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports"); + while (resources.hasMoreElements()) { + try (var in = resources.nextElement().openStream()) { + new String(in.readAllBytes(), StandardCharsets.UTF_8).lines() + .map(String::trim) + .filter(line -> !line.isEmpty()) + .forEach(classNames::add); + } + } + return classNames; + } +} \ No newline at end of file diff --git a/providers/openai/src/main/java/ai/javaclaw/providers/openai/OpenAIAgentAutoConfiguration.java b/providers/openai/src/main/java/ai/javaclaw/providers/openai/OpenAIAgentAutoConfiguration.java new file mode 100644 index 00000000..9f5bf77d --- /dev/null +++ b/providers/openai/src/main/java/ai/javaclaw/providers/openai/OpenAIAgentAutoConfiguration.java @@ -0,0 +1,14 @@ +package ai.javaclaw.providers.openai; + +import ai.javaclaw.onboarding.AgentOnboardingProvider; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.context.annotation.Bean; + +@AutoConfiguration +public class OpenAIAgentAutoConfiguration { + + @Bean + public AgentOnboardingProvider openAIAgentOnboardingProvider() { + return new OpenAIAgentOnboardingProvider(); + } +} \ No newline at end of file diff --git a/providers/openai/src/main/java/ai/javaclaw/providers/openai/OpenAIAgentOnboardingProvider.java b/providers/openai/src/main/java/ai/javaclaw/providers/openai/OpenAIAgentOnboardingProvider.java index f981f981..412821a0 100644 --- a/providers/openai/src/main/java/ai/javaclaw/providers/openai/OpenAIAgentOnboardingProvider.java +++ b/providers/openai/src/main/java/ai/javaclaw/providers/openai/OpenAIAgentOnboardingProvider.java @@ -1,9 +1,7 @@ package ai.javaclaw.providers.openai; import ai.javaclaw.onboarding.AgentOnboardingProvider; -import org.springframework.stereotype.Component; -@Component public class OpenAIAgentOnboardingProvider implements AgentOnboardingProvider { @Override diff --git a/providers/openai/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/providers/openai/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 00000000..dd7c105b --- /dev/null +++ b/providers/openai/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +ai.javaclaw.providers.openai.OpenAIAgentAutoConfiguration \ No newline at end of file diff --git a/providers/openai/src/test/java/ai/javaclaw/providers/openai/OpenAIAgentAutoConfigurationTest.java b/providers/openai/src/test/java/ai/javaclaw/providers/openai/OpenAIAgentAutoConfigurationTest.java new file mode 100644 index 00000000..9ab53a2d --- /dev/null +++ b/providers/openai/src/test/java/ai/javaclaw/providers/openai/OpenAIAgentAutoConfigurationTest.java @@ -0,0 +1,50 @@ +package ai.javaclaw.providers.openai; + +import ai.javaclaw.onboarding.AgentOnboardingProvider; +import org.junit.jupiter.api.Test; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; + +import java.io.IOException; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +class OpenAIAgentAutoConfigurationTest { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(OpenAIAgentAutoConfiguration.class)); + + @Test + void registersOnboardingProvider() { + contextRunner.run(context -> { + assertThat(context).hasSingleBean(AgentOnboardingProvider.class); + assertThat(context.getBean(AgentOnboardingProvider.class).getId()).isEqualTo("openai"); + }); + } + + @Test + void autoConfigurationIsRegisteredViaImportsFile() throws IOException { + assertThat(importedAutoConfigurations()) + .contains(OpenAIAgentAutoConfiguration.class.getName()); + } + + static List importedAutoConfigurations() throws IOException { + List classNames = new ArrayList<>(); + Enumeration resources = Thread.currentThread().getContextClassLoader() + .getResources("META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports"); + while (resources.hasMoreElements()) { + try (var in = resources.nextElement().openStream()) { + new String(in.readAllBytes(), StandardCharsets.UTF_8).lines() + .map(String::trim) + .filter(line -> !line.isEmpty()) + .forEach(classNames::add); + } + } + return classNames; + } +} \ No newline at end of file From d6ea74810bb26b3cdedf534f123f9bf2415bba23 Mon Sep 17 00:00:00 2001 From: Marcos Date: Mon, 13 Jul 2026 12:04:08 +0200 Subject: [PATCH 2/3] Address review feedback on pluggable providers auto-config - Make Anthropic's ChatModel auto-config ordering explicit against AnthropicChatAutoConfiguration instead of relying on alphabetical package sort. - Add @ConditionalOnMissingBean, typed to the concrete provider class, to each provider's onboarding auto-config so user overrides win. - Fix the imports-file test helper to read each module's own .imports resource instead of scanning the whole classpath, so it can't pass on another module's entry. --- .../AnthropicAgentAutoConfiguration.java | 2 ++ .../AnthropticClaudeCodeConfiguration.java | 3 ++- .../AnthropicAgentAutoConfigurationTest.java | 20 +++++++------------ .../GoogleGenAIAgentAutoConfiguration.java | 2 ++ ...GoogleGenAIAgentAutoConfigurationTest.java | 20 +++++++------------ .../ollama/OllamaAgentAutoConfiguration.java | 2 ++ .../OllamaAgentAutoConfigurationTest.java | 20 +++++++------------ .../openai/OpenAIAgentAutoConfiguration.java | 2 ++ .../OpenAIAgentAutoConfigurationTest.java | 20 +++++++------------ 9 files changed, 38 insertions(+), 53 deletions(-) diff --git a/providers/anthropic/src/main/java/ai/javaclaw/providers/anthropic/AnthropicAgentAutoConfiguration.java b/providers/anthropic/src/main/java/ai/javaclaw/providers/anthropic/AnthropicAgentAutoConfiguration.java index 91b3ff87..14a0eeeb 100644 --- a/providers/anthropic/src/main/java/ai/javaclaw/providers/anthropic/AnthropicAgentAutoConfiguration.java +++ b/providers/anthropic/src/main/java/ai/javaclaw/providers/anthropic/AnthropicAgentAutoConfiguration.java @@ -2,12 +2,14 @@ import ai.javaclaw.onboarding.AgentOnboardingProvider; import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.context.annotation.Bean; @AutoConfiguration public class AnthropicAgentAutoConfiguration { @Bean + @ConditionalOnMissingBean(AnthropicAgentOnboardingProvider.class) public AgentOnboardingProvider anthropicAgentOnboardingProvider() { return new AnthropicAgentOnboardingProvider(); } diff --git a/providers/anthropic/src/main/java/ai/javaclaw/providers/anthropic/AnthropticClaudeCodeConfiguration.java b/providers/anthropic/src/main/java/ai/javaclaw/providers/anthropic/AnthropticClaudeCodeConfiguration.java index 95f53528..f1157b54 100644 --- a/providers/anthropic/src/main/java/ai/javaclaw/providers/anthropic/AnthropticClaudeCodeConfiguration.java +++ b/providers/anthropic/src/main/java/ai/javaclaw/providers/anthropic/AnthropticClaudeCodeConfiguration.java @@ -8,6 +8,7 @@ import org.springframework.ai.anthropic.AnthropicChatOptions; import org.springframework.ai.anthropic.http.okhttp.SpringAiAnthropicHttpClient; import org.springframework.ai.chat.observation.ChatModelObservationConvention; +import org.springframework.ai.model.anthropic.autoconfigure.AnthropicChatAutoConfiguration; import org.springframework.ai.model.anthropic.autoconfigure.AnthropicChatProperties; import org.springframework.ai.model.anthropic.autoconfigure.AnthropicConnectionProperties; import org.springframework.beans.factory.ObjectProvider; @@ -15,7 +16,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Bean; -@AutoConfiguration +@AutoConfiguration(before = AnthropicChatAutoConfiguration.class) @ConditionalOnProperty(name = "spring.ai.anthropic.api-key", havingValue = AnthropticClaudeCodeConfiguration.CLAUDE_CODE_OATH_TOKEN_PLACEHOLDER) public class AnthropticClaudeCodeConfiguration { diff --git a/providers/anthropic/src/test/java/ai/javaclaw/providers/anthropic/AnthropicAgentAutoConfigurationTest.java b/providers/anthropic/src/test/java/ai/javaclaw/providers/anthropic/AnthropicAgentAutoConfigurationTest.java index 03243549..bb315556 100644 --- a/providers/anthropic/src/test/java/ai/javaclaw/providers/anthropic/AnthropicAgentAutoConfigurationTest.java +++ b/providers/anthropic/src/test/java/ai/javaclaw/providers/anthropic/AnthropicAgentAutoConfigurationTest.java @@ -8,8 +8,6 @@ import java.io.IOException; import java.net.URL; import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.Enumeration; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; @@ -35,17 +33,13 @@ void bothAutoConfigurationsAreRegisteredViaImportsFile() throws IOException { } static List importedAutoConfigurations() throws IOException { - List classNames = new ArrayList<>(); - Enumeration resources = Thread.currentThread().getContextClassLoader() - .getResources("META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports"); - while (resources.hasMoreElements()) { - try (var in = resources.nextElement().openStream()) { - new String(in.readAllBytes(), StandardCharsets.UTF_8).lines() - .map(String::trim) - .filter(line -> !line.isEmpty()) - .forEach(classNames::add); - } + URL resource = AnthropicAgentAutoConfigurationTest.class.getClassLoader() + .getResource("META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports"); + try (var in = resource.openStream()) { + return new String(in.readAllBytes(), StandardCharsets.UTF_8).lines() + .map(String::trim) + .filter(line -> !line.isEmpty()) + .toList(); } - return classNames; } } \ No newline at end of file diff --git a/providers/google/src/main/java/ai/javaclaw/providers/google/genai/GoogleGenAIAgentAutoConfiguration.java b/providers/google/src/main/java/ai/javaclaw/providers/google/genai/GoogleGenAIAgentAutoConfiguration.java index 46986756..145f9c53 100644 --- a/providers/google/src/main/java/ai/javaclaw/providers/google/genai/GoogleGenAIAgentAutoConfiguration.java +++ b/providers/google/src/main/java/ai/javaclaw/providers/google/genai/GoogleGenAIAgentAutoConfiguration.java @@ -2,12 +2,14 @@ import ai.javaclaw.onboarding.AgentOnboardingProvider; import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.context.annotation.Bean; @AutoConfiguration public class GoogleGenAIAgentAutoConfiguration { @Bean + @ConditionalOnMissingBean(GoogleGenAIAgentOnboardingProvider.class) public AgentOnboardingProvider googleGenAIAgentOnboardingProvider() { return new GoogleGenAIAgentOnboardingProvider(); } diff --git a/providers/google/src/test/java/ai/javaclaw/providers/google/genai/GoogleGenAIAgentAutoConfigurationTest.java b/providers/google/src/test/java/ai/javaclaw/providers/google/genai/GoogleGenAIAgentAutoConfigurationTest.java index d1882155..0b155004 100644 --- a/providers/google/src/test/java/ai/javaclaw/providers/google/genai/GoogleGenAIAgentAutoConfigurationTest.java +++ b/providers/google/src/test/java/ai/javaclaw/providers/google/genai/GoogleGenAIAgentAutoConfigurationTest.java @@ -8,8 +8,6 @@ import java.io.IOException; import java.net.URL; import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.Enumeration; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; @@ -34,17 +32,13 @@ void autoConfigurationIsRegisteredViaImportsFile() throws IOException { } static List importedAutoConfigurations() throws IOException { - List classNames = new ArrayList<>(); - Enumeration resources = Thread.currentThread().getContextClassLoader() - .getResources("META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports"); - while (resources.hasMoreElements()) { - try (var in = resources.nextElement().openStream()) { - new String(in.readAllBytes(), StandardCharsets.UTF_8).lines() - .map(String::trim) - .filter(line -> !line.isEmpty()) - .forEach(classNames::add); - } + URL resource = GoogleGenAIAgentAutoConfigurationTest.class.getClassLoader() + .getResource("META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports"); + try (var in = resource.openStream()) { + return new String(in.readAllBytes(), StandardCharsets.UTF_8).lines() + .map(String::trim) + .filter(line -> !line.isEmpty()) + .toList(); } - return classNames; } } \ No newline at end of file diff --git a/providers/ollama/src/main/java/ai/javaclaw/providers/ollama/OllamaAgentAutoConfiguration.java b/providers/ollama/src/main/java/ai/javaclaw/providers/ollama/OllamaAgentAutoConfiguration.java index 2335410d..1eb98bb0 100644 --- a/providers/ollama/src/main/java/ai/javaclaw/providers/ollama/OllamaAgentAutoConfiguration.java +++ b/providers/ollama/src/main/java/ai/javaclaw/providers/ollama/OllamaAgentAutoConfiguration.java @@ -2,12 +2,14 @@ import ai.javaclaw.onboarding.AgentOnboardingProvider; import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.context.annotation.Bean; @AutoConfiguration public class OllamaAgentAutoConfiguration { @Bean + @ConditionalOnMissingBean(OllamaAgentOnboardingProvider.class) public AgentOnboardingProvider ollamaAgentOnboardingProvider() { return new OllamaAgentOnboardingProvider(); } diff --git a/providers/ollama/src/test/java/ai/javaclaw/providers/ollama/OllamaAgentAutoConfigurationTest.java b/providers/ollama/src/test/java/ai/javaclaw/providers/ollama/OllamaAgentAutoConfigurationTest.java index e3bd3b7f..355ebda8 100644 --- a/providers/ollama/src/test/java/ai/javaclaw/providers/ollama/OllamaAgentAutoConfigurationTest.java +++ b/providers/ollama/src/test/java/ai/javaclaw/providers/ollama/OllamaAgentAutoConfigurationTest.java @@ -8,8 +8,6 @@ import java.io.IOException; import java.net.URL; import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.Enumeration; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; @@ -34,17 +32,13 @@ void autoConfigurationIsRegisteredViaImportsFile() throws IOException { } static List importedAutoConfigurations() throws IOException { - List classNames = new ArrayList<>(); - Enumeration resources = Thread.currentThread().getContextClassLoader() - .getResources("META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports"); - while (resources.hasMoreElements()) { - try (var in = resources.nextElement().openStream()) { - new String(in.readAllBytes(), StandardCharsets.UTF_8).lines() - .map(String::trim) - .filter(line -> !line.isEmpty()) - .forEach(classNames::add); - } + URL resource = OllamaAgentAutoConfigurationTest.class.getClassLoader() + .getResource("META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports"); + try (var in = resource.openStream()) { + return new String(in.readAllBytes(), StandardCharsets.UTF_8).lines() + .map(String::trim) + .filter(line -> !line.isEmpty()) + .toList(); } - return classNames; } } \ No newline at end of file diff --git a/providers/openai/src/main/java/ai/javaclaw/providers/openai/OpenAIAgentAutoConfiguration.java b/providers/openai/src/main/java/ai/javaclaw/providers/openai/OpenAIAgentAutoConfiguration.java index 9f5bf77d..c21ae3ea 100644 --- a/providers/openai/src/main/java/ai/javaclaw/providers/openai/OpenAIAgentAutoConfiguration.java +++ b/providers/openai/src/main/java/ai/javaclaw/providers/openai/OpenAIAgentAutoConfiguration.java @@ -2,12 +2,14 @@ import ai.javaclaw.onboarding.AgentOnboardingProvider; import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.context.annotation.Bean; @AutoConfiguration public class OpenAIAgentAutoConfiguration { @Bean + @ConditionalOnMissingBean(OpenAIAgentOnboardingProvider.class) public AgentOnboardingProvider openAIAgentOnboardingProvider() { return new OpenAIAgentOnboardingProvider(); } diff --git a/providers/openai/src/test/java/ai/javaclaw/providers/openai/OpenAIAgentAutoConfigurationTest.java b/providers/openai/src/test/java/ai/javaclaw/providers/openai/OpenAIAgentAutoConfigurationTest.java index 9ab53a2d..1097914a 100644 --- a/providers/openai/src/test/java/ai/javaclaw/providers/openai/OpenAIAgentAutoConfigurationTest.java +++ b/providers/openai/src/test/java/ai/javaclaw/providers/openai/OpenAIAgentAutoConfigurationTest.java @@ -8,8 +8,6 @@ import java.io.IOException; import java.net.URL; import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.Enumeration; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; @@ -34,17 +32,13 @@ void autoConfigurationIsRegisteredViaImportsFile() throws IOException { } static List importedAutoConfigurations() throws IOException { - List classNames = new ArrayList<>(); - Enumeration resources = Thread.currentThread().getContextClassLoader() - .getResources("META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports"); - while (resources.hasMoreElements()) { - try (var in = resources.nextElement().openStream()) { - new String(in.readAllBytes(), StandardCharsets.UTF_8).lines() - .map(String::trim) - .filter(line -> !line.isEmpty()) - .forEach(classNames::add); - } + URL resource = OpenAIAgentAutoConfigurationTest.class.getClassLoader() + .getResource("META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports"); + try (var in = resource.openStream()) { + return new String(in.readAllBytes(), StandardCharsets.UTF_8).lines() + .map(String::trim) + .filter(line -> !line.isEmpty()) + .toList(); } - return classNames; } } \ No newline at end of file From a28113907fda52075f8cbbc39503192f1cbd5e2d Mon Sep 17 00:00:00 2001 From: Marcos Date: Sun, 26 Jul 2026 12:05:52 +0200 Subject: [PATCH 3/3] Deduplicate AutoConfiguration.imports test helper across providers Each provider test class duplicated the same block for reading its module's AutoConfiguration.imports file. Extract it into a shared AutoConfigurationImportsTestSupport test fixture in base (using Files.readAllLines instead of manual stream reading) and have all four provider tests use it. --- base/build.gradle | 1 + .../AutoConfigurationImportsTestSupport.java | 31 +++++++++++++++++++ providers/anthropic/build.gradle | 1 + .../AnthropicAgentAutoConfigurationTest.java | 17 ++-------- providers/google/build.gradle | 1 + ...GoogleGenAIAgentAutoConfigurationTest.java | 17 ++-------- providers/ollama/build.gradle | 1 + .../OllamaAgentAutoConfigurationTest.java | 17 ++-------- providers/openai/build.gradle | 1 + .../OpenAIAgentAutoConfigurationTest.java | 17 ++-------- 10 files changed, 44 insertions(+), 60 deletions(-) create mode 100644 base/src/testFixtures/java/ai/javaclaw/testsupport/AutoConfigurationImportsTestSupport.java diff --git a/base/build.gradle b/base/build.gradle index e84bd4b8..bdf4d655 100644 --- a/base/build.gradle +++ b/base/build.gradle @@ -1,5 +1,6 @@ plugins { id 'java-library' + id 'java-test-fixtures' id("com.skillsjars.gradle-plugin") version "0.0.2" } diff --git a/base/src/testFixtures/java/ai/javaclaw/testsupport/AutoConfigurationImportsTestSupport.java b/base/src/testFixtures/java/ai/javaclaw/testsupport/AutoConfigurationImportsTestSupport.java new file mode 100644 index 00000000..e1389b71 --- /dev/null +++ b/base/src/testFixtures/java/ai/javaclaw/testsupport/AutoConfigurationImportsTestSupport.java @@ -0,0 +1,31 @@ +package ai.javaclaw.testsupport; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; + +public final class AutoConfigurationImportsTestSupport { + + private static final String IMPORTS_RESOURCE = + "META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports"; + + private AutoConfigurationImportsTestSupport() { + } + + public static List importedAutoConfigurations(Class testClass) throws IOException { + URL resource = testClass.getClassLoader().getResource(IMPORTS_RESOURCE); + try { + Path path = Path.of(resource.toURI()); + return Files.readAllLines(path).stream() + .map(String::trim) + .filter(line -> !line.isEmpty()) + .toList(); + } catch (URISyntaxException e) { + throw new UncheckedIOException(new IOException(e)); + } + } +} \ No newline at end of file diff --git a/providers/anthropic/build.gradle b/providers/anthropic/build.gradle index 5c80063b..3df69570 100644 --- a/providers/anthropic/build.gradle +++ b/providers/anthropic/build.gradle @@ -8,4 +8,5 @@ dependencies { implementation 'org.springframework.ai:spring-ai-starter-model-anthropic' testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation testFixtures(project(':base')) } diff --git a/providers/anthropic/src/test/java/ai/javaclaw/providers/anthropic/AnthropicAgentAutoConfigurationTest.java b/providers/anthropic/src/test/java/ai/javaclaw/providers/anthropic/AnthropicAgentAutoConfigurationTest.java index bb315556..0bf7ea51 100644 --- a/providers/anthropic/src/test/java/ai/javaclaw/providers/anthropic/AnthropicAgentAutoConfigurationTest.java +++ b/providers/anthropic/src/test/java/ai/javaclaw/providers/anthropic/AnthropicAgentAutoConfigurationTest.java @@ -6,10 +6,8 @@ import org.springframework.boot.test.context.runner.ApplicationContextRunner; import java.io.IOException; -import java.net.URL; -import java.nio.charset.StandardCharsets; -import java.util.List; +import static ai.javaclaw.testsupport.AutoConfigurationImportsTestSupport.importedAutoConfigurations; import static org.assertj.core.api.Assertions.assertThat; class AnthropicAgentAutoConfigurationTest { @@ -27,19 +25,8 @@ void registersOnboardingProvider() { @Test void bothAutoConfigurationsAreRegisteredViaImportsFile() throws IOException { - assertThat(importedAutoConfigurations()).contains( + assertThat(importedAutoConfigurations(AnthropicAgentAutoConfigurationTest.class)).contains( AnthropicAgentAutoConfiguration.class.getName(), AnthropticClaudeCodeConfiguration.class.getName()); } - - static List importedAutoConfigurations() throws IOException { - URL resource = AnthropicAgentAutoConfigurationTest.class.getClassLoader() - .getResource("META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports"); - try (var in = resource.openStream()) { - return new String(in.readAllBytes(), StandardCharsets.UTF_8).lines() - .map(String::trim) - .filter(line -> !line.isEmpty()) - .toList(); - } - } } \ No newline at end of file diff --git a/providers/google/build.gradle b/providers/google/build.gradle index b74f1158..3d179917 100644 --- a/providers/google/build.gradle +++ b/providers/google/build.gradle @@ -8,4 +8,5 @@ dependencies { implementation 'org.springframework.ai:spring-ai-starter-model-google-genai' testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation testFixtures(project(':base')) } diff --git a/providers/google/src/test/java/ai/javaclaw/providers/google/genai/GoogleGenAIAgentAutoConfigurationTest.java b/providers/google/src/test/java/ai/javaclaw/providers/google/genai/GoogleGenAIAgentAutoConfigurationTest.java index 0b155004..b902ab3f 100644 --- a/providers/google/src/test/java/ai/javaclaw/providers/google/genai/GoogleGenAIAgentAutoConfigurationTest.java +++ b/providers/google/src/test/java/ai/javaclaw/providers/google/genai/GoogleGenAIAgentAutoConfigurationTest.java @@ -6,10 +6,8 @@ import org.springframework.boot.test.context.runner.ApplicationContextRunner; import java.io.IOException; -import java.net.URL; -import java.nio.charset.StandardCharsets; -import java.util.List; +import static ai.javaclaw.testsupport.AutoConfigurationImportsTestSupport.importedAutoConfigurations; import static org.assertj.core.api.Assertions.assertThat; class GoogleGenAIAgentAutoConfigurationTest { @@ -27,18 +25,7 @@ void registersOnboardingProvider() { @Test void autoConfigurationIsRegisteredViaImportsFile() throws IOException { - assertThat(importedAutoConfigurations()) + assertThat(importedAutoConfigurations(GoogleGenAIAgentAutoConfigurationTest.class)) .contains(GoogleGenAIAgentAutoConfiguration.class.getName()); } - - static List importedAutoConfigurations() throws IOException { - URL resource = GoogleGenAIAgentAutoConfigurationTest.class.getClassLoader() - .getResource("META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports"); - try (var in = resource.openStream()) { - return new String(in.readAllBytes(), StandardCharsets.UTF_8).lines() - .map(String::trim) - .filter(line -> !line.isEmpty()) - .toList(); - } - } } \ No newline at end of file diff --git a/providers/ollama/build.gradle b/providers/ollama/build.gradle index 2a082562..0012f99a 100644 --- a/providers/ollama/build.gradle +++ b/providers/ollama/build.gradle @@ -8,4 +8,5 @@ dependencies { implementation 'org.springframework.ai:spring-ai-starter-model-ollama' testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation testFixtures(project(':base')) } diff --git a/providers/ollama/src/test/java/ai/javaclaw/providers/ollama/OllamaAgentAutoConfigurationTest.java b/providers/ollama/src/test/java/ai/javaclaw/providers/ollama/OllamaAgentAutoConfigurationTest.java index 355ebda8..0160a2c3 100644 --- a/providers/ollama/src/test/java/ai/javaclaw/providers/ollama/OllamaAgentAutoConfigurationTest.java +++ b/providers/ollama/src/test/java/ai/javaclaw/providers/ollama/OllamaAgentAutoConfigurationTest.java @@ -6,10 +6,8 @@ import org.springframework.boot.test.context.runner.ApplicationContextRunner; import java.io.IOException; -import java.net.URL; -import java.nio.charset.StandardCharsets; -import java.util.List; +import static ai.javaclaw.testsupport.AutoConfigurationImportsTestSupport.importedAutoConfigurations; import static org.assertj.core.api.Assertions.assertThat; class OllamaAgentAutoConfigurationTest { @@ -27,18 +25,7 @@ void registersOnboardingProvider() { @Test void autoConfigurationIsRegisteredViaImportsFile() throws IOException { - assertThat(importedAutoConfigurations()) + assertThat(importedAutoConfigurations(OllamaAgentAutoConfigurationTest.class)) .contains(OllamaAgentAutoConfiguration.class.getName()); } - - static List importedAutoConfigurations() throws IOException { - URL resource = OllamaAgentAutoConfigurationTest.class.getClassLoader() - .getResource("META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports"); - try (var in = resource.openStream()) { - return new String(in.readAllBytes(), StandardCharsets.UTF_8).lines() - .map(String::trim) - .filter(line -> !line.isEmpty()) - .toList(); - } - } } \ No newline at end of file diff --git a/providers/openai/build.gradle b/providers/openai/build.gradle index 3a4d1108..7e11f2ed 100644 --- a/providers/openai/build.gradle +++ b/providers/openai/build.gradle @@ -8,4 +8,5 @@ dependencies { implementation 'org.springframework.ai:spring-ai-starter-model-openai' testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation testFixtures(project(':base')) } diff --git a/providers/openai/src/test/java/ai/javaclaw/providers/openai/OpenAIAgentAutoConfigurationTest.java b/providers/openai/src/test/java/ai/javaclaw/providers/openai/OpenAIAgentAutoConfigurationTest.java index 1097914a..42588ae6 100644 --- a/providers/openai/src/test/java/ai/javaclaw/providers/openai/OpenAIAgentAutoConfigurationTest.java +++ b/providers/openai/src/test/java/ai/javaclaw/providers/openai/OpenAIAgentAutoConfigurationTest.java @@ -6,10 +6,8 @@ import org.springframework.boot.test.context.runner.ApplicationContextRunner; import java.io.IOException; -import java.net.URL; -import java.nio.charset.StandardCharsets; -import java.util.List; +import static ai.javaclaw.testsupport.AutoConfigurationImportsTestSupport.importedAutoConfigurations; import static org.assertj.core.api.Assertions.assertThat; class OpenAIAgentAutoConfigurationTest { @@ -27,18 +25,7 @@ void registersOnboardingProvider() { @Test void autoConfigurationIsRegisteredViaImportsFile() throws IOException { - assertThat(importedAutoConfigurations()) + assertThat(importedAutoConfigurations(OpenAIAgentAutoConfigurationTest.class)) .contains(OpenAIAgentAutoConfiguration.class.getName()); } - - static List importedAutoConfigurations() throws IOException { - URL resource = OpenAIAgentAutoConfigurationTest.class.getClassLoader() - .getResource("META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports"); - try (var in = resource.openStream()) { - return new String(in.readAllBytes(), StandardCharsets.UTF_8).lines() - .map(String::trim) - .filter(line -> !line.isEmpty()) - .toList(); - } - } } \ No newline at end of file