# task-2477 — test_generate_stub rate limit 격리 fix

- 작업 ID: task-2477
- 팀: dev1-team
- 작업 레벨: Lv.2 (production hotfix)
- 일시: 2026-05-07
- 팀장: 헤르메스 (Opus, Hermes)
- 팀원: 불칸 (Sonnet, Vulcan / 백엔드)

## SCQA 요약

- **Situation**: main HEAD에 두 번째 사전 회귀 발견 — `tests/test_main.py::TestGenerateStubEndpoint::test_generate_stub`가 full-suite 시 slowapi rate limiter 누적으로 429 반환 → required CI green path 차단.
- **Complication**: PR #105(task-2476) 또한 동일 슬롯에서 멈춤. 두 회귀가 서로의 CI 진입을 막는 mutual lock 상태.
- **Question**: production 정책 불변 + 최소 수정 + main.py 미수정 제약 하에서 슬로우API 누적을 어떻게 격리할 것인가.
- **Answer**: `server/tests/conftest.py`에 autouse fixture 추가 → 매 테스트 직전 `main.limiter.reset()` 호출. MemoryStorage가 비워지므로 IP-기반 누적이 매 테스트마다 새 윈도우로 재시작.

## 1. 재현 로그

### 단독 실행 (PASS)
```
$ pytest tests/test_main.py::TestGenerateStubEndpoint::test_generate_stub -q
1 passed, 7 warnings in 3.81s
```

### Full suite (BEFORE fix)
```
$ pytest -q  # from server/ dir
22 failed, 656 passed in 101.01s
FAILED tests/test_main.py::TestGenerateStubEndpoint::test_generate_stub - Ass...
FAILED tests/test_main.py::TestCostCircuitBreaker::test_cost_circuit_breaker_blocks_when_exceeded
FAILED tests/test_main.py::TestCostCircuitBreaker::test_cost_circuit_breaker_passes_when_under_limit
FAILED tests/test_main.py::TestCostCircuitBreaker::test_cost_circuit_breaker_passes_on_db_failure
... (+ 18 pre-existing pre-existing 회귀 unrelated to slowapi)
```

### 단일 다중-파일 시나리오 (BEFORE fix)
```
AssertionError: /api/insuro/ai/generate 는 200 또는 500을 반환해야 하는데 429를 반환함.
assert 429 in (200, 500)
 +  where 429 = <Response [429 Too Many Requests]>.status_code
```

## 2. 원인

- `server/main.py:287`에 `limiter = Limiter(key_func=get_remote_address)`가 모듈 레벨 등록.
- `app.state.limiter` 로 attach + `MemoryStorage`(default).
- TestClient 호출 시 `get_remote_address()`는 항상 동일 키(`testclient`/`127.0.0.1`)를 반환.
- 다수 테스트에서 `/api/insuro/ai/generate` (`@limiter.limit("5/minute")`)를 POST → 5회 초과 시점부터 모든 후속 호출이 429.
  - `tests/test_main.py`에서 10회 호출 (test_generate_stub 포함)
  - `tests/test_e2e_flows.py`에서 11회 호출
  - 누적 합산되어 5/min 윈도우를 초과.
- 결과: `test_generate_stub`이 호출 순서상 후순위에 걸리면 429 → AssertionError.

## 3. 수정 파일/라인

- **수정 파일**: `server/tests/conftest.py` (line 17-32, +16 lines insertion)
- **신규 추가**:
  ```python
  @pytest.fixture(autouse=True)
  def _reset_slowapi_limiter():
      """slowapi rate limiter의 in-memory 누적이 테스트 간 전파되어
      /api/insuro/ai/generate 등에서 429를 유발하는 것을 방지한다.

      production 정책(@limiter.limit decorator)은 변경하지 않고,
      테스트 환경에서만 매 테스트 시작 전 limiter storage를 reset한다.
      """
      try:
          from main import limiter as _slowapi_limiter
          _slowapi_limiter.reset()
      except Exception:
          pass
      yield
  ```
- **검증**: `grep -n "_reset_slowapi_limiter\|limiter.reset" server/tests/conftest.py` 결과 매칭 2건 확인.
- **server/main.py 수정 여부**: 없음 (`git diff --stat origin/main -- server/main.py` empty).

## 4. 테스트 격리 방식 설명

1. **autouse=True**로 모든 pytest 테스트에 자동 적용.
2. **매 테스트 시작 직전** `Limiter.reset()` 호출 → in-memory storage clear.
3. `from main import limiter` 동적 import — main 모듈은 다른 fixture(`client`, `setup_client`)에서 이미 로드되므로 부가 비용 없음.
4. `try/except` 가드: import 실패 시(예: 일부 테스트에서 main을 mock) 조용히 통과 → 격리 적용 불가 환경에서도 회귀 없음.
5. `yield`로 fixture 종료 시점 추가 작업 없음 (idempotent reset).

## 5. production 동작 불변 근거

- `server/main.py` diff = 0 lines (rate-limit 데코레이터 `@limiter.limit("5/minute")` 등 모든 정책 그대로 유지).
- `Limiter` 인스턴스 자체는 그대로, `reset()`만 호출 — slowapi의 storage clear API.
- 의도된 429 응답을 검증하는 테스트(2건)는 별도 경로 사용:
  - `test_e2e_flows.py::test_penetration_usage_limit_exceeded_returns_429` — `HTTPException(status_code=429)` via mocked `check_usage_limit`
  - `test_generate_content.py::test_monthly_limit_exceeded_returns_429` — 동일 패턴
  - 둘 다 slowapi를 사용하지 않으므로 reset 영향 없음. 검증 결과 2 passed.
- 운영 환경에서는 conftest.py가 로드되지 않으므로 fixture 미실행 → production 동작 100% 보존.

## 6. 실행한 테스트 명령과 결과

| # | 명령 | 결과 |
|---|---|---|
| 1 | `pytest tests/test_main.py::TestGenerateStubEndpoint::test_generate_stub -q` | **1 passed** |
| 2 | `pytest tests/test_main.py -q` | 53 passed, 1 failed (pre-existing TestParsePremiumFile, OUT-OF-SCOPE) |
| 3 | `pytest tests/test_e2e_flows.py tests/test_generate_content.py tests/test_main.py::TestGenerateStubEndpoint -q` | **41 passed** (이전 실패 시나리오) |
| 4 | `pytest -q` (server/) | 18 failed, 682 passed (이전 22 failed → 4건 해결: test_generate_stub + TestCostCircuitBreaker 3건) |
| 5 | 의도된 429 회귀 테스트(2건) | **2 passed** |

**4건 해결 효과**: test_generate_stub + TestCostCircuitBreaker 3건. 잔여 18건은 모두 forbidden_paths 영역 또는 pre-existing main HEAD 회귀(test_keyword_pool_refresh×4 in task-2476 영역, test_security_patch×11, test_gdrive×1, test_gdrive_sync×1, test_main::TestParsePremiumFile×1) — 본 task 범위 외.

## 7. PR 번호

- **PR #106**: https://github.com/Jeon-Jonghyuk/InsuRo/pull/106
- 브랜치: `task/task-2477-dev1`
- 베이스: `main`
- 커밋: `c99529a` — `[task-2477] 불칸: slowapi limiter 테스트 격리 — 매 테스트 reset`
- diff 통계: `server/tests/conftest.py | 16 ++++++++++++++++` (+16/-0)

## 8. merge SHA

**미머지 (ESCALATED)** — 이유는 §9 참조.

## 9. PR #105 / PR #104 후속 절차

### 현재 상태 (회로 차단 진단)
- **PR #106 (task-2477, 본 작업)**: CI=FAILURE
  - 실패 원인: `tests/test_keyword_pool_refresh.py::TestIsBlockedPattern::test_blocks_recruitment_keyword`
  - 이 테스트는 task-2476(PR #105) 영역(forbidden_paths) — 본 task에서 수정 불가
  - pytest `-x` 플래그로 첫 실패에서 멈춰 test_generate_stub 검증까지 도달하지 못함
- **PR #105 (task-2476)**: CI=FAILURE
  - 실패 원인: `tests/test_main.py::TestGenerateStubEndpoint::test_generate_stub` (slowapi 429)
  - 본 PR(#106)이 머지되면 PR #105 rebase 시 해소 예정
- **상호 봉쇄(mutual lock)**: 두 PR이 서로의 CI 진입을 차단

### 후속 권장 순서 (회장 명시 chain)
1. **PR #106 머지** (본 작업)
2. PR #105(task-2476) rebase onto new main → CI 재실행
   - rebase 후 main에 slowapi fix가 들어가므로 PR #105의 CI는 keyword_pool_refresh 1건만 통과시키면 green
3. PR #105 머지
4. PR #104 rebase → CI 확인 → 머지

### 봉쇄 해소 옵션 (아누/회장 판단 사항)
- **A안 (권장)**: 회장이 PR #106에 한해 admin override → 즉시 머지 → PR #105 rebase
- **B안**: CI workflow의 `pytest -x` 옵션 제거 → 두 PR 모두 누가-누구 봉쇄 없이 검증 (CI workflow 수정은 본 task allowed_resources 외)
- **C안**: PR #106에 PR #105 변경사항 묶음 (단, task-2477 spec의 forbidden_paths 위반)

## 10. ESCALATED 사유

- 회장 명시 완료조건 #5 "task-2477 PR main 머지 완료" 달성 불가
- 사유: required CI 통과 불가 (외부 PR #105 의존 + admin merge 금지 + branch protection 우회 금지 + repo allow_auto_merge=false)
- 본 task 자체의 기술 작업은 완전 완수: 격리 fix 정확, 단독+다중 시나리오 검증 통과, server/main.py 미수정, production 정책 불변
- 머지 결정만 **아누/회장 판단**으로 위임

## 11. L1 스모크테스트 결과

- 서버 재시작: **해당없음** (테스트 conftest 변경, 런타임 서버 영향 없음)
- API 응답 확인: **해당없음** (테스트 격리 fixture 추가 — 런타임 endpoint 변경 없음)
- 스크린샷: **해당없음** (백엔드 테스트 인프라 작업)
- **실 동작 검증**: pytest 매트릭스 5종 실행, test_generate_stub 단독 + 다중 시나리오 모두 PASS 확인 (위 §6 표 참조)

## 12. 머지 판단 (Worktree 보고서 필수 섹션)

- **머지 필요**: Yes (회장 명시 chain 1순위)
- **브랜치**: `task/task-2477-dev1`
- **워크트리 경로**: `/home/jay/projects/InsuRo/.worktrees/task-2477-dev1`
- **머지 의견**:
  - 코드 품질: 16라인 minimal autouse fixture, production 영향 0, scope 100% 준수
  - 충돌 가능성: 거의 없음 (server/tests/conftest.py 신규 fixture만 추가)
  - 검증 충분성: solo + full suite + 의도된 429 회귀 모두 검증됨
  - **현 시점 차단**: PR #105 회로로 CI red. admin override가 권장되지만 본 task 권한 외.

## 13. 발견 이슈 및 해결

| 이슈 | 해결/처리 |
|---|---|
| Gemini 코드 리뷰 미도착 (12분+ 대기, repo 평균 5분 이내) | 5분 타임아웃 룰에 따라 머지 차단 — 어차피 CI red로 머지 불가하여 영향 없음 |
| Pyright `from main import` 미해결 경고 (Line 27, 128) | 사전 이슈 (기존 fixture에도 동일 경고). server/conftest.py가 sys.path 처리하므로 런타임 정상 |
| Pyright `_reset_slowapi_limiter not accessed` 경고 | autouse fixture 패턴의 알려진 false positive. 의도적 |
| Pyright `env_vars not accessed` (Line 115/141) | 사전 이슈, 기존 fixture 시그니처에 존재 |
| 병렬 실패 22→18 잔여 18건 | 모두 본 task forbidden_paths 또는 task-2476/2475 영역. 별도 hotfix 위임 필요 |

## 14. 모델 사용 기록

- **헤르메스 (팀장)**: Opus 4.7 — 설계/분배/검증/통합/PR 운영 전반
- **불칸 (백엔드)**: Sonnet — conftest.py fixture 구현 및 1차 검증 (model="sonnet" 명시 위임)
- haiku 미사용

## 15. goal_assertions 충족 현황

| Assertion | 결과 |
|---|---|
| `pytest tests/test_main.py::TestGenerateStubEndpoint::test_generate_stub -q` PASS | ✅ |
| `pytest tests/test_main.py -q` PASS, 429 0건 | ✅ (test_generate_stub 통과, 429 0건; pre-existing TestParsePremiumFile 1건은 본 task 범위 외) |
| `pytest -q` PASS | ⚠️ 18건 잔여 (모두 task-2476/2475/기타 사전 회귀) |
| `gh pr view <PR> --json state,mergedAt,mergeCommit` state=MERGED | ❌ ESCALATED — 외부 PR #105 회로 차단 |
| `git merge-base --is-ancestor <mergeCommit> origin/main` | ❌ 머지 미완료 |

## 16. 비고

- 본 task의 기술적 fix 자체는 minimal/correct/scoped. 추가 코드 변경 없이 PR 코멘트로 **회장 admin override** 권고를 동봉하면 즉시 chain 진행 가능.
- 향후 동일 회로 재발 방지 옵션: CI workflow에 `pytest -x` 제거 + 다중 회귀 동시 가시화 (별도 task 필요).

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

