# task-2803 — TRACK_B_FOLLOWUP: 신규 실패 5건 해소 (테스트 격리)

- 팀: dev6-team (팀장 페룬)
- 브랜치: `task/task-2801-dev6` (worktree `/home/jay/projects/InsuRo/.worktrees/task-2801-dev6`)
- 커밋: `8199bd7` (0acbd91 이후 **정확히 1개**)
- 수정 파일: `server/tests/conftest.py` **단 1개** (201 insertions, 0 deletions)
- PR 생성 0 · merge 0

---

## SCQA

**Situation** — `task/task-2801-dev6` 은 테스트 66개를 추가했으나 신규 실패 5건을 만들었다 (clean main 17 failed → B 22 failed).

**Complication** — 5건 전부 단독 실행 시 PASS. 기능 결함이 아니라 **테스트 격리 실패**(env 누수 + rate limiter 전역 버킷 누수)다.

**Question** — `server/tests/conftest.py` 만 손대서 신규 실패 0으로 되돌릴 수 있는가?

**Answer** — 가능했다. 전체 스위트 **22 failed → 16 failed**, 신규 실패 0, 대상 5건 전부 PASS, 테스트 개수 감소 0.

---

## 원인별 조치

### 원인 A — INSURO_GOOGLE_* env 누수 (gdrive 2건)

근거: `server/main.py:53-54` 가 import 시 `load_dotenv()` 를 실행 → `.env` 의 `INSURO_GOOGLE_*` 가 프로세스 환경에 주입되고 monkeypatch 관리 밖이라 잔류. `gdrive.py:33-40` / `gdrive_sync.py:188-190` 은 `INSURO_GOOGLE_*` 를 `GOOGLE_*` 보다 **우선** 사용하는데, 해당 테스트는 `GOOGLE_*` 만 조작 → "env 없으면 raise" 가 raise 하지 않음.

조치: conftest.py 끝에 함수스코프 `autouse=True` fixture `_isolate_insuro_google_env(monkeypatch)` 신규 추가. 4개 키를 `monkeypatch.delenv(..., raising=False)` 로 제거.
- 지시대로 **`monkeypatch` 사용** (`patch.dict(os.environ)` 아님) — 저장소 기존 패턴 `env_vars(monkeypatch)` 와 일치.
- **`env_vars` 충돌 없음 (실측 확인)**: autouse fixture 가 명시적 요청 fixture 보다 먼저 실행되므로 `env_vars` 의 setenv 가 나중에 적용된다. 전체 스위트에서 `env_vars` 의존 테스트 신규 파손 0건.

### 원인 B — rate limiter 전역 버킷 누수 (CostCircuitBreaker 3건)

조치: 브랜치 `task/ci-2-260719` 커밋 `e404e44f42527b3c16c0df1d5805ffd393eb51b1` 의 검증된 구현을 **재설계 없이 그대로 이식**.
- `_reset_rate_limiter_storage()` + 함수스코프 autouse `_reset_rate_limiter_bucket_per_test()`
- fail-closed (import 실패·reset 실패 시 `RuntimeError`)
- session-scope negative control fixture 유지

> **지시서와 실제 상태의 차이 (중요)**: 지시서는 "커밋 e404e44f 의 conftest.py **변경분**(175-228행, 252행 이하)을 반영"하라고 했으나, 실측 결과 우리 브랜치의 conftest.py 는 **142줄이며 rate limiter 관련 코드가 전혀 없었다**(`grep limiter` 0건). 즉 그 커밋의 diff 는 이미 해당 코드가 있는 버전을 대상으로 한 것이라 **diff 만 적용하는 것은 불가능**했다. 대신 참조 파일의 **143~308줄 전체를 이식**했다. 참조 파일 1~142줄이 우리 파일과 **완전히 동일**(diff 0)함을 먼저 확인했으므로 이 이식은 안전하다.

---

## 팀원 산출물 검증에서 발견·수정한 결함

스바로그(sonnet)의 1차 산출물에서 **참조 구현의 마지막 줄 `    yield` 가 누락**되어 있었다 (session-scope `_negative_control_rate_limit_still_enforced` fixture 종단). 팀원 보고서에는 언급되지 않았고, pytest 가 non-generator fixture 도 정상 처리하므로 **테스트는 전부 통과해 표면 검증으로는 잡히지 않았다**.

- 발견 방법: `diff <(참조 143-308행) <(우리 143-308행)` 직접 대조
- 조치: 팀장이 `yield` 복원 후 `--amend` (커밋 1개 제약 유지)
- 검증: 재대조 결과 **`PORT_EXACT_MATCH`** — 이식 구간이 참조와 바이트 단위 동일

> 교훈: "테스트 전부 PASS" 는 "지시대로 이식됨" 을 보증하지 않는다. 이식 작업은 반드시 원본과 직접 diff 해야 한다.

---

## 완료 판정 (6개 항목 전부 검증)

| # | 기준 | 결과 |
|---|---|---|
| 1 | 전체 스위트 17 failed 이하 · 신규 실패 0 | ✅ **16 failed, 729 passed** |
| 2 | 대상 5건 전체 스위트에서 PASS | ✅ 5건 모두 FAILED 목록에 없음 |
| 3 | negative control 429 assert 유지 | ✅ 이식 원문 그대로 + `yield` 복원 |
| 4 | fail-closed 유지 | ✅ **주입 실증** (아래) |
| 5 | 테스트 개수 감소 0 · skip/xfail 증가 0 | ✅ **745 collected** (전후 동일) |
| 6 | B 의 신규 테스트 66개 유지·통과 | ✅ 삭제 0, 실패 목록에 없음 |

### 실행 원문

**전체 스위트 (수정 후)**
```
16 failed, 729 passed, 38 warnings in 114.26s (0:01:54)
```
잔존 16건 = `test_keyword_pool_refresh`(4) + `test_main::test_detect_company_hanwha`(1) + `test_security_patch`(11, FileNotFoundError 계열).
**전부 이번 변경과 무관한 기존 결함**이며 수정 전 22건의 부분집합. 손대지 않았다.

**전체 스위트 (수정 전 기준선, 스바로그 측정)**
```
22 failed, 723 passed, 38 warnings in 112.16s
```
→ 지시서의 "B 22 failed" 와 일치. 사라진 6건 = 목표 5건 + `test_main::TestGenerateStubEndpoint::test_generate_stub`(추가 수혜).

**원인 A 양방향 순서 (팀장 직접 재실행)**
```
pytest test_gdrive.py::...missing_env test_consultation_history_v1.py  → 39 passed
pytest test_consultation_history_v1.py test_gdrive.py::...missing_env  → 39 passed
```
수정 전에는 역순에서 `DID NOT RAISE` 로 1 failed 였다. **양방향 대칭 통과 = 순서의존 제거 확인.**

**대상 5건 개별**
```
tests/test_main.py::TestCostCircuitBreaker::test_cost_circuit_breaker_blocks_when_exceeded PASSED
tests/test_main.py::TestCostCircuitBreaker::test_cost_circuit_breaker_passes_when_under_limit PASSED
tests/test_main.py::TestCostCircuitBreaker::test_cost_circuit_breaker_passes_on_db_failure PASSED
tests/test_gdrive.py::TestGetDriveService::test_get_drive_service_missing_env PASSED
tests/test_gdrive_sync.py::TestSyncDriveFolderReturnValue::test_sync_uses_env_folder_id_when_none PASSED
5 passed
```

**수집 개수**
```
745 tests collected   (수정 전 745 → 수정 후 745, 증감 0)
```

---

## L1 스모크테스트 결과

- **서버 재시작**: 해당없음 — 본 작업은 테스트 격리 fixture 전용이며 `server/main.py` 등 런타임 소스 변경 0건. 재시작으로 관측 가능한 동작 변화가 없다.
- **API 응답 확인**: 해당없음 (동일 사유). 단, negative control fixture 가 세션 시작 시 `TestClient` 로 `/api/insuro/ai/generate` 를 7회 실호출하여 **429 를 실제 관측**하며, 이것이 매 스위트 실행마다 자동 수행된다(전체 스위트 통과 = 이 실호출 통과).
- **스크린샷**: 해당없음 (프론트엔드 변경 0건).
- **L1 실행·통과 항목 (pytest PASS 를 넘는 실동작 검증)**: **fail-closed 주입 실증**

```bash
# /tmp/failinject_2803.py (저장소 밖) 로 main.limiter.reset 을 강제 실패시킴
PYTHONPATH=/tmp:$(pwd) python3 -m pytest tests/test_gdrive.py -q -p no:randomly -p failinject_2803
  → 6 warnings, 13 errors in 0.83s   / exit code 1     ← 명시적 실패 (fail-closed 작동)

# 대조군 (주입 없음)
python3 -m pytest tests/test_gdrive.py -q -p no:randomly
  → 13 passed, 6 warnings in 0.45s   / exit code 0
```
→ reset 실패 시 **조용히 넘어가지 않고 세션이 시끄럽게 깨진다**는 것을 인위적 결함 주입으로 직접 관측했다. 임시 plugin 은 저장소 밖(`/tmp`)에 두어 git stage 0 을 유지했다.

---

## 절대 제약 준수 확인

| 제약 | 확인 방법 | 결과 |
|---|---|---|
| 커밋 정확히 1개 | `git log --oneline 0acbd91..HEAD` | ✅ 1개 (`8199bd7`) |
| conftest.py 외 수정 0 | `git diff --stat 0acbd91..HEAD` | ✅ 1 file changed |
| 신규 파일 0 | 위 동일 (insertions만) | ✅ |
| 소스(main/gdrive/gdrive_sync) 수정 0 | 위 동일 | ✅ |
| skip/xfail/assertion 완화 0 | 수집 745 동일 · diff 내 skip/xfail 0 | ✅ |
| 기존 테스트 삭제 0 | 0 deletions | ✅ |
| PR 생성 0 · merge 0 | 미실행 | ✅ |
| working tree clean | `git status --short` | ✅ 공란 |
| 보고서 git stage 0 | 저장소 밖 `/home/jay/workspace/memory/reports/` | ✅ |

---

## MATCH / GAP

**MATCH**
- 원인 A·B 진단이 실측과 일치 (양방향 순서 실험으로 재현·해소 확인)
- 지시된 fixture 형태(함수스코프 autouse, monkeypatch, fail-closed, negative control) 전부 반영
- 완료 판정 6항목 전부 충족
- "B 22 failed" 사전 상태 수치 일치

**GAP**
1. **지시서의 이식 방법이 실제 파일 상태와 불일치** — "diff 를 반영" 은 불가능했고 "143-308행 전체 이식" 으로 수행. (위 원인 B 참조). 결과물은 참조와 바이트 동일이므로 의도는 충족.
2. **clean main 17 기준선은 본 세션에서 직접 재측정하지 않았다.** 우리 브랜치의 사전 상태 22 는 측정했고 지시서 수치와 일치했으나, clean main 을 체크아웃해 17 을 확인하지는 않았다(브랜치 오염 위험 회피). 판정 기준 "17 이하"는 실측값 16 으로 충족.
3. **잔존 16건 미해결** — 범위 외(기존 결함, 본 작업 이전부터 실패). 지시서가 테스트 격리로 범위를 한정했으므로 손대지 않았다. 별도 task 필요.

---

## 모델 사용 기록

- **스바로그(백엔드)** — `sonnet`. conftest.py 이식 + env 격리 fixture 구현 + 검증 실행.
- **팀장(페룬, Opus)** — 직접 개입 1회: 팀원이 누락한 `yield` 복원. 사유 = 참조 원본과의 diff 대조에서만 드러나는 결함이었고, 커밋 1개 제약 때문에 `--amend` 가 필요해 팀장이 직접 처리하는 편이 안전했다.
- 라다/모코시/벨레스 미소집 — 프론트엔드·UX·별도 테스터 작업 없음(단일 테스트 인프라 파일). 검증은 팀장이 직접 재실행하여 대체.
- haiku 미사용.

---

## 머지 판단

- **머지 필요**: **No** — 지시서가 PR 생성·merge 를 명시적으로 금지(`merge_policy: none`).
- **브랜치**: `task/task-2801-dev6`
- **워크트리 경로**: `/home/jay/projects/InsuRo/.worktrees/task-2801-dev6`
- **머지 의견**: 커밋 `8199bd7` 은 테스트 전용 파일 1개만 건드리며 런타임 소스에 영향 0. 신규 실패 0 · 실패 6건 감소 · 테스트 개수 감소 0 으로 회귀 위험이 낮다. 다만 본 커밋은 task-2801 브랜치 위에 쌓인 것이므로, **머지 판단은 task-2801 전체 묶음에 대해 아누(개발실장)가 내려야 한다.** 본 task 단독으로는 머지 대상 아님.

---

## 미해결 / 후속 필요

- 잔존 16건(`test_keyword_pool_refresh` 4 · `test_main::test_detect_company_hanwha` 1 · `test_security_patch` 11) — 본 작업 범위 외 기존 결함. `test_security_patch` 는 FileNotFoundError 계열로 경로/실행 위치 의존 가능성이 있어 별도 조사 권장.
- 디자인팀 호출 필요: 없음.

---

## ★ 완료 처리 상태 — scope guard 에스컬레이션 (ANU 판정 필요)

`finish-task.sh` 를 실행했고 **QC 자체는 통과(WARN 등급, git_evidence PASS · duplicate_check PASS · l1_smoketest_check PASS)** 했으나,
**scope guard 가 위반 9건을 보고하여 `.done` 이 생성되지 않았다.** 수동 `.done` 생성은 금지되어 있으므로 만들지 않았다.

### 이것은 구조적 오탐이다 (근거)

`scripts/finish-task.sh` 의 scope-diff 계산식:
```bash
git diff --name-only "${MAIN_BRANCH}..HEAD"      # 비STRICT
merge-base "origin/${MAIN_BRANCH}" HEAD ..HEAD   # STRICT
```
→ **어느 쪽이든 baseline 이 main 이다.**

그런데 본 task 지시서는 명시적으로 stacked-branch 를 요구했다:
> "기존 브랜치 `task/task-2801-dev6` 에 이어서 커밋 (7커밋 위에 추가)"

따라서 main 기준 diff 에는 **task-2801 의 선행 7커밋 산출물 9개 파일이 필연적으로 포함**된다. scope guard 는 "main 에서 분기한 신규 브랜치"를 전제하므로 stacked task 에서 구조적으로 과대보고한다.

### 실제 변경 증거
```
$ git diff --name-only 0acbd91..HEAD
server/tests/conftest.py          ← 1개
```
capability snapshot 의 `allowed_resources.paths = ["server/tests/conftest.py"]` 와 **정확히 일치**. 권한 위반 0건.

보고된 9건은 전부 task-2801 산출물(`server/main.py`, `routes/consultation_history_v1.py`, `schemas/consultation_history_v1.py`, `utils/legacy_write_guard.py`, migrations 2건, tests 3건)로 **본 task 가 건드린 파일은 0건**이다.

### 내가 하지 않은 것 (우회 시도 0)
- capability snapshot 수정 — 안 함 (`forbidden_paths: memory/capabilities/**` 로 금지)
- `.worktree-base.json` 조작으로 scope-base 우회 — 안 함
- 수동 `.done` 생성 — 안 함
- 커밋 되돌리기/rebase 로 diff 세탁 — 안 함

### ANU 판정 요청 사항
stacked-branch task 의 scope-base 를 지시서가 명시한 base `0acbd9107d188c6558b27eafcc72246f759ce44c` 로 인정할지 판정 바람.
인정 시 위반 0건이며 나머지 완료 조건은 전부 충족 상태다.

- 에스컬레이션 마커: `memory/events/task-2803.escalate`
- 위반 상세: `memory/events/task-2803.scope-violation.json`
- 기계판독: `memory/events/task-2803.result.json` → `scope_guard_adjudication`

> 참고: 본 건은 finish-task.sh 의 stacked-branch 미지원이라는 **하네스 일반 결함**으로 보이며, 앞으로 "기존 브랜치 위에 이어서 커밋" 을 지시하는 task 마다 재발한다. 별도 개선 task 를 권장한다.
