|
| 1 | +# Behaviors: Multi-module restructuring |
| 2 | + |
| 3 | +These scenarios are largely **build- and wiring-level** (Maven reactor, classpath composition, Spring |
| 4 | +auto-configuration) rather than HTTP request/response behaviors. They are written so each can be |
| 5 | +turned into a reactor build assertion, a Maven dependency/classpath check, or a Spring integration |
| 6 | +test (`ApplicationContextRunner` / `@SpringBootTest`). |
| 7 | + |
| 8 | +## Reactor build |
| 9 | + |
| 10 | +### Reactor builds all modules |
| 11 | + |
| 12 | +- **Given** the multi-module reactor (`spring-services` parent + `core`, `slack`, `mcp`, `email`, |
| 13 | + `search`, `dbbackup`, `all`, `bom`) |
| 14 | +- **When** `mvn verify` runs at the reactor root |
| 15 | +- **Then** every module builds and its tests pass, and the build produces a jar for each non-pom |
| 16 | + module plus pom artifacts for the parent and the BOM |
| 17 | + |
| 18 | +### Parent coordinate is no longer a consumable jar |
| 19 | + |
| 20 | +- **Given** the repurposed `com.open-elements:spring-services` coordinate |
| 21 | +- **When** its packaging is inspected |
| 22 | +- **Then** it is `pom` (reactor parent) and produces no class-bearing jar |
| 23 | + |
| 24 | +### Lockstep version across all modules |
| 25 | + |
| 26 | +- **Given** the reactor after a global version change via `versions:set` |
| 27 | +- **When** the version of each module and the BOM is read |
| 28 | +- **Then** all modules and the BOM carry the identical version, and each module references the parent |
| 29 | + version explicitly |
| 30 | + |
| 31 | +## Dependency isolation |
| 32 | + |
| 33 | +### Core does not pull optional heavy dependencies |
| 34 | + |
| 35 | +- **Given** `spring-services-core` only |
| 36 | +- **When** its resolved transitive runtime classpath is computed |
| 37 | +- **Then** it contains none of `slack-api-client`, the MCP SDK artifacts, or `spring-boot-starter-mail` |
| 38 | + |
| 39 | +### À-la-carte consumer pulls only chosen features |
| 40 | + |
| 41 | +- **Given** a consumer depending on `spring-services-core` + `spring-services-slack` only |
| 42 | +- **When** the consumer's transitive classpath is computed |
| 43 | +- **Then** `slack-api-client` is present, but the MCP SDK, mail, and the search/dbbackup modules are |
| 44 | + absent |
| 45 | + |
| 46 | +### BOM resolves all module versions from one import |
| 47 | + |
| 48 | +- **Given** a consumer that imports `spring-services-bom` (scope `import`) and declares |
| 49 | + `spring-services-core` and `spring-services-mcp` **without** versions |
| 50 | +- **When** dependency resolution runs |
| 51 | +- **Then** both modules resolve at the BOM's lockstep version |
| 52 | + |
| 53 | +### all-module brings the complete set |
| 54 | + |
| 55 | +- **Given** a consumer depending only on `spring-services-all` |
| 56 | +- **When** its transitive classpath is computed |
| 57 | +- **Then** every spring-services module and every feature dependency (slack, mcp, mail, …) is present |
| 58 | + |
| 59 | +## Auto-configuration activation |
| 60 | + |
| 61 | +### Full classpath activates every feature |
| 62 | + |
| 63 | +- **Given** an application context with `spring-services-all` on the classpath and all feature |
| 64 | + properties enabled |
| 65 | +- **When** the context starts |
| 66 | +- **Then** the core beans and every optional feature's beans (Slack, MCP, email, search, db-backup) |
| 67 | + are present — equivalent to today's `@Import(FullSpringServiceConfig)` behavior |
| 68 | + |
| 69 | +### Absent optional module does not break startup |
| 70 | + |
| 71 | +- **Given** an application with `spring-services-core` only (no optional modules) |
| 72 | +- **When** the context starts |
| 73 | +- **Then** it starts successfully, exposes the core beans, and no `NoClassDefFoundError` or missing-bean |
| 74 | + failure occurs for any absent optional feature |
| 75 | + |
| 76 | +### Optional module self-activates by classpath presence |
| 77 | + |
| 78 | +- **Given** an application with `spring-services-core` + `spring-services-mcp` on the classpath and |
| 79 | + `openelements.mcp.enabled=true` |
| 80 | +- **When** the context starts |
| 81 | +- **Then** the MCP auto-configuration activates and the MCP server beans are present, without the |
| 82 | + application declaring any `@Import` |
| 83 | + |
| 84 | +### Optional module stays inert when its class is missing |
| 85 | + |
| 86 | +- **Given** the `spring-services-mcp` auto-configuration class on the classpath but the MCP SDK type |
| 87 | + it guards on absent |
| 88 | +- **When** the context starts (simulated via `ApplicationContextRunner` without the SDK) |
| 89 | +- **Then** the `@ConditionalOnClass` guard prevents activation and no MCP bean is created |
| 90 | + |
| 91 | +### Per-feature property toggle still disables a present module |
| 92 | + |
| 93 | +- **Given** an application with `spring-services-mcp` present but `openelements.mcp.enabled=false` |
| 94 | +- **When** the context starts |
| 95 | +- **Then** the MCP endpoint and beans are not registered (existing `@ConditionalOnProperty` behavior is |
| 96 | + preserved) |
| 97 | + |
| 98 | +### all-module ships no aggregate configuration |
| 99 | + |
| 100 | +- **Given** `spring-services-all` |
| 101 | +- **When** its artifact contents are inspected |
| 102 | +- **Then** it contains no `@Configuration`/`@AutoConfiguration` class and no |
| 103 | + `AutoConfiguration.imports` of its own — it is a pure dependency bundle |
| 104 | + |
| 105 | +## Persistence unit (carried over from spec 013) |
| 106 | + |
| 107 | +### Single persistence unit resolves all entities |
| 108 | + |
| 109 | +- **Given** an application with `spring-services-core` (entities in `oe_spring_services`) and its own |
| 110 | + `@Entity` in a separate package, with no `@Import`/`@EntityScan`/`@EnableJpaRepositories` |
| 111 | +- **When** the context starts |
| 112 | +- **Then** both the application's and the library's entities and Spring Data repositories resolve under |
| 113 | + one `EntityManagerFactory` / one `TransactionManager` |
| 114 | + |
| 115 | +### Library-internal foreign keys and transactions still work |
| 116 | + |
| 117 | +- **Given** the core module against a Testcontainers PostgreSQL |
| 118 | +- **When** an operation writes across library-internal associations (e.g. `audit_log.user_id → users.id`) |
| 119 | + within one transaction |
| 120 | +- **Then** the FK holds and the transaction commits atomically |
| 121 | + |
| 122 | +### Entity schema-guard test lives in core and covers all entities |
| 123 | + |
| 124 | +- **Given** the schema-convention guard test in `spring-services-core` |
| 125 | +- **When** it scans the classpath for `@Entity` under `com.openelements.spring.base` |
| 126 | +- **Then** it finds all seven entities and asserts each carries `@Table(schema = DbSchema.NAME)`; a new |
| 127 | + entity without the schema fails the build |
| 128 | + |
| 129 | +### No optional module contributes an entity |
| 130 | + |
| 131 | +- **Given** any optional feature module on the classpath |
| 132 | +- **When** the classpath is scanned for `@Entity` types contributed by that module |
| 133 | +- **Then** none are found (all entities live in core) |
| 134 | + |
| 135 | +## Aggregate / starter integration (carried over from spec 013, relocated) |
| 136 | + |
| 137 | +### Zero-config starter test runs against the full classpath |
| 138 | + |
| 139 | +- **Given** the aggregate test in `spring-services-all` with a test app declaring its own `@Entity` + |
| 140 | + repository in a separate package, no `@Import`/`@EntityScan`/`@EnableJpaRepositories` |
| 141 | +- **When** the context starts with the full reactor on the classpath |
| 142 | +- **Then** the app's own and the library's entities/repositories resolve, and representative |
| 143 | + optional-feature beans (e.g. a Slack and an MCP bean) are present |
| 144 | + |
| 145 | +## Consumer migration (breaking change) |
| 146 | + |
| 147 | +### Old coordinate no longer compiles |
| 148 | + |
| 149 | +- **Given** an existing consumer depending on `com.open-elements:spring-services` (the old jar) |
| 150 | +- **When** it builds against the new release |
| 151 | +- **Then** resolution yields the reactor parent pom (no classes) and the consumer's compilation fails — |
| 152 | + signalling the required migration |
| 153 | + |
| 154 | +### Migration to all-module restores behavior |
| 155 | + |
| 156 | +- **Given** that same consumer switched to `com.open-elements:spring-services-all` |
| 157 | +- **When** it builds and starts |
| 158 | +- **Then** all previously available features are present and behavior matches the pre-split release |
| 159 | + |
| 160 | +### Explicit-wiring consumer wires only core features |
| 161 | + |
| 162 | +- **Given** a consumer on `spring-services-core` using `@Import(FullSpringServiceConfig.class)` |
| 163 | +- **When** the context starts |
| 164 | +- **Then** only the core feature beans are wired; optional features require adding their module or |
| 165 | + importing their feature config explicitly |
0 commit comments