# CI-UNBLOCK 진단 결과 + 최소 수정 계획 (제출용)

**상태**: 진단 완료 · **코드 수정 0 · PR 0 · dispatch 0**. 회장 승인 대기.
**작성**: 아누 2026-07-19 KST · **검토**: Codex CC 합의 완료

---

## 0. 결론 한 줄
**PR #108 의 CI 실패는 계약 PR 이 원인이 아니다.** `clean main` 이 이미 red 이며, **`HEAD_ONLY` 실패는 0** 이다.

---

## 1. 재현 (ANU 직접 실행)

| 대상 | SHA | 명령 | 결과 |
|---|---|---|---|
| clean main | `cc7476bfbd9f6b3ae3c99f9a7136cc9f94cd3ada` (worktree `/tmp/ci-base`) | `python3 -m pytest server/tests -q -p no:randomly --maxfail=12` | **12 failed / 604 passed** |
| PR head | `ce7fb92f2ba298c914e7af71a202b687e8901062` | 동일 | **12 failed / 607 passed** (동일 실패군) |

**`git diff cc7476b ce7fb92f -- server/ tests/` = 비어 있음** → PR 은 `server/`·`tests/` 를 전혀 건드리지 않았다.

---

## 2. 축별 분류

| 축 | 대상 | 건수 | 분류 |
|---|---|---|---|
| **1** | `server/tests/test_security_patch.py` 하드코딩 worktree 절대경로 | 11 | **DETERMINISTIC · BASE_EXISTING** |
| **2** | rate limiter 429 누수 (`CostCircuitBreaker` 3 + `TestGenerateStubEndpoint` 1) | 4 | **ORDER_DEPENDENT · BASE_EXISTING** |
| **3** | 한화 canonical 불일치 | 1 | **DETERMINISTIC · BASE_EXISTING** |
| **3b** | `test_keyword_pool_refresh.py::TestIsBlockedPattern` ★**회장 4축에 없던 5번째 축** | 4 | **DETERMINISTIC · BASE_EXISTING** |
| **4** | e2e 502 토스트 | 1 | **FLAKY · 외부 동기화 의존** |

**`HEAD_ONLY` = 0.**

---

## 3. 축별 원인·authoritative·최소 수정 (Codex 합의)

### 축1 — 사라진 worktree 절대경로
- **CAUSE**: `server/tests/test_security_patch.py:5` `WORKTREE = Path("/home/jay/projects/InsuRo/.worktrees/task-2264-dev2")` → 해당 worktree **부재 확정**(`git worktree list` 대조) → `FileNotFoundError` 로 11건 연쇄 실패
- **AUTHORITATIVE**: **소스가 맞다.** 검증 대상은 현재 repo 소스인데 테스트만 사라진 외부 경로를 전제한다
- **FILES**: `server/tests/test_security_patch.py`
- **수정 방향**: 절대경로 → **repo 루트 상대 해석**(`Path(__file__).resolve().parents[2]`). ⚠️ **다른 로컬 절대경로로 치환 금지**(회장 금지사항)
- **TEST**: `pytest server/tests/test_security_patch.py -q -p no:randomly` → 11건 PASS

### 축2 — rate limiter 상태 누수
- **CAUSE**: `server/main.py:287-289` 전역 `Limiter` 가 같은 프로세스의 여러 `TestClient(app)` 간 상태를 공유 → `/api/insuro/ai/generate` 의 `5/minute` 버킷 누수
- **증거**: **단독 실행 시 전부 PASS**(`TestCostCircuitBreaker` 3 passed, `TestGenerateStubEndpoint` 2 passed), 전체 스위트에서만 429
- **AUTHORITATIVE**: **테스트가 맞다.** 429 는 비즈니스 로직이 아니라 **테스트 상태 오염**
- **FILES**: `server/tests/conftest.py`
- **수정 방향**: autouse fixture 로 각 테스트 전후 **limiter 버킷 reset**. ⚠️ **429 를 기대값으로 바꾸지 않는다**(회장 금지사항)
- **TEST**: 4건을 **단독 + 전체 스위트 순서 포함**으로 실행해도 429 없이 기대 status(200/503/401)만

### 축3 — 한화 canonical
- **CAUSE**: `server/tests/test_main.py:1291` 만 구형 canonical `"한화손보"` 기대. 실제 `_detect_company` 는 `"한화손해보험"` 반환
- **AUTHORITATIVE**: **소스가 맞다.** Codex 교차확인 — `server/main.py:4855,4910` · `server/config/insurance_domain_terms.json` · `src/data/insurance-companies.ts` · `extension/__tests__` · `supabase` migration 이 **전부 `한화손해보험`을 정식명**으로 사용
- **FILES**: `server/tests/test_main.py`
- **TEST**: `-k detect_company_hanwha` PASS

### 축3b — `is_blocked` 규칙 ★ **회장 결정 필요**
- **CAUSE**: `server/scripts/keyword_pool_refresh.py:119-127` 이 **`"보험"` 포함 키워드를 블록리스트에서 면제** → `보험 채용/시험/사기`, `대출 보험` 이 `False`
- **Codex 판정**: 소스가 맞다(동일 규칙이 `server/scripts/news_keyword_extract.py:161-168` 에도 존재)
- **⚠️ ANU 이견 — 기계적으로 테스트를 고치면 안 된다**: 이건 **사업 규칙 판단**이다. "보험 채용" 키워드를 **차단 면제하는 것이 의도된 동작인지**(회장님 리쿠르팅 캠페인 맥락) 아니면 **면제 규칙이 과광범위한 버그인지**를 회장님이 정해야 한다.
  - 소스가 의도대로면 → **테스트 수정**
  - 면제 규칙이 버그면 → **소스 수정**
  - **확정 전 착수 금지.** 소스에 맞춰 테스트를 바꾸는 것은 사실상 assertion 완화가 될 수 있다.
- **FILES**: (결정에 따라) `server/tests/test_keyword_pool_refresh.py` **또는** `server/scripts/keyword_pool_refresh.py`

### 축4 — e2e flaky
- **CAUSE**: `tests/e2e/keyword-analysis-info-generate.spec.ts:405`. spec 은 이미 `page.route` 로 502 를 mock 하지만, **mocked 502 관측 전에 toast assertion 이 실행**되어 흔들린다
- **AUTHORITATIVE**: **테스트가 맞다.** 앱 코드(`src/pages/InfoKeyword.tsx:391-393,455-456`)는 502 `detail` 을 toast 로 노출하도록 되어 있다 → 불안정한 쪽은 **E2E 동기화**
- **FILES**: `tests/e2e/keyword-analysis-info-generate.spec.ts`
- **수정 방향**: 클릭과 동시에 `waitForResponse` 로 **502 응답 수신을 동기화**한 뒤 toast assert. ⚠️ **skip·retry 증설 금지**
- **TEST**: `--grep '시나리오2'` **연속 재실행 안정 통과**

---

## 4. task 분리안 (거대 수정 금지 — 회장 지시)

| task | 축 | FILES | 병렬 |
|---|---|---|---|
| **CI-1** | 축1 | `server/tests/test_security_patch.py` | O |
| **CI-2** | 축2 | `server/tests/conftest.py` | O |
| **CI-3** | 축3 | `server/tests/test_main.py` | O |
| **CI-4** | 축3b | **회장 결정 후 확정** | **HOLD** |
| **CI-5** | 축4 | `tests/e2e/keyword-analysis-info-generate.spec.ts` | O |

**파일 중첩 0** — CI-1·2·3·5 는 서로 다른 파일이라 **병렬 개발 가능**. merge 는 순차.
**CI-4 는 회장 결정 전 착수 금지.**

---

## 5. 금지사항 준수 확인
테스트 skip **0** · required check 해제 **0** · assertion 완화 **0** · 429 기대값화 **0** · 다른 로컬 절대경로 치환 **0** · 무제한 retry **0** · admin override merge **0** · A/B 조기 dispatch **0**

---

## 6. 완료 판정 기준 (ANU 독립 확인)
1. **clean main 기준 전체 required checks green**
2. **PR #108 checks 재실행 green**
3. **기존 테스트 감소 0** (base 604 passed → 이상)
4. **flaky 재실행에서도 안정 통과**(축4 연속 실행)

---

## 7. 회장 결정 필요 2건
1. **축3b 방향** — `"보험"` 면제 규칙이 의도인가 버그인가 (테스트 수정 vs 소스 수정)
2. **CI-1·2·3·5 dispatch 승인** — 진단만 끝났고 수정 착수는 미승인 상태

## 8. 정리 필요
- 진단용 임시 worktree **`/tmp/ci-base`** 존재. 검증 재사용 후 `git worktree remove` 예정.
