# task-2607 — Track D ROOT CAUSE REPORT: test_23 교차오염

> read-only 진단. 수정 0. git HEAD 전후 EQUAL(`20456b5f83fc039f2fd6f50f4b94095c29b41bfb`), 진단 대상 byte-0. 본 보고서·matrix·result 외 write 0.

## 1. 증상 (실증)

- **단독**: `pytest tests/regression/test_callback_owner_validation_2553plus49.py` → **19/19 PASS** (test_23 PASS).
- **합산**: `pytest tests/regression/` → **test_23 FAIL** (1879 passed, 8 failed; test_23 포함). 결정론적 재현.
- spec 인용 `+54 collector-verify: combined_44_47_49_53_WITHOUT_54 = 122 passed 1 failed SAME test_23` 와 동일 7-suite 만 격리 실행 시에는 **123/123 PASS** → 실패는 **collection scope/ordering 의존**.

## 2. ROOT CAUSE (확정)

**`anu_v3.callback_owner_validator` 모듈이 단일 인터프리터 내에서 2개 이상의 객체로 공존 → `CallbackRegistrationBlocked` 예외 클래스 identity 불일치.**

test_23 의 전체 traceback 이 이를 단정적으로 증명한다:

```
with self.assertRaises(CallbackRegistrationBlocked):
    guard_callback_registration(... executor self-key ...)
...
dispatch/cron_dispatch_guard.py:439: assert_registration_permitted(val)
...
E   anu_v3.callback_owner_validator.CallbackRegistrationBlocked:
    callback registration blocked: verdict=FAIL
    classifications=['SELF_COLLECTOR_FORBIDDEN','CALLBACK_OWNER_MISMATCH']
anu_v3/callback_owner_validator.py:157: CallbackRegistrationBlocked
```

예외는 **정상적으로 raise** 되었다(`callback_owner_validator.py:157`, FQN `anu_v3.callback_owner_validator.CallbackRegistrationBlocked`). 그럼에도 `self.assertRaises(CallbackRegistrationBlocked)` 가 **포착에 실패**했다. 단일 모듈 인스턴스 세계에서는 불가능한 결과 → 동일 qualname·상이 `id()` 의 클래스 객체 ≥2개가 동시에 살아있음을 의미한다. 즉:

- **객체 X** — 테스트가 collection 시점에 `CallbackRegistrationBlocked = _valmod.CallbackRegistrationBlocked` 로 바인딩한 클래스 (`_valmod = _load("anu_v3.callback_owner_validator", ...)` 가 그 순간 `sys.modules` 에 있던 것을 반환).
- **객체 Y** — test_23 실행 시점에 `cron_dispatch_guard.guard_callback_registration` 내부의 **런타임 lazy import** `from anu_v3.callback_owner_validator import (assert_registration_permitted, CallbackRegistrationBlocked)` (`cron_dispatch_guard.py:431/394`) 가 그 순간 `sys.modules["anu_v3.callback_owner_validator"]` 에서 해석한 클래스.

`assertRaises(X)` 인데 raise 된 것은 `Y` → `except X` 미포착 → 예외가 테스트 밖으로 전파 → test_23 FAIL. **이것이 단 하나의 직접 원인이며, 단독 실행에서는 X≡Y(슬롯 미교체)이므로 PASS 한다.**

## 3. 결함을 가능케 한 4대 구조적 요인 (§3 후보 — 전부 확정)

전부 코드 read-only 분석으로 확인됨.

### ① 모듈레벨 `_load()` 10모듈 sys.modules pre-seed, teardown 0 — **CONFIRMED**
`test_callback_owner_validation_2553plus49.py:24-58` 의 `_load()` 는 `if modname in sys.modules: return sys.modules[modname]` 후 `spec_from_file_location` 로 적재. 모듈 import(=pytest collection) 시점에 10개 dotted 모듈(`dispatch.executor_completion_contract`, `dispatch.spec_template_validator`, `anu_v3.callback_4tuple_registry`, `dispatch.callback_owner_enforcer`, `dispatch.normal_fallback_callback_helper`, `anu_v3.callback_owner_validator`, `anu_v3.authoritative_verdict_selector`, `anu_v3.self_collector_guard`, `anu_v3.writeback_binding_conflict_guard`, `dispatch.cron_dispatch_guard`)를 `sys.modules` 에 선적재하지만 **정리(teardown) 코드가 전혀 없다.** `_valmod`/`_grd` 바인딩은 "그 시점의 슬롯"에 영구 고정되는 반면, `guard_callback_registration` 의 lazy import 는 "호출 시점의 슬롯"을 본다 — 두 시점 사이 슬롯이 바뀌면 즉시 identity 불일치.

### ② `tests/conftest.py` autouse restore fixture 가 8/10 모듈 미커버 — **CONFIRMED**
`_restore_dispatch_module` (conftest.py:56-70) 은 **`sys.modules["dispatch"]`(부모 패키지 1개)만** 저장·복원한다. `dispatch.cron_dispatch_guard` / `dispatch.callback_owner_enforcer` / `dispatch.normal_fallback_callback_helper` / `dispatch.executor_completion_contract` / `dispatch.spec_template_validator` 등 **서브모듈은 복원 대상이 아니다.** `_restore_verifier_modules` (conftest.py:73-100) 는 `verifiers.*` 만 커버 — `anu_v3.*` 전무. 따라서 어떤 선행 테스트가 `anu_v3.callback_owner_validator` 슬롯을 교체/reload 해도 **원복되지 않고 영구 오염**된다. pre-loaded 핵심 8모듈(`anu_v3.callback_owner_validator` 포함)이 복원 화이트리스트에서 누락.

### ③ unittest.TestCase + pytest collection/run ordering 불명확 — **CONFIRMED (실증)**
바인딩(X)은 **collection import 시점**, raise(Y)는 **test_23 run 시점**. pytest 는 전 모듈을 먼저 import 후 실행하므로 그 사이 임의의 선행 모듈/테스트가 슬롯을 변경할 수 있다. Q1(5)·Q2(5) 각각은 PASS, Q1+Q2(=H1 10) 합산만 FAIL — **단일 파일이 아니라 복수 suite 의 상이 import 전략 누적 상호작용**이 필요함이 실측으로 입증됨. ordering·scope 가 결과를 좌우 = ordering 비결정성 확정.

### ④ `pyproject.toml` 격리 설정 부재 — **CONFIRMED**
`[tool.pytest.ini_options]` 는 `testpaths`/`python_files`/`addopts="-v"`/`markers` 뿐. `--forked`(프로세스 격리)·`-p no:cacheprovider`·테스트 순서/리셋 플러그인·모듈 isolation **전무.** 1879개 테스트가 **단일 인터프리터·단일 `sys.modules`** 를 공유 → ①②③ 의 오염이 전 세션으로 누적·전파. 표준 +49 단독 명령만이 오염 무발생 환경.

## 4. 인과 체인 (요약)

격리 부재(④) ∴ 단일 sys.modules → `_load` pre-seed 가 teardown 없이(①) dotted 슬롯 점유 → 어떤 선행 suite(H1 누적)가 `anu_v3.callback_owner_validator` 슬롯을 다른 인스턴스로 교체 → conftest restore 가 그 슬롯을 커버 안 함(②) ∴ 영구 오염 → collection 바인딩 X 와 run-time lazy import Y 가 ordering 상(③) 상이 인스턴스 → `assertRaises` identity 불일치 → **test_23 FAIL**. 단독 실행은 슬롯 교체 사건이 없어 X≡Y → PASS.

## 5. 비-스코프 (collateral, 별개 선재 이슈)

전체 regression 동반 FAIL 7건은 본 root cause 와 무관한 별개 이슈 — `affected-suites.json` 에 분리 기록(수정 권고 아님, 정보 제공).

## 6. 수정 권고 요약 (상세: fix-recommendation.json)

별도 GO 전 **수정 0**. 권고안은 fix-recommendation.json 참조. 핵심: 테스트 측 module-aliasing 제거(권장 P0, 진단대상 외 최소표면) 또는 conftest restore 화이트리스트에 pre-loaded 8모듈 추가(P1) 또는 pyproject 프로세스 격리(P2). **본 보고서는 진단·권고까지이며 어떤 코드 변경도 수행하지 않았다.**
