vignette/docs/guides/testing.md
2026-06-27 18:42:09 +09:00

290 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 테스트·검증 가이드
Vignette 저장소의 모든 검증 수단(백엔드 단위 테스트, 웹 타입체크/빌드, Playwright E2E)을
"무엇을 어떻게 실행하고, 어떤 의존이 필요한가" 기준으로 정리한다. 이 문서는 실제
`apps/web/package.json`, `apps/web/playwright.config.ts`, `apps/web/e2e/`,
`apps/api/app/test_*.py`, `apps/api/engine_gateway/test_*.py`,
`infra/docker-compose.yml`을 읽고 작성했으며, 명령·경로는 그대로 따라 할 수 있다.
주 환경은 Windows 11 + PowerShell이다. 아래 명령은 셸 공통(`python -m ...`, `npm run ...`)
형태로 적었고, 환경변수 지정은 PowerShell/Bash 양쪽 예시를 병기한다.
---
## 0. 한눈에 보는 검증 매트릭스
| 검증 | 작업 디렉터리 | 명령 | DB | API(8000) | 웹(5173) | 엔진GW(9099) | 브라우저 | 현재 통과 |
|---|---|---|---|---|---|---|---|---|
| 백엔드 단위 테스트 | `apps/api` | `python -m pytest app/ -q` | 불필요 | 불필요 | 불필요 | 불필요 | 불필요 | 113 pass |
| 엔진 게이트웨이 테스트 | `apps/api` | `python -m pytest engine_gateway/ -q` | 불필요 | 불필요 | 불필요 | 불필요 | 불필요 | 7 pass |
| API 타입 생성 체크 | `apps/web` | `npm run check:api-types` | 불필요 | 불필요 | 불필요 | 불필요 | 불필요 | pass |
| 웹 타입체크 | `apps/web` | `npm run typecheck` | 불필요 | 불필요 | 불필요 | 불필요 | 불필요 | — |
| 웹 빌드 | `apps/web` | `npm run build` | 불필요 | 불필요 | 불필요 | 불필요 | 불필요 | — |
| Playwright E2E(전체) | `apps/web` | `npm run e2e` | **필요(+시드)** | **필요** | 자동기동 | 일부만 | **필요** | 102 tests / 17 files |
핵심 원칙: **단위 테스트(pytest)와 타입체크/빌드는 외부 서비스 없이 단독 실행된다.**
**E2E만 풀스택(DB+API+웹+브라우저)을 요구한다.** 아래 각 절에서 근거와 절차를 설명한다.
---
## 1. 백엔드 단위 테스트 (pytest)
### 1.1 대상과 구성
- 작업 디렉터리: `apps/api`
- 테스트는 FastAPI `TestClient`와 인메모리 `store.py` 폴백으로 돌기 때문에 **Postgres/엔진
게이트웨이가 없어도 통과한다.** 별도 `pytest.ini`/`pyproject.toml` 설정은 없고 기본 수집
규칙(`test_*.py`)을 그대로 쓴다.
- `pydantic-settings``apps/api/.env`를 자동 로드한다(있으면). 단위 테스트는 `.env` 없이도
돈다.
### 1.2 실행
```sh
# apps/api
python -m pytest app/ -q # 앱 단위 테스트 (현재 113 pass)
python -m pytest engine_gateway/ -q # 게이트웨이 단위 테스트 (현재 7 pass)
```
수집만 빠르게 확인하려면:
```sh
python -m pytest app/ --collect-only -q # → "113 tests collected"
python -m pytest engine_gateway/ --collect-only -q # → "7 tests collected"
```
> 참고: 실행 중 `PendingDeprecationWarning: Please use 'import python_multipart'`
> 경고가 보일 수 있으나 무해하며 통과 결과에 영향을 주지 않는다.
### 1.3 `app/` 테스트 파일 (보안·상태머신·평가 중심)
| 파일 | 검증 영역 |
|---|---|
| `app/test_runtime_policy.py` | 런타임 정책(공개/dev 모드, dev-login 가드) |
| `app/test_session_turn_persistence.py` | 세션 턴 영속화(DB/인메모리 양쪽) |
| `app/test_evaluation_persistence.py` | fast-loop 평가 정규화 적재/복원 매핑 |
| `app/test_orchestrator_masking.py` | 오케스트레이터 PII 마스킹 게이트 |
| `app/test_state_machine_resistance.py` | 저항엔진 상태머신(openness 전이) |
| `app/test_rbac_idor.py` | RBAC / IDOR 권한 경계 |
| `app/test_auth_providers.py` | 인증 프로바이더(SSO/allowlist) |
| `app/test_persona_review.py` | 페르소나 리뷰 워크플로 |
| `app/test_voice_service.py` | 음성 캐스케이드 서비스(STT/TTS) |
| `app/test_voice_ws.py` | 음성 WebSocket 경계 |
### 1.4 `engine_gateway/` 테스트
| 파일 | 검증 영역 |
|---|---|
| `engine_gateway/test_gateway_model.py` | 게이트웨이 모델 선택·요청/응답 계약(ENGINE_MODE별) |
---
## 2. 웹 타입체크 / 빌드
### 2.1 대상과 구성
- 작업 디렉터리: `apps/web`
- `package.json` 스크립트(실측):
- `generate:api-types` → FastAPI OpenAPI export 후 `src/lib/api.gen.ts` 재생성
- `check:api-types` → FastAPI OpenAPI export 후 생성 타입 stale 여부 확인
- `typecheck``tsc -b`
- `lint``tsc -b` (현재 lint는 타입체크와 동일)
- `build``tsc -b && vite build`
- `dev``vite`
- `e2e``playwright test`
- 타입체크/빌드는 **순수 정적 검사**라 DB·API·브라우저가 필요 없다.
### 2.2 실행
```sh
# apps/web
npm install # 최초 1회 (devDependencies: typescript, vite, @playwright/test 등)
npm run check:api-types # FastAPI OpenAPI ↔ src/lib/api.gen.ts 동기화 확인
npm run typecheck # tsc -b — 타입 오류 0 확인
npm run build # tsc -b && vite build — 프로덕션 번들 생성까지 확인
```
API DTO를 바꿨다면 먼저 생성 산출물을 갱신한다.
```sh
# apps/web
npm run generate:api-types
```
### 2.3 CI 계약 게이트
GitHub Actions `.github/workflows/api-contract.yml`은 API/Web 계약 관련 파일이 바뀌는
`pull_request``master` push에서 Python 3.11 API 의존성, Node 22 web 의존성을 설치한 뒤
`apps/web``npm run check:api-types`를 실행한다. 이 게이트는 FastAPI OpenAPI와
`src/lib/api.gen.ts`의 drift만 막는 좁은 CI이며, DB·API 서버·브라우저는 필요 없다.
---
## 3. Playwright E2E (풀스택)
### 3.1 의존성 — 왜 풀스택인가
E2E는 Playwright route fixture로 앱 데이터를 대체하지 않고, **Vite `/api` 프록시를 통해 실제
로컬 FastAPI를 호출한다**(`apps/web/e2e/README.md`). 특히 공용 헬퍼
`apps/web/e2e/support.ts``fetchAvailablePersonas()`
`source === "database" && !degraded` 페르소나만 사용 가능으로 간주한다.
```ts
// support.ts
const usable = personas.filter((p) => p.source === "database" && !p.degraded);
expect(usable.length, ...).toBeGreaterThan(0);
```
**인메모리 degraded 페르소나로는 대부분의 E2E가 통과하지 못한다.** 따라서 E2E는:
1. **Postgres(pgvector pg16)** — 실제 DB가 떠 있어야 함
2. **시드 페르소나**`AUTO_SEED_PERSONAS=true`(필요 시 `ALLOW_SEED_PERSONA_FALLBACK=true`)
3. **FastAPI**`127.0.0.1:8000`에서 수동 기동
4. **Vite 웹 서버** — Playwright가 자동 기동(아래 3.3)
5. **Chromium**`npx playwright install chromium`로 1회 설치
6. **엔진 게이트웨이(9099)****일부 테스트만** 필요(3.5 참고). 레이아웃/시각 게이트는 불필요.
### 3.2 사전 준비
```sh
# (1) 브라우저 바이너리 설치 — 최초 1회
cd apps/web
npx playwright install chromium
# (2) DB 기동 — Docker Desktop 필요. infra/docker-compose.yml의 db 서비스
# (pgvector/pgvector:pg16). 전체 스택을 띄우려면:
# docker compose -f infra/docker-compose.yml up -d db
```
### 3.3 API 서버 기동 (시드 포함)
PowerShell:
```powershell
# apps/api
$env:AUTH_DEV_LOGIN_ENABLED = "true" # dev-login 허용 (support.ts가 사용)
$env:ENVIRONMENT = "dev"
$env:AUTO_SEED_PERSONAS = "true" # DB에 SEED P1~P3 적재
$env:ALLOW_SEED_PERSONA_FALLBACK = "true"
python -m uvicorn app.main:app --host 127.0.0.1 --port 8000
```
Bash:
```sh
cd apps/api
AUTH_DEV_LOGIN_ENABLED=true ENVIRONMENT=dev AUTO_SEED_PERSONAS=true \
ALLOW_SEED_PERSONA_FALLBACK=true \
python -m uvicorn app.main:app --host 127.0.0.1 --port 8000
```
> `.env`에 동일 값을 넣어두면 매번 환경변수를 줄 필요가 없다. DB 연결이 실패하면 main.py
> lifespan이 인메모리 degraded로 기동되지만(`/health` → `{"status":"degraded","db":false}`),
> 이 상태에서는 E2E 페르소나 헬퍼가 실패하므로 **E2E용으로는 반드시 DB를 붙여야 한다.**
### 3.4 E2E 실행
웹 서버는 Playwright `webServer`가 자동으로 띄운다(`PLAYWRIGHT_BASE_URL` 미설정이고
`PLAYWRIGHT_SKIP_WEB_SERVER`가 없을 때, `npm run dev -- --host localhost --port 5173`).
```sh
# apps/web — API(8000)와 DB가 떠 있는 상태에서
npm run e2e # = playwright test (전체)
npm run e2e:headed # 브라우저 표시
npm run e2e:ui # Playwright UI 모드
```
특정 스펙/그룹만:
```sh
npx playwright test e2e/session-layout.spec.ts
npx playwright test --grep "@single-run" # 직렬 실행 시나리오만
npx playwright test --grep-invert "@single-run" # 병렬 시나리오만
npx playwright test --list # 실행하지 않고 목록·개수만
```
유용한 오버라이드(`playwright.config.ts` 실측):
```sh
PLAYWRIGHT_PORT=5174 npm run e2e # 웹 포트 변경
PLAYWRIGHT_BASE_URL=http://localhost:5173 npm run e2e # 외부에 이미 뜬 웹 사용(자동기동 끔)
VITE_API_BASE=http://127.0.0.1:8000 npm run e2e # 프록시 대신 API 직접 지정
```
### 3.5 프로젝트(브라우저 프로파일) 구성
`playwright.config.ts``testDir: ./e2e`, `timeout: 30s`, `expect.timeout: 5s`,
`fullyParallel: true`로 다음 프로젝트를 정의한다.
| 프로젝트 | 뷰포트/디바이스 | 대상 grep |
|---|---|---|
| `chromium-desktop` | 1440×900 | `@single-run`·`@public-auth` 제외 전부 |
| `chromium-mobile` | Pixel 5 | `@single-run`·`@public-auth` 제외 전부 |
| `chromium-single-run` | 1280×800 | `@single-run`만(직렬) |
| `chromium-public-auth` | 1440×900 | `E2E_PUBLIC_AUTH=1`일 때만 활성, 공개 사이트 대상 |
- CI(`process.env.CI`)에서는 `retries: 2`, `workers: 1`, `list`+`html` 리포터를 쓴다.
- 실패 시 trace(첫 재시도)·스크린샷·비디오를 `node_modules/.tmp/`에 남긴다.
### 3.6 실측 테스트 개수 (현재)
`npx playwright test --list` 기준 **총 102 tests / 17 files**.
- **병렬 시나리오**: 88 tests (desktop + mobile)
- **`@single-run` 직렬 시나리오**: 14 tests (DB 영속화·세션 MVP·음성 성공경로 등)
레이아웃·시각 회귀 게이트(핵심 합격선):
| 게이트 | 스펙 | 구성 | 개수 |
|---|---|---|---|
| 세션 레이아웃 | `e2e/session-layout.spec.ts` | 4 테스트 × (desktop+mobile) | **8 / 8** |
| 시각 레이아웃 게이트 | `e2e/layout-visual-gate.spec.ts` | `@single-run`, 7개 화면 × 7개 폭 검사 | **7 / 7** |
| 레이아웃 포커스(재설계 화면) | `session-layout`·`session-review`·`admin`·`learner`·`settings`·`teacher`, `@single-run` 제외 | desktop+mobile 병렬 | **54** |
> `layout-visual-gate`는 7개 폭(390/720/861/900/1024/1280/1440)에서 가로 오버플로·잘린
> 컨트롤을 검사하고 전체 페이지 스크린샷을 `node_modules/.tmp/layout-gate/`에 남긴다.
> `session-layout`은 회기 전/활성 화면이 뷰포트를 벗어나지 않는지, 우측 패널이 코어 영역을
> 침범하지 않는지, 스트림 실패 시 미저장 전사가 남지 않는지를 검증한다.
### 3.7 공개 인증 스모크(선택)
`e2e/public-auth-turn.spec.ts`는 공개 사이트(`https://vignette.chanpaca.net`)를 직접 타격하는
옵트인 스모크로, 로컬 웹 서버를 띄우지 않는다. 절차는 `apps/web/e2e/README.md` 참고
(`E2E_PUBLIC_AUTH=1`, `npx playwright codegen ... --save-storage`로 인증 상태 캡처 후
`E2E_PUBLIC_STORAGE_STATE` 재사용). 캡처한 storage state에는 API 세션 쿠키가 들어 있으니
민감 정보로 취급한다.
---
## 4. 전체 검증 순서 권장안 (로컬)
```sh
# 1) 외부 의존 없는 빠른 검증부터
cd apps/api && python -m pytest app/ -q && python -m pytest engine_gateway/ -q
cd apps/web && npm run typecheck && npm run build
# 2) 풀스택 E2E (DB+API 준비 후)
# 터미널 A: docker compose -f infra/docker-compose.yml up -d db
# 터미널 B: cd apps/api && (3.3의 env) python -m uvicorn app.main:app --host 127.0.0.1 --port 8000
# 터미널 C:
cd apps/web && npm run e2e
```
엔진 턴 생성까지 보려면(음성/세션 MVP 등 일부 `@single-run`) 별도로 엔진 게이트웨이를
포트 9099에 띄운다(`ENGINE_MODE=claude_cli`). 게이트웨이가 없으면 `/health``engine:false`
이고, 실제 턴 생성 시나리오는 실패한다. 레이아웃/시각 게이트는 게이트웨이 없이도 통과한다.
---
## 5. 운영 원칙 — "가짜 DONE" 금지
검증 결과는 **실제로 실행해 통과한 명령**만 근거로 보고한다.
- "통과했다/완료다"라고 말하려면 그 자리에서 **실행 가능한 검증 명령과 관측된 결과(통과 개수)**를
함께 제시한다. 예: `python -m pytest app/ -q``100 passed`.
- 의존을 갖춘 검증을 우회하지 않는다. E2E를 DB/API 없이 돌려 놓고 "그린"이라고 보고하지
않는다. 풀스택을 못 띄웠으면 **"E2E 미실행"**이라고 명시한다.
- 인메모리 degraded 기동(`db:false`)에서 페르소나 헬퍼가 실패하는 것은 환경 문제이지 코드가
통과한 것이 아니다. 환경을 고친 뒤 재실행한 결과로만 판단한다.
- 수집(`--collect-only`)·목록(`--list`)은 "있다"는 근거일 뿐 "통과했다"는 근거가 아니다.
통과 주장은 실제 실행 출력으로 뒷받침한다.