대시보드 폴드아웃/드릴다운 정리 + 페르소나 역린·misconduct 반응 + 게이트웨이 격리·RAG 비차단 수정

SSOT 대시보드:
- 한신대 기술분석 PDF(19쪽) 정합성 분석 + 이번 세션 발견 섹션 추가
- 섹션 폴드아웃(접기)·상단 목차(드릴다운)·모두 펼치기/접기 — 내용 보존, 레이아웃만 정리

페르소나 반응 강화('저항·반응 조절' 핵심 차별):
- PersonaCard.triggers(역린) 필드 + CCD 핵심상처 파생 역린 블록
- L0에 무례·모욕·조롱 시 현실적 동맹 균열 반응 지침

버그·성능 수정(라이브/E2E로 포착):
- 게이트웨이 페르소나 격리: --append-system-prompt를 --system-prompt(교체)로 + --exclude-dynamic-system-prompt-sections (내담자 캐릭터 붕괴·개발맥락 누출 차단)
- RAG: 임베더 동기 로드(약 7-13초)를 _warm_rag_caches 백그라운드 warm으로(세션 생성 블로킹 회귀 수정)
- voice TTS RMS 데드힌트 제거, init_state OpennessParams 파라미터객체화
- 한국어 PII(날짜·금액·주소) 마스킹 보강
- 레이아웃 시각 게이트: 폼 컨트롤 값 스크롤 오탐 제외(7/7)

검증: 백엔드 84/84, E2E 42(데스크톱 27·모바일 11·아바타 4), 시각 게이트 7/7
This commit is contained in:
Yun Chan 2026-06-27 02:30:46 +09:00
parent cb2aebd76c
commit 085460b5e0
327 changed files with 31226 additions and 1829 deletions

271
docs/guides/testing.md Normal file
View file

@ -0,0 +1,271 @@
# 테스트·검증 가이드
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` | 불필요 | 불필요 | 불필요 | 불필요 | 불필요 | 77 pass |
| 엔진 게이트웨이 테스트 | `apps/api` | `python -m pytest engine_gateway/ -q` | 불필요 | 불필요 | 불필요 | 불필요 | 불필요 | 7 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 # 앱 단위 테스트 (현재 77 pass)
python -m pytest engine_gateway/ -q # 게이트웨이 단위 테스트 (현재 7 pass)
```
수집만 빠르게 확인하려면:
```sh
python -m pytest app/ --collect-only -q # → "77 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_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` 스크립트(실측):
- `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 typecheck # tsc -b — 타입 오류 0 확인
npm run build # tsc -b && vite build — 프로덕션 번들 생성까지 확인
```
---
## 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``77 passed`.
- 의존을 갖춘 검증을 우회하지 않는다. E2E를 DB/API 없이 돌려 놓고 "그린"이라고 보고하지
않는다. 풀스택을 못 띄웠으면 **"E2E 미실행"**이라고 명시한다.
- 인메모리 degraded 기동(`db:false`)에서 페르소나 헬퍼가 실패하는 것은 환경 문제이지 코드가
통과한 것이 아니다. 환경을 고친 뒤 재실행한 결과로만 판단한다.
- 수집(`--collect-only`)·목록(`--list`)은 "있다"는 근거일 뿐 "통과했다"는 근거가 아니다.
통과 주장은 실제 실행 출력으로 뒷받침한다.