plugins { id 'ca.spring-library' id 'ca.spring-config' id 'java-test-fixtures' id 'ca.auxiliary-source-set' id 'ca.mongo-verification' } // Shared test code as a Gradle test-fixtures variant — ADR-BUILD-001. Applied here rather than // from the root, the way the GraphQL leaf does: only a leaf that has shared test code needs it. // MongoDB Document Persistence Platform leaf — see // docs/superpowers/specs/2026-08-11-mongodb-document-persistence-platform-design.md (design package) // and docs/mongodb/repository-adaptation.md (how the design's 19 stable + 12 advanced library // modules map here). // // The design models the platform as 19 Stable and 12 Advanced Gradle modules under // `modules/mongodb` and `modules/mongodb-advanced`. This repository's fail-closed module registry // (src/config/architecture/modules.json) outranks that layout, so the module boundaries are // packages under dev.caskeleton.adapter.outbound.mongo. MongoModuleBoundaryTest holds a closed // edge matrix — every package and what it may import — compares it against the tree for exact // equality, and rejects any observed edge that is not declared. // // Driver and Spring Data MongoDB versions come from the Spring Boot BOM applied to every module in // src/build.gradle (design §4: "개별 Driver 버전 override 금지"), so nothing here pins them. description = 'Outbound adapter: MongoDB document persistence platform (manifests, atomic writes, ' + 'consistency profiles, guardrails, change streams)' dependencies { // D1/D2 imperative execution path and the mapping subsystem. implementation 'org.springframework.boot:spring-boot-starter-data-mongodb' // D1/D2 reactive execution path: reactive template, cursors and change streams (design §20). implementation 'org.springframework.boot:spring-boot-starter-data-mongodb-reactive' // The starter package registers auto-configuration and binds typed properties. implementation 'org.springframework.boot:spring-boot-autoconfigure' // Driver-native observability conventions (design §27) publish through Micrometer. implementation 'io.micrometer:micrometer-core' implementation 'org.slf4j:slf4j-api' // The design's module dependency table is enforced as package rules, so ArchUnit is what keeps // "packages instead of modules" from meaning "no boundary at all". testImplementation libs.archunit.junit5 testImplementation 'io.projectreactor:reactor-test' // Real replica set / failover / migration lanes (design §29). Test-scoped so no production // package can reach a container fixture. testImplementation 'org.testcontainers:testcontainers' testImplementation 'org.testcontainers:testcontainers-junit-jupiter' testImplementation 'org.testcontainers:testcontainers-mongodb' testImplementation 'org.testcontainers:testcontainers-toxiproxy' } // Shared test code lives in src/testFixtures, per ADR-BUILD-001. This leaf is the first migration // off `ca.testkit-publisher`: one leaf, two lanes, no cross-module consumer — the proof that the // path works before the published testkits follow. // // `java-test-fixtures` creates the source set and puts it on `test`'s classpath itself, so neither a // `sourceSet('testFixtures')` declaration nor a `testFixturesPublisher { consumedBy 'test' }` is needed. // The performance lane is not `test` and still has to say it consumes the fixtures, which is what // `compilesAgainst` is for — it looks a source set up rather than creating one, so naming a // plugin-created set works unchanged. // // What is given up: the convention's opt-in publishing. This leaf's fixtures are now a consumable // variant whether or not anybody asks for them. ADR-BUILD-001 records that as an accepted loss — // unconsumed fixtures are unconsumed, and a leaf that genuinely must not offer them needs a module, // not a third convention. auxiliarySourceSets { sourceSet('mongoPerformanceTest') { compilesAgainst 'main', 'testFixtures' } } // The fixtures declare what they compile against, rather than inheriting the test configuration. // // The `testkit` source set used to extend `testImplementation`, so it silently saw every test // library this leaf declared. That is convenient and it is also how a fixture acquires a dependency // nobody chose for it. Listing them here is three more lines and makes the fixtures' own surface // reviewable — the same model the GraphQL leaf already uses. dependencies { testFixturesImplementation libs.archunit.junit5 testFixturesImplementation 'io.projectreactor:reactor-test' testFixturesImplementation 'org.assertj:assertj-core' testFixturesImplementation 'org.mongodb:bson' // The ArchUnit rule pack names Spring Data's Repository marker in a rule, so it needs the // type on its compile classpath even though it never calls it. testFixturesImplementation 'org.springframework.data:spring-data-commons' testFixturesImplementation 'org.testcontainers:testcontainers' testFixturesImplementation 'org.testcontainers:testcontainers-junit-jupiter' testFixturesImplementation 'org.testcontainers:testcontainers-mongodb' testFixturesImplementation 'org.testcontainers:testcontainers-toxiproxy' testFixturesImplementation libs.toxiproxy.java } // Docker-backed lanes are excluded from the default unit run: they fail closed without Docker, and // a `check` that fails on a laptop without Docker teaches people to skip `check`. // // `mongodb-contract` is excluded here too. It was not, and `check` depends on both `test` and // `mongoStableContractTest`, so every one of the 382 hermetic contract tests ran twice on a fresh // build — once in each task. The two lanes are now disjoint by construction, and // `MongoTestLaneDisjointnessTest` asserts it against the JUnit XML rather than trusting this // comment. tasks.named('test', Test) { useJUnitPlatform { excludeTags 'mongodb-contract', 'mongodb-replicaset', 'mongodb-failover', 'mongodb-migration', 'mongodb-compatibility', 'mongodb-security-integration' } } // Six lanes, declared rather than assembled. `ca.strict-test-lane` owns testClassesDirs, classpath, // tag selection, failOnNoDiscoveredTests and the up-to-date refusal — the five lines that used to be // copied once per lane here and again in four other leaves. strictTestLanes { lane('mongoReplicaSetTest') { integration() tag = 'mongodb-replicaset' description = 'Single-node replica set contract lane: mapping, atomic write, transaction, ' + 'change stream (design §29).' systemProperty 'mongodb.primary.image', providers.gradleProperty('mongoPrimaryImage').orElse('mongo:8.0.16').get() systemProperty 'mongodb.compatibility.image', providers.gradleProperty('mongoCompatibilityImage').orElse('mongo:7.0.28').get() systemProperty 'mongodb.toxiproxy.image', providers.gradleProperty('mongoToxiproxyImage').orElse('ghcr.io/shopify/toxiproxy:2.12.0').get() } lane('mongoFailoverTest') { integration() tag = 'mongodb-failover' description = 'Three-node replica set failover lane: primary kill, partition, unknown ' + 'commit, resume (design §29).' systemProperty 'mongodb.primary.image', providers.gradleProperty('mongoPrimaryImage').orElse('mongo:8.0.16').get() systemProperty 'mongodb.compatibility.image', providers.gradleProperty('mongoCompatibilityImage').orElse('mongo:7.0.28').get() systemProperty 'mongodb.toxiproxy.image', providers.gradleProperty('mongoToxiproxyImage').orElse('ghcr.io/shopify/toxiproxy:2.12.0').get() } lane('mongoMigrationTest') { integration() tag = 'mongodb-migration' description = 'Migration lane: empty / N-1 / oldest-supported snapshots, lock, checkpoint ' + 'restart (design §12).' systemProperty 'mongodb.primary.image', providers.gradleProperty('mongoPrimaryImage').orElse('mongo:8.0.16').get() systemProperty 'mongodb.compatibility.image', providers.gradleProperty('mongoCompatibilityImage').orElse('mongo:7.0.28').get() systemProperty 'mongodb.toxiproxy.image', providers.gradleProperty('mongoToxiproxyImage').orElse('ghcr.io/shopify/toxiproxy:2.12.0').get() } lane('mongoCompatibilityTest') { integration() tag = 'mongodb-compatibility' description = 'MongoDB 7.0 compatibility and 8.0 primary certification matrix (design §30).' systemProperty 'mongodb.primary.image', providers.gradleProperty('mongoPrimaryImage').orElse('mongo:8.0.16').get() systemProperty 'mongodb.compatibility.image', providers.gradleProperty('mongoCompatibilityImage').orElse('mongo:7.0.28').get() systemProperty 'mongodb.toxiproxy.image', providers.gradleProperty('mongoToxiproxyImage').orElse('ghcr.io/shopify/toxiproxy:2.12.0').get() } lane('mongoSecurityIntegrationTest') { integration() tag = 'mongodb-security-integration' description = 'RBAC, TLS, injection and redaction release gate against a real server ' + '(design §26).' systemProperty 'mongodb.primary.image', providers.gradleProperty('mongoPrimaryImage').orElse('mongo:8.0.16').get() systemProperty 'mongodb.compatibility.image', providers.gradleProperty('mongoCompatibilityImage').orElse('mongo:7.0.28').get() systemProperty 'mongodb.toxiproxy.image', providers.gradleProperty('mongoToxiproxyImage').orElse('ghcr.io/shopify/toxiproxy:2.12.0').get() } lane('mongoStableContractTest') { tag = 'mongodb-contract' description = 'Hermetic stable contract suite: manifests, guardrails, retry scopes, ' + 'redaction (design §30).' } // Driven by its own source set rather than a tag: for this shape the source set is the // selection, so the convention asks for no tag. lane('mongoPerformanceTest') { performance() sourceSet = 'mongoPerformanceTest' description = 'Certifies contention, aggregation spill, pagination and pool resource ' + 'bounds (design §29).' systemProperty 'mongodb.primary.image', providers.gradleProperty('mongoPrimaryImage').orElse('mongo:8.0.16').get() systemProperty 'mongodb.compatibility.image', providers.gradleProperty('mongoCompatibilityImage').orElse('mongo:7.0.28').get() systemProperty 'mongodb.toxiproxy.image', providers.gradleProperty('mongoToxiproxyImage').orElse('ghcr.io/shopify/toxiproxy:2.12.0').get() // Assertions on by default: a performance gate without bounds is only a report. systemProperty 'performance.assertions.enabled', providers.gradleProperty('performance.assertions.enabled').orElse('true').get() } } // `check` gains only the hermetic lanes. The Docker-backed ones stay opt-in for the reason above. tasks.named('check') { dependsOn 'mongoStableContractTest', 'verifyMongoTestLaneDisjointness', 'verifyMongoReleaseContractLanes' } // The two verification tasks are registered by `ca.mongo-verification`. The leaf owns only // the semantic lane selection above and the lifecycle dependency below; JSON/JUnit parsing and // evidence decisions live in typed Java build tooling. // verifyMongoApiSurface — every public type this leaf exposes is a committed decision. // // 311 of this leaf's 313 production files declare a public top-level type. One jar means `public` // is public to every adopter, so the intended split between contract and implementation — `api` is // the surface, the rest is how it is built — is a convention the compiler does not know about. // // The full move of implementation packages under an `internal` root is a separate, mechanical // change; this is what keeps the surface from growing while that is pending. A snapshot does not // shrink anything. It makes each addition a decision somebody made in review rather than something // discovered later by an adopter who imported it. // // A snapshot does not shrink the surface. It makes each addition visible in review, which is the // prerequisite for shrinking it: the `api` and `spi` packages are the surface an adopter is meant // to use, and everything else in this file is a candidate for becoming internal when the leaf is // split into capability artifacts. Until then the number cannot grow by accident. apiSurface { label = 'Mongo' baseline = rootProject.file('../docs/architecture/mongo-api-surface.txt') description = 'MongoDB leaf public API surface — every public top-level type in src/main/java.' rationale = [ 'A public type in a single-jar leaf is reachable from every adopter\'s code, so', 'additions are reviewed rather than discovered. `api` is the intended external', 'surface; the rest is implementation that has not been moved under an internal', 'root yet.', ] }