docs: 빌드·CI 감사 계획과 진행 중이던 문서 정리

- CI 단계 분리 계획 추가 (docs/superpowers/plans/2026-09-16-ci-stage-separation.md).
  빌드·CI 레이어 전수 리뷰 133건의 결론과 Track A/B/C 작업 순서를 담는다.
- public-path 보안 기준선을 실제 배포 기본값(/v1/healthcheck)으로 재생성.
  이전 값은 gitignore 된 src/.env 에서 유래해 재현이 불가능했다.
- 진행 중이던 ADR·리뷰·테스트 전략 문서 반영, 대체된 grpc 계획 문서 제거.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
DongHyeonka
2026-09-16 16:52:37 +09:00
co-authored by Claude Opus 5
parent 21234e38cd
commit 2a8d34f85c
14 changed files with 5313 additions and 6930 deletions
@@ -0,0 +1,119 @@
# ADR-BUILD-001: `java-test-fixtures` is the standard for shared test code
- Status: Accepted
- Date: 2026-09-07
- Scope: every leaf that publishes or consumes shared test code
- Source: `docs/reviews/2026-09-07-app-bootstrap-module-code-review.md` BOOT-015
## Context
Two conventions do the same job in this repository.
`ca.testkit-publisher` — a convention plugin — gives a leaf a `testkit` source set, wires its output
onto the lanes that leaf names, and optionally publishes it as a consumable configuration. Five
leaves use it: `persistence-jpa` (published as `jpaTestkit`), `web` (`webTestkit`), `websocket`
(`websocketTestkit`), `persistence-mongo` and `httpclient` (both unpublished).
`java-test-fixtures` — Gradle's own plugin — gives a leaf a `testFixtures` source set, puts it on
`test`'s classpath automatically, and always publishes it as a variant consumers reach with
`testFixtures(project(':x'))`. One leaf uses it: `graphql`, which additionally fails its build when a
fixture is written outside `src/testFixtures/java`.
Two conventions for one purpose is the defect. A contributor adding shared test code has to know
which leaf they are in before they know where the file goes, and the two answers are not
interchangeable: a consumer of the first writes `project(path: ':x', configuration: 'jpaTestkit')`
and has to know the configuration's name, while a consumer of the second writes
`testFixtures(project(':x'))` and does not.
## Decision
**`java-test-fixtures` is the standard.** New shared test code goes in `src/testFixtures/java`, and a
consumer depends on it with `testFixtures(project(':x'))`.
Three reasons, in order of weight:
1. **The consumer side describes itself.** `testFixtures(project(':adapter:inbound:web'))` says what
it is. `project(path: ':adapter:inbound:web', configuration: 'webTestkit')` says where to look,
and only after the reader has learned that `webTestkit` is a testkit rather than a lane.
2. **The enforcement already exists and is copyable.** `graphql`'s build fails when a fixture is
declared in the wrong place. The same guard applies unchanged to any leaf that adopts the plugin.
3. **It is one fewer local concept.** A convention plugin that reimplements a Gradle plugin has to be
maintained against it.
## What the local plugin does better, and how it is replaced
This is worth writing down, because the review that prompted this ADR recommended the migration
before reading `ca.testkit-publisher`, and the plugin turns out to encode two deliberate decisions
rather than being an oversight.
**Publishing is opt-in.** `persistence-mongo` and `httpclient` have a testkit and publish nothing;
`persistence-jpa` publishes. The plugin's own comment names this as "a real difference in what each
leaf offers rather than an oversight to normalise away". `java-test-fixtures` always creates the
variant, so the distinction is lost — a leaf that never meant to offer its fixtures will offer them.
> Replacement: none at the build level. The distinction moves to review: the fixtures of a leaf that
> nobody consumes are simply unconsumed. This is a real, accepted loss.
**Lane consumption is declared.** `persistence-jpa` says `consumedBy 'test', 'postgresqlIntegrationTest'`.
`java-test-fixtures` puts fixtures on `test` only, so every other lane needs the output added
explicitly.
> Replacement: `strictTestLanes`' existing `compilesAgainst` expresses this unchanged — a lane
> declares `compilesAgainst 'main', 'testFixtures'`. The first draft of this ADR assumed the DSL
> would need a change, because `sourceSet(name)` creates what it is given and `testFixtures` already
> exists. The `persistence-mongo` migration showed otherwise: `compilesAgainst` only *looks a source
> set up*, so naming a plugin-created one works as-is. What the leaf drops is the
> `sourceSet('testkit')` declaration, not the lane's.
## Migration: done, and what it cost
Five leaves, eleven lanes, two published testkits, all migrated leaf by leaf with the suite run
between each. `ca.testkit-publisher` is deleted.
The order was chosen so a mistake would be cheap: unpublished leaves first, published ones last with
their consumer in the same step.
1. `persistence-mongo` — one leaf, two lanes, no cross-module consumer; the proof the path works.
What it took, per leaf:
- `apply plugin: 'java-test-fixtures'` at the top of the leaf build file;
- `git mv src/testkit src/testFixtures`;
- drop `sourceSet('testkit')` and the whole `testkitPublisher` block; keep every other lane's
`compilesAgainst`, renaming `'testkit'` to `'testFixtures'`;
- rename `testkitImplementation` to `testFixturesImplementation`, **and add what the old source
set was inheriting silently**. This is the one non-mechanical step: `testkit*` extended
`testImplementation`, so the fixtures saw every test library the leaf declared. Mongo's needed
four more lines (AssertJ, BSON, Spring Data commons, Toxiproxy) — none of which the leaf had
ever stated the fixtures depended on;
- regenerate the leaf's lock state.
2. `httpclient`, then `websocket` — unpublished as well, more lanes.
3. `web` and `persistence-jpa` with `app-bootstrap`'s two consumer declarations, which became
`testImplementation(testFixtures(project(':…')))`.
4. `ca.testkit-publisher` deleted, along with its `plugins {}` entry and its application in the root
build.
### Two things the migration broke, and what they taught
Both were caught by tests that exist to catch exactly this, which is the argument for having them.
**ArchUnit corpora went wrong in opposite directions.** `httpclient`'s boundary rules *excluded*
`build/classes/java/testkit`; after the move the fixtures arrived as a `…-test-fixtures.jar` on the
same classpath, so the exclusion missed them and 258 fixture-to-fixture calls were reported as
production depending on the testkit. `persistence-jpa`'s rules *included* only
`build/classes/java/main`; applying `java-test-fixtures` makes the module's own test classpath carry
the module as a **jar** rather than as a class directory, so its corpus became empty. The second is
the dangerous one — an empty corpus makes every `noClasses()` rule pass — and it surfaced only
because that suite asserts its corpus is non-empty before asserting anything about it.
**Fixtures had invisible dependencies.** `testkit*` configurations extended `testImplementation`, so
the fixtures compiled against every test library their leaf declared without ever naming one. Making
them explicit took roughly thirty `testFixturesImplementation` lines across the five leaves —
Micrometer, Spring Web, Netty, logback, Jackson, JUnit, AssertJ, Spring Data. None of them were
wrong; none of them were stated.
## Consequences
- `docs/testing/TESTING_STRATEGY.md` §5 records the standard; this ADR records why and at what cost.
- Until step 5, two conventions remain visible. The strategy document says so explicitly, so a
contributor reading it is not left to infer which one is current.
- The opt-in-publishing distinction is given up. If it later proves load-bearing — a leaf whose
fixtures genuinely must not be reachable — the answer is a separate module, not a third convention.