# task-2792 — CI-4 is_blocked 보험 도메인 정책 계약 테스트 강화

- 팀: dev4-team (비슈누) · 작성일: 2026-07-19
- worktree: `/home/jay/projects/InsuRo/.worktrees/ci-4` · 브랜치 `task/ci-4-260719` · base `cc7476b`
- 커밋: `ded17c1` (정확히 1개) · **PR 생성 0 · main merge 0**

## S (Situation)
`server/tests/test_keyword_pool_refresh.py` 에서 4건이 실패 중이었다. 실패 원인은 소스 버그가 아니라, 테스트가 `keyword_pool_refresh.py:125-127` 의 "보험 포함 키워드 면제"를 반영하지 않은 것이었다.

## C (Complication)
회장 결정: 면제는 **의도된 보험 도메인 정책**이므로 소스를 유지하고 테스트를 고친다. 단, 단순 `True`→`False` 뒤집기는 금지 — 정책이 문서화되지 않고 면제 규칙의 전체 차단 우회가 잡히지 않기 때문이다.

## Q (Question)
면제 정책을 **계약(contract)으로 명문화**하면서, 면제가 어디까지 통하는지의 경계를 정직하게 드러내는 테스트를 어떻게 구성할 것인가?

## A (Answer) — ①②③ 3블록 구조

### ① 유효 보험 주제 = blocked **false** — `TestInsuranceDomainExemption` (신규, 4건)
`보험 채용` · `보험 시험` · `보험 사기` · `대출 보험` → 전부 `is_blocked(...) is False`.
각 docstring 에 소스 근거(`keyword_pool_refresh.py:125-127`)와 "블록 문구를 포함해도 보험 문맥이므로 유효 주제" 취지를 한국어로 명시.

### ② 보험 문맥 없는 기존 차단 패턴 = blocked **true** — `TestIsBlockedPattern` (기존 확장, 23건)
BLOCKLIST_PATTERNS 10개 그룹 **전부**에 대해 보험 문구 없는 케이스로 회귀 방지:
`채용 공고`·`연봉 비교`·`공무원 시험`·`먹튀 사이트`·`고소 방법`·`대출 금리 비교`·`신용카드 추천`·`주식 투자`·`나스닥 지수`·`코스피 전망`·`etf 추천`·`아파트 전세`·`월세 계약`·`환율 전망`·`비트코인 시세`·`연말정산 방법`·`종합소득세 신고`·`적금 금리`·`예금 통장`·`음주운전 처벌`·`마약 단속`·`파산 신청`·`횡령 처벌` → 전부 True.
기존 4건은 삭제하지 않고 **입력을 보험 문구 없는 형태로 교체**(예: `보험 채용`→`채용 공고`)했고, 보험 버전은 ①로 이관했다. 기대값 뒤집기가 아니라 **케이스 분리**다.

### ③ negative control — `TestInsuranceExemptionScope` (신규, 7건)
**(a) 면제 트리거는 리터럴 부분문자열 `"보험"` 에만 반응** (면제가 블록리스트 전체를 무력화하는 스위치가 아님을 증명):
- `is_blocked("보협 마약") is True` (오타)
- `is_blocked("보 험 마약") is True` (공백 삽입 → 연속 부분문자열 부재)
- `is_blocked("insurance 마약") is True` (영문 표기)

**(b) 대조군 쌍**: `마약 단속`=True vs `보험 마약`=False · `살인 사건`=True vs `보험 살인`=False

**(c) ★ GAP — 정직 보고**:
> **소스에 "보험" 면제를 뚫는 절대 금지 규칙은 존재하지 않는다.**
> 근거: `is_blocked()` 는 `if "보험" in kw_normalized: return False` 를 **함수 최상단**에서 무조건 실행하며, BLOCKLIST_PATTERNS 순회(128-130행)와 BRAND_LIST exact match(132-133행)가 **모두 그 뒤**에 온다. 따라서 살인/자살/마약 등 어떤 패턴도 면제를 관통하지 못한다.
> 지시대로 **테스트를 지어내지 않고**, `보험 마약`·`보험 살인` 이 False 임을 현행 정책의 **특성화(characterization)** 로 assert 하고 클래스·테스트 docstring 에 GAP 을 명시했다. 절대 금지 계층이 필요하면 **소스 변경이 선행**되어야 하며 본 task 범위 밖이다(소스 수정 금지 제약).
>
> 부수 관찰: BRAND_LIST 항목 중 "보험"을 포함하는 것이 없으므로, 면제가 브랜드 검사를 우회하는 **도달 가능한 케이스는 없다**(구조상 선행하나 실질 영향 0).

## 테스트 카운트 (전후)
- **전: 38 collected → 4 failed / 34 passed**
- **후: 67 collected → 67 passed / 0 failed**
- 감소 0 (+29) · skip 0 · xfail 0 (`grep -c "skip\|xfail"` = 0)

## 변경 파일
- `server/tests/test_keyword_pool_refresh.py` — `191 insertions(+), 10 deletions(-)`
- `server/scripts/keyword_pool_refresh.py` — **무수정 확인** (`git diff --stat` 에 미등장, `git status --porcelain` 1파일만)
- expected_files 밖 수정 **0** · 신규 파일 **0**

## 실행 명령과 원문 출력
```
$ python3 -m pytest server/tests/test_keyword_pool_refresh.py -q -p no:randomly   # (수정 전, git stash)
FAILED ...::TestIsBlockedPattern::test_blocks_fraud_keyword
FAILED ...::TestIsBlockedPattern::test_blocks_loan_keyword
4 failed, 34 passed in 0.11s

$ python3 -m pytest server/tests/test_keyword_pool_refresh.py -q -p no:randomly   # (수정 후)
...................................................................      [100%]
67 passed in 0.14s

$ git rev-list --count cc7476b..HEAD
1
```

## L1 스모크테스트 결과
- **서버 재시작**: 해당없음 — 본 task 는 서버·API 표면이 없는 순수 유닛 테스트 파일 1개 변경이다.
- **API 응답 확인**: 해당없음 — 대신 **실제 모듈 직접 호출**로 실동작 검증 (pytest PASS 에만 의존하지 않음).
  - `keyword_pool_refresh` 모듈을 임포트해 실 `is_blocked()` 를 14개 키워드로 호출 → 테스트 기대값과 **전건 일치** 확인.
  - `main()` 의 노이즈 필터 라인(`[e for e in deduped if not is_blocked(...)]`)을 파이프라인 형태로 재현:
    ```
    입력 8 → 통과 4
    통과: ['암보험 비교', '보험 사기', '보험 마약', '삼성 암보험']
    차단: ['대출 금리 비교', '마약 단속', '삼성', '나스닥 지수']
    ```
    → 유닛 테스트 계약이 **파이프라인 레벨에서도 동일하게 성립**하며, GAP(`보험 마약` 이 실제 키워드 풀까지 진입)이 실동작으로도 재현됨을 확인.
- **스크린샷**: 해당없음 (프론트엔드 UI 변경 없음).
- **컴파일 검증**: `python3 -m py_compile` 양 파일 OK.

## 검증 방법 (무엇을 어떻게 확인했는가)
팀원 보고를 그대로 신뢰하지 않고 팀장이 독립 재검증했다:
1. 하누만은 ③(a) 기대값 확인 시 **소스 로직을 복제해 실행**했다고 보고 — 복제본이 원본과 어긋날 수 있어 신뢰 불가로 판단.
2. 팀장이 **실제 `kpr.is_blocked` 를 직접 임포트해 재실행** → 7개 케이스 전건 일치 확인 후 수용.
3. 수정 전 카운트도 `git stash` 로 직접 재측정 → 하누만 보고(38)가 정확, 팀장 초기 추정(39)이 오류였음을 확인·정정.
4. 신규 테스트 3블록을 팀장이 **직접 읽어** assert 가 실질적인지(vacuous 아닌지), GAP 문구가 정직한지 확인.

## 발견 이슈 및 해결
- **이슈**: 팀장이 파일 통독으로 추정한 기존 테스트 수(39)가 실측(38)과 불일치. → **해결**: `git stash` 후 `--collect-only` 로 재측정, 실측값 38 채택. 완료 판정("감소 0")은 38 기준으로도 충족.
- **이슈**: 서브에이전트의 로직 복제 기반 검증. → **해결**: 실 모듈 호출로 교차 검증, 일치 확인.

## 게이트 / 머지 판단
- **머지 필요: No** — task 파일이 `child PR 생성 0 · main merge 0 · 커밋 정확히 1개` 를 절대 제약으로 명시. 수거는 ANU 가 parent 로 수행.
- Lv.2 기본 게이트(G3 `finish --action pr`)와 충돌하나, **task 파일의 명시적 절대 제약이 우선**한다고 판단하여 PR/머지를 수행하지 않았다.
- 브랜치: `task/ci-4-260719` · 커밋 `ded17c1` · 워크트리 `/home/jay/projects/InsuRo/.worktrees/ci-4`
- 충돌 가능성: 낮음 (테스트 파일 1개 단독 변경).

## 완료 판정 대조
- [x] `python3 -m pytest server/tests/test_keyword_pool_refresh.py -q -p no:randomly` 전부 PASS (67/67)
- [x] ①②③ 각각 실제 assert 로 존재 (①4건 ②23건 ③7건)
- [x] 테스트 개수 감소 0 (38→67) · skip/xfail 증가 0
- [x] `server/scripts/keyword_pool_refresh.py` 무수정
- [x] expected_files 밖 수정 0 · 커밋 1개 · PR 0 · merge 0

## 모델 사용 기록
- 하누만(테스터) — **sonnet**: 테스트 코드 작성. 단순 유틸이 아닌 정책 계약 설계가 포함되어 haiku 미사용.
- 카르티케야/사라스바티/락슈미 — 미소집 (백엔드 소스·프론트·디자인 변경 없음).
- 비슈누(팀장, opus) — 소스 정책 분석·검증·통합만. 코드 작성 위임.

## ★ 완료 상태 — .done 미생성 (G4 게이트 에스컬레이션)

**작업 산출물은 green 이나 `finish-task.sh` 가 `.done` 생성을 차단했다. 본 task 는 완료 선언하지 않는다.**

- QC: PASS (`.qc-done` 생성, qc_result=WARN — scope_check/claude_md_check WARN, 나머지 PASS/SKIP)
- scope-diff: `server/tests/test_keyword_pool_refresh.py` 1건 = expected_files 정확 일치
- merge: `merge_policy=none` resolver 확인 → finalize-only 마커 `merge_executed:false` (task 절대제약 준수)
- **G4-GATE: `ESCALATED_OWNER_DECISION`** — `fix_loop_count=2 >= max=2` (FIX_LOOP_CAP_VIOLATION)
  - 마커: `memory/events/task-2792.g4-fix-loop-cap.json`
  - **정직한 원인**: 아래 하네스 이슈 2건을 해소하려고 `finish-task.sh` 를 반복 실행하는 과정에서 fix_loop 카운터가 cap 에 도달했다.
  - **카운터 리셋/override 는 수행하지 않았다** — 게이트 우회에 해당하며 금지 사항이다. 회장/ANU 결정을 대기한다.
  - **필요한 결정**: fix_loop 카운터 리셋 후 `finish-task.sh` 재실행 승인 여부.

### 하네스 관찰 (작업 코드와 무관, 재발 방지용 기록)
1. **QC evidence root 폴백** — `finish-task.sh` 를 `project_path` 인자 없이 호출하면 evidence root 가 `/home/jay/workspace` 로 폴백되어 `qc_verify.py` 가 `.qc-result` 를 쓰지 못하고 `[ERROR] ... exit 1`. worktree 를 명시하면 정상.
2. **GOAL-GATE cwd 의존** — `goal_assertion_exec_isolated` 가 `bash -c "$cmd"` 를 스크립트 cwd 에서 실행. 실측 대조:
   ```
   cwd=/home/jay/workspace          -> exit=4   (server/tests 경로 부재 → FAIL 오판)
   cwd=.worktrees/ci-4              -> exit=0   (67 passed)
   ```
   즉 GOAL-GATE 의 최초 FAIL 은 테스트 실패가 아니라 **경로 부재**였다.
3. **task-timers.json 엔트리 소실** — 실행 중 `task-2792` 엔트리가 MISSING 으로 바뀜. 동시간대 타 봇(dev6/task-2794, ci-5)이 같은 파일에 기록 중 → 동시 쓰기 경합 의심. (timer end 미기록 원인)
4. **Gemini CLI 인증 실패** — G4 에서 rc=1 `IneligibleTierError` → `gemini_fallback_static` 폴백(정적 검증 통과).

### 산출물
- `result.json`: `/home/jay/workspace/memory/events/task-2792.result.json` (`callback_schedule_created:false`, `relay_hints.merge_ready_ambiguous:true`)
- executor self-key callback 등록 **0** — OS-level pickup runner 가 closeout 소유.

## MATCH / GAP 요약
- **MATCH**: ① 면제 4건 · ② 회귀 방지 23건 · ③(a) 면제 경계 3건 + (b) 대조군 4건 — 요구사항 전건 충족.
- **GAP**: ③(c) "보험" 면제를 뚫는 절대 금지 규칙이 소스에 **부재** → 면제는 무제한. 테스트를 지어내지 않고 characterization + docstring GAP 명시로 처리. 절대 금지 계층 도입은 소스 변경이 필요하므로 **후속 판단 요청**.

## 세션 통계
- 총 도구 호출: 0회

