diff --git a/app/build.gradle b/app/build.gradle index 22e2ce0..b05b3d6 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -34,6 +34,8 @@ dependencies { testImplementation 'org.springframework.boot:spring-boot-starter-websocket-test' testImplementation 'org.springframework.boot:spring-boot-testcontainers' testImplementation 'org.testcontainers:testcontainers-junit-jupiter' + testImplementation libs.spring.modulith.starter.test + testImplementation libs.archunit.junit5 } bootRun { diff --git a/app/src/test/java/ai/javaclaw/ModularityTests.java b/app/src/test/java/ai/javaclaw/ModularityTests.java new file mode 100644 index 0000000..d82b593 --- /dev/null +++ b/app/src/test/java/ai/javaclaw/ModularityTests.java @@ -0,0 +1,14 @@ +package ai.javaclaw; + +import org.junit.jupiter.api.Test; +import org.springframework.modulith.core.ApplicationModules; + +class ModularityTests { + + ApplicationModules modules = ApplicationModules.of(JavaClawApplication.class); + + @Test + void verifiesModularStructure() { + modules.verify(); + } +} \ No newline at end of file diff --git a/app/src/test/java/ai/javaclaw/ModuleBoundaryArchTest.java b/app/src/test/java/ai/javaclaw/ModuleBoundaryArchTest.java new file mode 100644 index 0000000..7d76a18 --- /dev/null +++ b/app/src/test/java/ai/javaclaw/ModuleBoundaryArchTest.java @@ -0,0 +1,56 @@ +package ai.javaclaw; + +import com.tngtech.archunit.core.importer.ImportOption; +import com.tngtech.archunit.junit.AnalyzeClasses; +import com.tngtech.archunit.junit.ArchTest; +import com.tngtech.archunit.lang.ArchRule; + +import java.util.stream.Stream; + +import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses; +import static com.tngtech.archunit.library.dependencies.SlicesRuleDefinition.slices; + +@AnalyzeClasses(packages = "ai.javaclaw", importOptions = ImportOption.DoNotIncludeTests.class) +class ModuleBoundaryArchTest { + + private static final String[] CORE_PACKAGES = { + "ai.javaclaw.agent..", "ai.javaclaw.tasks..", "ai.javaclaw.configuration..", "ai.javaclaw.files.." + }; + + private static final String[] PLUGIN_PACKAGES = { + "ai.javaclaw.channels.*..", "ai.javaclaw.tools.*.." + }; + + private static final String[] PROVIDER_PACKAGES = { + "ai.javaclaw.providers.." + }; + + @ArchTest + static final ArchRule channelPluginsAreIndependent = + slices().matching("..channels.(*)..").should().notDependOnEachOther(); + + @ArchTest + static final ArchRule toolPluginsAreIndependent = + slices().matching("..tools.(*)..").should().notDependOnEachOther(); + + @ArchTest + static final ArchRule llmProvidersAreIndependent = + slices().matching("..providers.(*)..").should().notDependOnEachOther(); + + @ArchTest + static final ArchRule providersDoNotDependOnPlugins = + noClasses().that().resideInAnyPackage(PROVIDER_PACKAGES) + .should().dependOnClassesThat().resideInAnyPackage(PLUGIN_PACKAGES); + + @ArchTest + static final ArchRule pluginsDoNotDependOnProviders = + noClasses().that().resideInAnyPackage(PLUGIN_PACKAGES) + .should().dependOnClassesThat().resideInAnyPackage(PROVIDER_PACKAGES); + + @ArchTest + static final ArchRule coreDoesNotDependOnPluginsOrProviders = + noClasses().that().resideInAnyPackage(CORE_PACKAGES) + .should().dependOnClassesThat().resideInAnyPackage( + Stream.concat(Stream.of(PLUGIN_PACKAGES), Stream.of(PROVIDER_PACKAGES)) + .toArray(String[]::new)); +} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f1e8d63..cdc19d4 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,6 +3,7 @@ spring-boot = "4.0.6" spring-ai = "2.0.0" telegrambots = "9.5.0" commonmark = "0.28.0" +archunit = "1.4.2" [libraries] bom-spring-boot = { module = "org.springframework.boot:spring-boot-dependencies", version.ref = "spring-boot" } @@ -10,6 +11,8 @@ bom-spring-ai = { module = "org.springframework.ai:spring-ai-bom", version.ref = bom-spring-modulith = { module = "org.springframework.modulith:spring-modulith-bom", version = "2.0.6" } spring-ai-agent-utils = { module = "org.springaicommunity:spring-ai-agent-utils", version = "0.9.0" } spring-ai-lucene = { module = "org.apache.lucene:lucene-core", version = "9.12.3" } +spring-modulith-starter-test = { module = "org.springframework.modulith:spring-modulith-starter-test" } +archunit-junit5 = { module = "com.tngtech.archunit:archunit-junit5", version.ref = "archunit" } netty-resolver-dns-native-macos = { module = "io.netty:netty-resolver-dns-native-macos", version = "4.2.12.Final" } jobrunr-spring-starter = { module = "org.jobrunr:jobrunr-spring-boot-4-starter", version = "8.6.1" }