// 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. apply plugin: 'java-test-fixtures' // 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' annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor' // 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. strictTestLanes { 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 } // Pinned server images. The design forbids `latest` for a certification lane (Task 44): a mutable // tag makes a red run unattributable. `-PmongoPrimaryImage=` / `-PmongoCompatibilityImage=` // override them for a one-off run. Closure applyMongoImageSelection = { task -> task.systemProperty 'mongodb.primary.image', (project.findProperty('mongoPrimaryImage') ?: 'mongo:8.0.16').toString() task.systemProperty 'mongodb.compatibility.image', (project.findProperty('mongoCompatibilityImage') ?: 'mongo:7.0.28').toString() task.systemProperty 'mongodb.toxiproxy.image', (project.findProperty('mongoToxiproxyImage') ?: 'ghcr.io/shopify/toxiproxy:2.12.0').toString() } // 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') { tag = 'mongodb-replicaset' description = 'Single-node replica set contract lane: mapping, atomic write, transaction, ' + 'change stream (design §29).' customize = { test -> applyMongoImageSelection(test) } } lane('mongoFailoverTest') { tag = 'mongodb-failover' description = 'Three-node replica set failover lane: primary kill, partition, unknown ' + 'commit, resume (design §29).' customize = { test -> applyMongoImageSelection(test) } } lane('mongoMigrationTest') { tag = 'mongodb-migration' description = 'Migration lane: empty / N-1 / oldest-supported snapshots, lock, checkpoint ' + 'restart (design §12).' customize = { test -> applyMongoImageSelection(test) } } lane('mongoCompatibilityTest') { tag = 'mongodb-compatibility' description = 'MongoDB 7.0 compatibility and 8.0 primary certification matrix (design §30).' customize = { test -> applyMongoImageSelection(test) } } lane('mongoSecurityIntegrationTest') { tag = 'mongodb-security-integration' description = 'RBAC, TLS, injection and redaction release gate against a real server ' + '(design §26).' customize = { test -> applyMongoImageSelection(test) } } 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') { sourceSet = 'mongoPerformanceTest' description = 'Certifies contention, aggregation spill, pagination and pool resource ' + 'bounds (design §29).' customize = { test -> applyMongoImageSelection(test) // Assertions on by default. They defaulted to false, so the lane measured numbers and // compared them to nothing — a performance gate whose bounds are never evaluated is a // report, and the release evidence called it a certification. test.systemProperty 'performance.assertions.enabled', (project.findProperty('performance.assertions.enabled') ?: 'true').toString() } } } // `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 tag exclusion above is a claim about two task configurations. This checks the claim against // what the two tasks actually ran, because the failure it prevents — every hermetic contract test // executing twice per `check` — is invisible in a green build and only shows up as time. tasks.register('verifyMongoTestLaneDisjointness') { group = 'verification' description = 'Fails when the unit lane and the stable contract lane execute the same test.' dependsOn 'test', 'mongoStableContractTest' def unitResults = layout.buildDirectory.dir('test-results/test') def contractResults = layout.buildDirectory.dir('test-results/mongoStableContractTest') inputs.dir(unitResults) inputs.dir(contractResults) outputs.file(layout.buildDirectory.file('reports/mongo-test-lane-disjointness.txt')) doLast { def executed = { java.io.File directory -> def names = [] as Set directory.listFiles({ File file -> file.name.endsWith('.xml') } as FileFilter) ?.each { file -> new groovy.xml.XmlParser().parse(file).testcase.each { testcase -> names << "${testcase.@classname}#${testcase.@name}".toString() } } names } def unit = executed(unitResults.get().asFile) def contract = executed(contractResults.get().asFile) def overlap = unit.intersect(contract) if (!overlap.isEmpty()) { throw new GradleException( "${overlap.size()} tests run in both the unit lane and the stable contract lane, " + "so `check` executes them twice: ${overlap.take(5)}") } def report = outputs.files.singleFile report.parentFile.mkdirs() report.text = "unit=${unit.size()} contract=${contract.size()} overlap=0\n" } } // Splitting the two lanes moved every tagged contract out of `test`, and the release manifest kept // naming the lane it had left. `MongoReleaseEvidenceVerifier` resolves // `test-results//TEST-.xml`, so a contract whose class now runs somewhere else // resolves to a file that will never exist: the Stable gate reports the transaction retry // invariants as evidence the run failed to produce, for a suite that ran them. // // Checked against the XML the lanes wrote rather than against a tag table, because a tag table here // would be a second copy of the selection above, and the copy is what drifted the first time. tasks.register('verifyMongoReleaseContractLanes') { group = 'verification' description = 'Fails when a blocking release contract names a lane that did not run its class.' dependsOn 'test', 'mongoStableContractTest' def manifest = rootProject.file('../src/config/mongodb/release-contracts.json') def hermeticLanes = ['test', 'mongoStableContractTest'] def resultsRoot = layout.buildDirectory.dir('test-results') inputs.file(manifest) inputs.dir(resultsRoot) outputs.file(layout.buildDirectory.file('reports/mongo-release-contract-lanes.txt')) doLast { def contracts = new groovy.json.JsonSlurper().parse(manifest).contracts def checked = [] def wrongLane = [] contracts.findAll { hermeticLanes.contains(it.task) }.each { contract -> def results = resultsRoot.get().dir(contract.task).file( "TEST-${contract.className}.xml").asFile if (!results.isFile()) { wrongLane << "${contract.id} names lane '${contract.task}', which did not run " + "${contract.className}" return } def suite = new groovy.xml.XmlParser().parse(results) int executed = (suite.@tests as int) - (suite.@skipped as int) if (executed < contract.minimumExecuted) { wrongLane << "${contract.id} requires ${contract.minimumExecuted} executed test(s) " + "in '${contract.task}' and the lane ran ${executed}" } checked << contract.id } if (!wrongLane.isEmpty()) { throw new GradleException( 'the Mongo release manifest points at lanes that cannot produce its evidence: ' + wrongLane.join('; ')) } def report = outputs.files.singleFile report.parentFile.mkdirs() report.text = "hermetic release contracts verified: ${checked.join(', ')}\n" } } // 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.', ] }