--- title: error / archunit-importpackages-empty-vacuous-stale-build-2026-06-20 source_type: error-note status: raw related_branches: [feature-test-taxonomy-fixture-contract] related_projects: [ca-skeleton] tags: [error, ca-skeleton, archunit, testing, flaky-test, vacuous-pass, importpackages, classloader] created: 2026-06-20 status_label: resolved --- # error: archunit-importpackages-empty-vacuous-stale-build-2026-06-20 > Layer: `raw/errors/` — 작업 중 마주친 단일 실패·트러블슈팅 기록. 원본은 raw에 영구 보관한다. ## Parent / 부모 - [[raw/branch-notes/feature-test-taxonomy-fixture-contract]] — Task 2/3/4 의 `TestTaxonomyArchitectureTest` 가 전체 스위트에서 비결정적으로 실패한 사건. ## 증상 / Symptom `TestTaxonomyArchitectureTest` 는 단독(`--tests '*TestTaxonomyArchitectureTest'`) 실행 시 6/6 PASS 이지만, 전체 `:app-bootstrap:test` 한 번에서 **5건 실패**(positive-control 3건 `Expecting true but was false` + `OperationalContractRuntimeTest` 2건). 같은 코드로 이후 5회 전체 실행은 모두 PASS → 비결정적(flaky)·재현율 낮음. 실패는 전체 스위트의 첫 실행(리소스 edit 직후, `--rerun-tasks` 없이)에서만 관측. ## 근본 원인 / Root cause 각 positive-control 의 코퍼스가 `static final JavaClasses = new ClassFileImporter().importPackages("dev.caskeleton.bootstrap.integration")` 형태였다. `importPackages(String)` 은 패키지를 thread-context classloader / 클래스패스 location enumeration 으로 해석하는데, 대형 멀티-컨텍스트 스위트 + 불완전한 incremental build 상태에서 **빈 코퍼스**를 돌려줄 수 있다. 빈 코퍼스의 영향: - positive-control(`hasViolation()==true` 기대): 빈 코퍼스 → 위반 0 → **loud FAIL** (이게 사건을 잡아줌). - clean-check(`hasViolation()==false` 기대): 빈 코퍼스 → 위반 0 → **silently PASS (vacuous)** — 규칙이 아무것도 검사 안 했는데 통과. 더 위험. 즉 이 테스트 자체가 (a) flaky 하고 (b) clean-check 가 vacuous-pass 할 수 있는, **test-taxonomy 계약이 금지하는 바로 그 안티패턴**이었다. ## 진단 / Diagnosis - `--tests` 단독 vs 전체 스위트 대조 → 단독 PASS, 전체 FAIL(간헐) → 상호작용/순서 의존. - `OperationalContractRuntimeTest + TestTaxonomy` 둘만 함께 실행 → PASS → 특정 클래스 쌍이 아님. - 코퍼스 size + TCCL 을 assertion 메시지에 심어 전체 스위트 반복 실행으로 포착 시도 → 이후 5회 모두 PASS(재현 안 됨) → stale-build 일회성 가능성 높음. 단 **vacuous-pass 위험 자체는 설계 결함**이라 재현 여부와 무관하게 수정. ## 해결 / Fix 1. positive-control / over-block 코퍼스를 **`importClasses(SomeFixture.class)` 클래스 리터럴**로 전환 — 특정 클래스 바이트코드만 읽고 패키지 enumeration 을 안 하므로 classloader 상태와 무관하게 결정적. - 슬라이스 fixture(`MixedSliceAnnotationsFixture`, `SingleSliceWebMvcFixture`)는 cross-package `.class` 참조를 위해 `public` 으로 승격. - Testcontainers positive-control 은 전용 `public TestcontainersUsingFixture`(PostgreSQLContainer 필드)를 `..contract..`/`..architecture..` 밖(`..taxonomyfixtures..`)에 두어 clean-check 코퍼스 오염 없이 importClasses. 2. clean-check 2건(contract/architecture 실 패키지 스캔)은 auto-coverage 위해 `importPackages` 유지하되, **non-vacuity 가드** `assertThat(corpus.size()).isGreaterThan(0)` 추가 → 빈 스캔이면 silent-pass 대신 loud-fail. 검증: `TestTaxonomyArchitectureTest` 단독 PASS + 전체 `:app-bootstrap:test` `--rerun-tasks` 3회 연속 PASS. ## 교훈 / Lesson - ArchUnit 규칙의 대상이 **test 클래스**면 `@AnalyzeClasses(DoNotIncludeTests)` 로는 못 보고 manual importer 가 필요한데([[raw/interviews/archunit-manual-importer-vs-analyzeclasses]]), 그 manual importer 를 `importPackages(String)` static 필드로 쓰면 대형 스위트에서 빈 코퍼스 → vacuous/flaky 위험. - 결정성이 필요한 positive-control 은 `importClasses(Class…)` (클래스 리터럴) 가 정석 — repo 의 `ArchitectureViolationFixtureTest` 가 같은 이유로 isolation 케이스에 importClasses 사용. - clean-check 처럼 패키지 스캔이 불가피하면 **non-vacuity 가드(코퍼스 비어있지 않음)** 를 반드시 동반 — “규칙이 실제로 무언가를 검사했다”를 보장. 관련: [[raw/errors/archunit-empty-should-anchor-2026-05-27]], [[raw/errors/archunit-test-scope-sample-ticket-inclusion-2026-05-28]].