# 테스트 계획 (Test Plan)

최근 갱신: 2026-04-22 (W0-F02 subset/repo 분리)

> ⚠️ **이 문서에는 두 종류의 수치가 함께 등장합니다.** 혼동 금지.
>
> | 구분 | 결과 | 의미 |
> |------|------|------|
> | **Refactor subset** (§2) | **256 passed** | 이번 리팩토링(Phase 0~5)에서 추가/유지한 테스트 묶음 |
> | **Repo 전체** (§1) | **637 passed / 56 failed / 2 errors / 1 skipped** | `pytest backend/tests -q` 전체 실행 결과 (2026-04-21 Codex 재측정) |
>
> repo 전체 green은 현재 **미달성**. `docs/review-codex-1/11-fix-plan.md`의 W3(cluster A/B/C) + W5(cluster D)에서 해소 예정.

## 구성 요약 (subset 기준)
| 구분 | Phase | 테스트 수 |
|---|---|---|
| manifest/catalog/applicability/checkpoint_io | 0~1 | 65 |
| sync services / service layer | 2 | 30 |
| SceneAnalysisContext DTO / loader | 3 부분 | 8 |
| analysis_dispatch_service | 4.1 | 26 |
| outlook_dedup | 4.3 | 15 |
| relation delta sync | 4.4 | 6 |
| outlook (character_outlook) delta sync | 4.5 | 4 |
| prompt_service | 3b.1 | 26 |
| image_service public API | 3b.4 | 5 |
| reference/scene_image_service | 3b.5 | ~30 |
| 기타 (test_step_manifest_v3 재작성 + P1 통합 테스트) | — | ~39 |
| **합계** | | **256** |

> Phase 5 Frontend는 별도 테스트 없이 TypeScript type-check + 수동 실측 + 듀얼 코드 리뷰로 검증.
> 세션 E(`except: pass` 제거)는 py_compile 21/21 통과 + 런타임 회귀 없음 확인.
> Frontend 자동화 테스트(Vitest + RTL)는 `docs/review-codex-1/11-fix-plan.md` W3에서 도입 예정.

---

## Repo 전체 기준선 (W0 보강, 2026-04-21 Codex 재측정)

### 실행 결과

| 명령 | 결과 |
|------|------|
| `backend/.venv/bin/python -m pytest backend/tests -q` | **637 passed / 56 failed / 2 errors / 1 skipped** (124.30s) |
| `backend/.venv/bin/python -m pytest backend/tests/test_pipeline_e2e.py -q` | 2 passed / 1 skipped (단독 실행은 통과) |

같은 파일이 단독에서는 통과하고 전체 suite에서 실패한다는 것은 **테스트 flaky가 아니라 startup/schema coupling** 때문이다 (`backend/app/main.py`의 import-time `init_db()` 부수효과).

### Failure clusters (4종)

| 클러스터 | 대표 파일 | 원인 | 해소 Wave |
|---------|----------|------|-----------|
| **A: startup/schema coupling** | `test_pipeline_e2e.py` | app import 시 `_ensure_default_user()`가 스키마 준비 전 `user_account` 접근 | W2 (F13 `init_db()` 분리 + lifespan) |
| **B: legacy pipeline contract drift** | `test_pipeline_v3_e2e.py`, `test_sync_v3.py`, `test_scene_director_v2.py`, `test_scene_verify_v2.py`, `test_scene_steps_v3.py`, `test_scene_dependency_v2.py`, `test_scene_detail_v3.py`, `test_outlook_v2.py`, `test_entity_extract_v4.py`, `test_entity_filter.py`, `test_entity_review_v4.py`, `test_location_consistency.py`, `test_scene_detail_analyze_one.py` | v2/v3 기준 step order, dependency count, registry key, checkpoint schema가 현 manifest(49개)와 drift | W3 cluster A/B/C |
| **C: helper/constructor API drift** | `test_text_cleanup.py` (`clean_text` 제거됨), `test_variation_pipeline.py` (`VariationRecommender(llm_client=...)` 시그니처 변경) | 모듈 공개 API가 호환 shim 없이 변경됨 | W3 cluster C (F20/F21) |
| **D: image/variation response shape drift** | `test_images_api.py`, `test_entities_api.py`, `test_variation_pipeline.py` (이미지 부분) | still/image response shape와 개수 계약이 현재 API 응답과 어긋남 | W5 (image domain facade-first 완료 후) |

### 누적 경고 (deprecation)

- `backend/app/core/config.py`: Pydantic v1 `class Config` 스타일 (W2 F16)
- `backend/app/main.py`: FastAPI `on_event("startup")` (W2 F14)
- `backend/app/api/v1/prompts.py`: `schema_json` shadow warning
- startup 시 insecure default credentials warning (W2 F17)

### Repo green 목표

- W2 완료 후: cluster A 해소 → 예상 **~610 passed / ~45 failed**
- W3 완료 후: cluster B/C 해소 → 예상 **~620+ passed / ~13 failed**
- **W5 완료 후: 0 failed** (G5 판정 조건)

### Lane 분리 (W3-1, 2026-04-22 반영)

repo 전체 수치는 `docs/architecture/08-test-lanes.md`의 4개 lane(core/startup/image/full) 합계로 대체된다:

| lane | collected | passed | failed | skipped |
|------|-----------|--------|--------|---------|
| core | 267 | 262 | 5 | 0 |
| startup | 205 | 202 | 2 | 1 |
| image | 77 | 71 | 6 | 0 |
| full | 148 | 112 | 36 | 0 |
| **합계** | **697** | **647** | **49** | **1** |

이전 repo 전체 기준선(637p/56f/2e)과 비교해 W1/W2 반영분(+10p, -7f, -2e)이 반영되어 있다. cluster 별 해소는 lane별 failing 분포로 추적한다 (core 5 = cluster B 일부, full 36 = cluster B 나머지).

---

> 아래부터는 기존 리팩토링 subset 기록. 숫자(256)는 subset 기준임에 유의.

### 1.1 총계

| 구분 | 파일 수 | 테스트 수 | 상태 |
|---|---|---|---|
| Phase 0~3 신규 | 10 | **103** | 전부 통과 |
| 기존 (v0.5.x 이하) | 다수 | 미측정 | Pre-existing 실패 여러 건 |

**Pre-existing 실패 (phase-3 변경 무관)**:
- `tests/test_variation_pipeline.py::test_image_service_version_bumped`
- `tests/test_pipeline_e2e.py::test_health_check` — sqlalchemy 설정 이슈
- `tests/test_costume_trace.py`, `test_costume_trace_gpt.py` — SQLite 의존 (CLAUDE.md PostgreSQL 규칙과 충돌)

> 이 블록은 **2026-04-17 Phase 0 당시 historical snapshot**입니다. 현재 repo 전체 기준선은 상단 "Repo 전체 기준선 (W0 보강)" 섹션(637p/56f/2e/1s) 참조.

### 1.2 신규 테스트 파일 목록

| 파일 | Phase | tests | 커버리지 |
|---|---|---|---|
| `tests/test_step_manifest_v3.py` | 0 (재작성) + 1 (확장) | **22** | 49 step manifest 계약 (count, 의존성, label, applicability) |
| `tests/test_checkpoint_io.py` | 1 | **8** | atomic_write_json + read_json_safe 경계 케이스, tmp 고유성 |
| `tests/test_manifest_fields.py` | 1 | **16** | step_type/lifecycle/resume_sensitive/modifies_checkpoints 제약 |
| `tests/test_step_catalog.py` | 1 | **13** | StepEntry + catalog ↔ manifest downstream 일치 |
| `tests/test_applicability.py` | 1 | **6** | resolve_applicability + registry 정합 + unknown rule ValueError |
| `tests/test_settings_registry.py` | 2 | **6** | is_feature_enabled + get_model_for_step |
| `tests/test_api_endpoint_decorator.py` | 2 | **6** | dict warnings + AppError 재전파 + 500 변환 |
| `tests/services/test_checkpoint_sync_services.py` | 2 | **10** | 5 Service 각 체크포인트 없음/불완전 케이스 |
| `tests/services/test_shot_selection_service.py` | 2 | **3** | toggle AppError + 손상 manifest 전파 |
| `tests/services/test_snapshot_service.py` | 2 | **5** | list/save/restore 경계 |
| `tests/core/test_scene_analysis_dto.py` | 3 | **8** | DTO 기본 + Loader 체크포인트 경로 (shot_dependency 우선, outlook_phase3 우선 등) |

---

## 2. 커버리지 현황

### 2.1 잘 커버된 영역

**✅ Manifest 계약** (22 + 16 = 38 tests)
- 49 step 존재 확인
- step_type / lifecycle / applicability 값 제약
- 의존성 DAG (scene_detail, scene_director 등)
- deprecated 전환 (replaced_by 참조 유효성)

**✅ Step Catalog** (13 tests)
- catalog ↔ manifest downstream 완전 일치
- runner_cls 바인딩 (autorun step)
- resume_sensitive 3건 고정

**✅ Applicability Validator** (6 tests)
- always / disabled / on_demand / if_* 전부
- unknown rule → ValueError
- manifest 사용 if_* rule과 레지스트리 동기화

**✅ Checkpoint IO** (8 tests)
- atomic write (생성 / 대체 / utf-8 / tmp 청소)
- read_json_safe (없음 / 손상 / 정상)
- uuid tmp 고유성

**✅ Service 계약** (18 tests)
- Checkpoint sync 5 Service — 빈 체크포인트/미완료 스킵
- ShotSelection — AppError + 손상 전파
- Snapshot — list/save/restore 경계

**✅ DTO + Loader** (8 tests)
- DTO 기본값
- Loader shot_dependency 우선 + scene_dependency fallback
- outlook_phase3 우선 + outlook_extraction fallback
- fixed_elements 빈 리스트 제외
- shot_selection 필터링

### 2.2 커버리지 공백

**❌ Service 내부 쿼리 경로**
- EntitySyncService 실제 UPSERT 로직 (canon short_id 카운터, existing_links 추적)
- RelationSyncService visual_variant 생성
- SceneStillSyncService 398줄 (shot별 UPSERT, legacy fallback, stale 마킹)
- OutlookSyncService CharacterOutlook scene_assignments 파싱
- EpisodeProjectionService t2i_appearance_count 카운팅
- **완화**: 기존 E2E (`test_sync_v3.py`, `test_pipeline_v3_e2e.py`) 간접 커버 — 단 sqlalchemy 에러로 실행 안 됨

**❌ SceneContextLoader edge case**
- `staging_map` key 형식 (`f"{si}_{shi}"`)
- `shot_director_vr` (variant_resolved 저장)
- `beats_by_scene` beat_index 필터 (falsy 제외)
- legacy `shot_cinematography` shot_desc_map 변환
- **우선순위**: Low (Codex #8 지적, 추가 test 3~5건으로 해소 가능)

**❌ `_analyze_one` 400줄 본문**
- SceneDetailStep._analyze_one 자체는 단위 테스트 전무
- Phase 3b에서 ctx 기반 단위 테스트 추가 예정
- **우선순위**: Medium (Phase 3b 진입 전 behavior 캡처 필요)

**❌ `_build_final_scene_prompt` 211줄**
- ImageService._build_final_scene_prompt 단위 테스트 없음
- Phase 3b PromptService 분리 시 각 메서드 단위로 테스트
- **우선순위**: High (Phase 3b 진입 시)

**❌ api_endpoint 실제 FastAPI 통합**
- 현재는 데코레이터 함수 단위 테스트만 (6 tests)
- FastAPI Depends 주입 순서 + dict 응답 실제 JSON 변환은 미검증
- **완화**: 실제 4 endpoint (snapshot/toggle)에 적용되어 있어 수동 smoke로 간접 확인 가능

### 2.3 Phase별 커버리지 요약

| Phase | 신규 tests | 내부 로직 직접 커버 | E2E 의존 |
|---|---|---|---|
| 0 | 22 (manifest) | ✅ | - |
| 1 | 43 (catalog/io/applicability) | ✅ | - |
| 2 | 30 (services/registry/decorator) | △ (계약만) | 🟡 UPSERT/full sync |
| 3 부분 | 8 (dto/loader) | △ (로더만) | 🟡 _analyze_one |

---

## 3. 앞으로의 테스트 계획 (Phase별)

### 3.1 Phase 4 테스트 계획

#### 4.1 `/analyze` 내부 교체 검증
**신규 파일**: `tests/api/test_episodes_analyze_route.py`

```python
# 예시 (개념)
def test_analyze_dispatches_step_runner_not_analysis_service(client, episode_fixture, mock_submit_bg):
    """/analyze 호출 시 AnalysisService 아닌 StepRunner 경로로 dispatch."""
    resp = client.post(f"/api/v1/projects/{pid}/episodes/{eid}/analyze")
    assert mock_submit_bg.called
    # submit_background_job에 전달된 target 함수가 _run_all_bg인지 확인

def test_reanalyze_scenes_forces_scene_save_downstream(...):
    """/reanalyze-scenes: scene_save + downstream만 force 실행."""
    ...
```

#### 4.2 AnalysisService 제거 회귀 방지
- `grep -r "AnalysisService"` → 0건 테스트
- 삭제 전 `tests/test_sync_v3.py`, `test_pipeline_v3_e2e.py`의 AnalysisService 호출 부분 새 경로로 교체
- 호환성 유지 위해 `analyze_episode` 공개 함수 시그니처 동일

#### 4.3 outlook_dedup UPSERT
**신규**: `tests/pipeline/test_outlook_dedup.py`
- 중복 outlook 2건 → merge → primary 지정 확인
- ImageAsset.outlook_id 재지정 (기존 이미지 보존)
- orphan outlook 정리 (O00 제외)

#### 4.4 RelationFact UPSERT
**확장**: `tests/services/test_checkpoint_sync_services.py`
- `test_relation_upsert_preserves_existing_reason`: 기존 관계 update
- `test_relation_delete_stale`: 체크포인트에 없는 관계 삭제

#### 4.5 CharacterOutlook UPSERT
**확장**: 동일 파일
- `test_character_outlook_upsert_preserves_image_asset_ref`: ImageAsset outlook_id 보존
- `test_character_outlook_delete_stale_pair`: 체크포인트에 없는 쌍 삭제

**목표 tests 추가**: **15~20건**.

### 3.2 Phase 3b 테스트 계획

#### 3b.1 PromptService 단위 테스트
**신규**: `tests/services/test_prompt_service.py`

```python
def test_resolve_ref_roles_resolves_c01_from_scene_entities(...):
    """t2i_prompt의 C01 → scene visible_entities 중 C01 매치."""

def test_inject_fixed_elements_appends_to_prompt_tail(...):
    """fixed_elements가 prompt 끝에 붙음."""

def test_translate_if_korean_passes_english_unchanged(...):
    """영어 prompt는 LLM 호출 없이 통과."""

def test_translate_if_korean_invokes_llm_for_korean(mock_llm):
    """한국어 감지 시 LLM 호출."""
```

**목표**: 8~12 tests.

#### 3b.2 ReferenceImageService
- `generate_base_references` 기존 동작 보존 (image_asset 생성)
- `generate_composites` 분리 후 동일 결과
- O00 null outlook 보호
- **목표**: 6~8 tests.

#### 3b.3 SceneImageService
- 씬 이미지 생성 경로
- fal.ai 플래그 분기
- 최종 선택 로직
- **목표**: 4~6 tests.

#### 3b.6 `_analyze_one` ctx 기반 테스트 ⭐ 중요
**신규**: `tests/core/test_scene_detail_analyze_one.py`

```python
@pytest.fixture
def sample_ctx():
    """최소 유효 SceneAnalysisContext fixture."""
    return SceneAnalysisContext(
        project_id="p", episode_id="e",
        segments=[{"scene_index": 1, "text": "샘플"}],
        scene_visible={1: ["C01"]},
        entities={"characters": [{"short_id": "C01", "name": "주인공"}]},
        ...
    )

def test_analyze_one_fixed_elements_injected(sample_ctx, mock_llm):
    """fixed_elements가 LLM 프롬프트에 포함된다."""

def test_analyze_one_c_ve_warning_on_unknown_entity(...):
    """VE 밖 엔티티 사용 시 retry."""

def test_analyze_one_respects_user_edited_representative_moment(...):
    """_user_edited=True면 체크포인트 표현 보존."""
```

**목표**: 15~20 tests (closure → ctx 전환으로 behavior 캡처).

**주의**: _analyze_one 전환 **전에** behavior 테스트를 먼저 작성하여 regression 감지.

### 3.3 Phase 5 테스트 계획

#### 5.1 React Query 도입
- Frontend Jest/Vitest 기반 테스트
- Server state mock (fetch adapter)
- 낡은 캐시 invalidation 검증

#### 5.2 컴포넌트 분할
- 각 신규 컴포넌트별 RTL (React Testing Library) 테스트
- props 계약 명시

---

## 4. E2E 테스트 전략

### 4.1 현재 상태
- E2E 테스트 파일 존재 (`test_pipeline_v3_e2e.py` 등)
- sqlalchemy 설정 이슈로 실행 불가
- CLAUDE.md는 PostgreSQL, 기존 일부 테스트는 SQLite 의존

### 4.2 각 Phase 커밋 전 E2E 수동 검증
1. 요괴전 프로젝트로 새 에피소드 생성
2. `/analyze` → run-all 분석 실행
3. 체크포인트 파일 검증 (JSON 파싱, 필드 존재)
4. DB 검증 (EntityCanon / SceneStill / CharacterOutlook 생성)
5. 이미지 생성 (ref → composite → scene)
6. Frontend UI 확인 (씬 이미지 렌더링)

### 4.3 스냅샷 기반 회귀 방지
- 각 Phase 진입 전 **스냅샷 저장** (UI에서)
- 회귀 발견 시 스냅샷 복원

### 4.4 Phase 4 E2E 필수 항목
- 기존 AnalysisService 결과와 새 StepRunner 경로 결과 **DB diff 0** 확인
- outlook_dedup 실행 전후 이미지 asset 보존 확인
- RelationFact UPSERT 전후 relation_participant 수 확인

---

## 5. 테스트 실행 명령

### 5.1 Phase 0~3 신규 테스트 전체
```bash
cd /Users/manta/Documents/Projects/TheRoad-I1/backend && .venv/bin/python -m pytest \
  tests/test_step_manifest_v3.py \
  tests/test_checkpoint_io.py \
  tests/test_manifest_fields.py \
  tests/test_step_catalog.py \
  tests/test_applicability.py \
  tests/test_settings_registry.py \
  tests/test_api_endpoint_decorator.py \
  tests/services/ \
  tests/core/ \
  -q
```
예상: **103 passed** (< 1초).

### 5.2 smoke test
```bash
cd backend && .venv/bin/python -c "from app.main import app; print('OK')"
```

### 5.3 전체 pytest (pre-existing 실패 포함)
```bash
cd backend && .venv/bin/python -m pytest --collect-only -q 2>&1 | tail -5
```
예상: 375 collected, 8 errors (23분+).

실제 실행은 개별 파일 단위 또는 E2E 제외:
```bash
cd backend && .venv/bin/python -m pytest tests/ -q \
  --ignore=tests/test_pipeline_v3_e2e.py \
  --ignore=tests/test_sync_v3.py \
  --ignore=tests/test_costume_trace.py \
  --ignore=tests/test_costume_trace_gpt.py
```

### 5.4 Phase 3b 진입 전 behavior 캡처
```bash
# _analyze_one 현재 동작 fixture 생성
cd backend && .venv/bin/python -m pytest tests/core/test_scene_analysis_dto.py -v
# → 통과 확인 후 Phase 3b 착수
```

---

## 6. CI/CD 통합 (향후)

현재 CI 파이프라인 없음. Phase 5 시점 또는 별도 작업으로:

1. GitHub Actions `.github/workflows/test.yml` 추가
2. PostgreSQL service container
3. `.venv/bin/python -m pytest tests/` 자동 실행
4. PR 마다 103+ test 통과 gate

---

## 7. 테스트 추가 우선순위

| 우선순위 | 항목 | 예상 tests | 시기 |
|---|---|---|---|
| P0 | Phase 4: AnalysisService 제거 회귀 방지 | 5~10 | Phase 4 진입 시 |
| P0 | Phase 4: outlook_dedup / RelationFact / CharacterOutlook UPSERT | 10~15 | Phase 4 진입 시 |
| P1 | Phase 3b: `_analyze_one` behavior 캡처 (**closure → ctx 전환 전**) | 15~20 | Phase 3b 진입 **전** |
| P1 | Phase 3b: PromptService / ReferenceImageService / SceneImageService | 20~30 | Phase 3b 중 |
| P2 | Loader edge case (Codex #8) | 3~5 | 여유 시 |
| P3 | E2E 자동화 (CI) | - | Phase 5 후 |

---

## 8. 테스트 작성 원칙

1. **behavior 기반**: 내부 구현 아닌 외부 관찰 가능한 결과 테스트
2. **fixture 재사용**: `_FakeRunner`, `project_episode` 등 conftest.py 승격
3. **mock 최소화**: SQLAlchemy는 MagicMock으로 쿼리 chain만 대응, 실제 쿼리는 E2E에 위임
4. **파일 시스템**: `tmp_path` + `monkeypatch settings.projects_dir` 패턴
5. **AppError 검증**: `with pytest.raises(AppError) as exc_info: ...; assert exc_info.value.code == ...`
6. **테스트 이름**: `test_<subject>_<condition>_<expected>` (predicative)
