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/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/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/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/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..14a0eeeb --- /dev/null +++ b/providers/anthropic/src/main/java/ai/javaclaw/providers/anthropic/AnthropicAgentAutoConfiguration.java @@ -0,0 +1,16 @@ +package ai.javaclaw.providers.anthropic; + +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(); + } +} \ 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..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,14 +8,15 @@ 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; +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(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/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..0bf7ea51 --- /dev/null +++ b/providers/anthropic/src/test/java/ai/javaclaw/providers/anthropic/AnthropicAgentAutoConfigurationTest.java @@ -0,0 +1,32 @@ +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 static ai.javaclaw.testsupport.AutoConfigurationImportsTestSupport.importedAutoConfigurations; +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(AnthropicAgentAutoConfigurationTest.class)).contains( + AnthropicAgentAutoConfiguration.class.getName(), + AnthropticClaudeCodeConfiguration.class.getName()); + } +} \ 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/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..145f9c53 --- /dev/null +++ b/providers/google/src/main/java/ai/javaclaw/providers/google/genai/GoogleGenAIAgentAutoConfiguration.java @@ -0,0 +1,16 @@ +package ai.javaclaw.providers.google.genai; + +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(); + } +} \ 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..b902ab3f --- /dev/null +++ b/providers/google/src/test/java/ai/javaclaw/providers/google/genai/GoogleGenAIAgentAutoConfigurationTest.java @@ -0,0 +1,31 @@ +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 static ai.javaclaw.testsupport.AutoConfigurationImportsTestSupport.importedAutoConfigurations; +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(GoogleGenAIAgentAutoConfigurationTest.class)) + .contains(GoogleGenAIAgentAutoConfiguration.class.getName()); + } +} \ 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/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..1eb98bb0 --- /dev/null +++ b/providers/ollama/src/main/java/ai/javaclaw/providers/ollama/OllamaAgentAutoConfiguration.java @@ -0,0 +1,16 @@ +package ai.javaclaw.providers.ollama; + +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(); + } +} \ 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..0160a2c3 --- /dev/null +++ b/providers/ollama/src/test/java/ai/javaclaw/providers/ollama/OllamaAgentAutoConfigurationTest.java @@ -0,0 +1,31 @@ +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 static ai.javaclaw.testsupport.AutoConfigurationImportsTestSupport.importedAutoConfigurations; +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(OllamaAgentAutoConfigurationTest.class)) + .contains(OllamaAgentAutoConfiguration.class.getName()); + } +} \ 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/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..c21ae3ea --- /dev/null +++ b/providers/openai/src/main/java/ai/javaclaw/providers/openai/OpenAIAgentAutoConfiguration.java @@ -0,0 +1,16 @@ +package ai.javaclaw.providers.openai; + +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(); + } +} \ 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..42588ae6 --- /dev/null +++ b/providers/openai/src/test/java/ai/javaclaw/providers/openai/OpenAIAgentAutoConfigurationTest.java @@ -0,0 +1,31 @@ +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 static ai.javaclaw.testsupport.AutoConfigurationImportsTestSupport.importedAutoConfigurations; +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(OpenAIAgentAutoConfigurationTest.class)) + .contains(OpenAIAgentAutoConfiguration.class.getName()); + } +} \ No newline at end of file