vignette/docs/guides/testing.md
2026-06-28 12:20:18 +09:00

15 KiB
Raw Blame History

테스트·검증 가이드

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 불필요 불필요 불필요 불필요 불필요 178 pass
엔진 게이트웨이 테스트 apps/api python -m pytest engine_gateway/ -q 불필요 불필요 불필요 불필요 불필요 11 pass
API 타입 생성 체크 apps/web npm run check:api-types 불필요 불필요 불필요 불필요 불필요 pass
웹 타입체크 apps/web npm run typecheck 불필요 불필요 불필요 불필요 불필요 pass
웹 빌드 apps/web npm run build 불필요 불필요 불필요 불필요 불필요 pass
Playwright E2E(전체) apps/web npm run e2e 필요(+시드) 필요 자동기동 일부만 필요 113 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-settingsapps/api/.env를 자동 로드한다(있으면). 단위 테스트는 .env 없이도 돈다.

1.2 실행

# apps/api
python -m pytest app/ -q              # 앱 단위 테스트 (현재 178 pass)
python -m pytest engine_gateway/ -q   # 게이트웨이 단위 테스트 (현재 11 pass)

수집만 빠르게 확인하려면:

python -m pytest app/ --collect-only -q            # → "178 tests collected"
python -m pytest engine_gateway/ --collect-only -q # → "11 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_session_share.py 회기 리뷰 공유 URL, sanitized 공개 payload, 토큰 폐기
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_admin_ops.py 관리자 콘솔 운영 저장 모델(헬스·티켓)
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 contract·SSE 요청/응답 계약(ENGINE_MODE별)

1.5 로컬 DB smoke — 저항엔진 openness 곡선

저항엔진은 단위 테스트와 별도로 실제 API 경로와 Postgres 저장 상태를 함께 확인할 수 있다. 이 smoke는 dev-login으로 학습자 세션을 만들고, P1 상담 2개에 공감 발화/조언점프 발화를 각각 5턴씩 넣은 뒤 app.session_state, app.turns, app.turn_client_state를 직접 조회한다.

사전 조건:

  • API가 DB 연결 상태로 떠 있어야 한다.
  • apps/api/.env 또는 환경변수에 DATABASE_URL이 있어야 한다.
  • API는 dev-login과 onboarding 저장이 가능한 dev 런타임이어야 한다.
# repo root
python scripts/smoke-resistance-openness-db.py \
  --api-base-url http://127.0.0.1:8000 \
  --timeout 240 \
  --out docs/ops/resistance-openness-db-smoke-YYYY-MM-DD.json

통과 기준:

  • 공감 세션은 5턴 안에 stage=탐색으로 열리고 effective_openness가 0보다 커진다.
  • 조언점프 세션은 stage=라포, effective_openness=0.0을 유지한다.
  • DB의 turn_seqapp.turns 저장 행 수가 API 턴 수와 일치한다.

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 여부 확인
    • typechecktsc -b
    • linttsc -b (현재 lint는 타입체크와 동일)
    • buildtsc -b && vite build
    • devvite
    • e2eplaywright test
  • 타입체크/빌드는 순수 정적 검사라 DB·API·브라우저가 필요 없다.

2.2 실행

# 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를 바꿨다면 먼저 생성 산출물을 갱신한다.

# apps/web
npm run generate:api-types

2.3 CI 계약 게이트

GitHub Actions .github/workflows/api-contract.yml은 API/Web 계약 관련 파일이 바뀌는 pull_requestmaster push에서 Python 3.11 API 의존성, Node 22 web 의존성을 설치한 뒤 apps/webnpm 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.tsfetchAvailablePersonas()source === "database" && !degraded 페르소나만 사용 가능으로 간주한다.

// 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. FastAPI127.0.0.1:8000에서 수동 기동
  4. Vite 웹 서버 — Playwright가 자동 기동(아래 3.3)
  5. Chromiumnpx playwright install chromium로 1회 설치
  6. 엔진 게이트웨이(9099)일부 테스트만 필요(3.5 참고). 레이아웃/시각 게이트는 불필요.

3.2 사전 준비

# (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:

# 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:

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).

# apps/web — API(8000)와 DB가 떠 있는 상태에서
npm run e2e            # = playwright test (전체)
npm run e2e:headed     # 브라우저 표시
npm run e2e:ui         # Playwright UI 모드

특정 스펙/그룹만:

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 실측):

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.tstestDir: ./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 기준 총 113 tests / 17 files.

  • 병렬 시나리오: 96 tests (desktop + mobile)
  • @single-run 직렬 시나리오: 17 tests (DB 영속화·세션 MVP·음성 성공경로 등)
  • e2e/voice-success.spec.ts는 직접 /voice/ws 캐스케이드와 Session 마이크 UI를 함께 검증하며, 브라우저 <audio>.play()가 차단된 조건에서도 Web Audio buffer source 재생이 시작되는지 확인한다.
  • 2026-06-27 최종 로컬 풀스택 검증: PLAYWRIGHT_PORT=5174 npm run e2e 113 passed.

레이아웃·시각 회귀 게이트(핵심 합격선):

게이트 스펙 구성 개수
세션 레이아웃 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 병렬 58

layout-visual-gate는 7개 폭(390/720/861/900/1024/1280/1440)에서 가로 오버플로·잘린 컨트롤을 검사하고 전체 페이지 스크린샷을 node_modules/.tmp/layout-gate/에 남긴다. session-layout은 회기 전/활성 화면이 뷰포트를 벗어나지 않는지, 우측 패널이 코어 영역을 침범하지 않는지, 시작 후 실제 session_id URL에서 새로고침해도 활성 회기 상세가 유지되는지, 스트림 실패 시 미저장 전사가 남지 않는지를 검증한다.

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. 전체 검증 순서 권장안 (로컬)

# 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). 게이트웨이가 없으면 /healthengine:false 이고, 실제 턴 생성 시나리오는 실패한다. 레이아웃/시각 게이트는 게이트웨이 없이도 통과한다.


5. 운영 원칙 — "가짜 DONE" 금지

검증 결과는 실제로 실행해 통과한 명령만 근거로 보고한다.

  • "통과했다/완료다"라고 말하려면 그 자리에서 **실행 가능한 검증 명령과 관측된 결과(통과 개수)**를 함께 제시한다. 예: python -m pytest app/ -q100 passed.
  • 의존을 갖춘 검증을 우회하지 않는다. E2E를 DB/API 없이 돌려 놓고 "그린"이라고 보고하지 않는다. 풀스택을 못 띄웠으면 **"E2E 미실행"**이라고 명시한다.
  • 인메모리 degraded 기동(db:false)에서 페르소나 헬퍼가 실패하는 것은 환경 문제이지 코드가 통과한 것이 아니다. 환경을 고친 뒤 재실행한 결과로만 판단한다.
  • 수집(--collect-only)·목록(--list)은 "있다"는 근거일 뿐 "통과했다"는 근거가 아니다. 통과 주장은 실제 실행 출력으로 뒷받침한다.