세션 평가·라이브코치·교수자 분석 라운드 마감 + 문서 정리 + 코드품질 리팩터

- 누적 작업트리 커밋: 회기 평가 복구·durable 저장, 라이브 코치 이력/근거, 교수자 학생분석, 음성 비언어 메타, PII 마스킹, 운영 티켓/헬스 등
- 문서: 완료 기록 docs/archive/ 냉동 보관, docs/ 단일 인덱스(docs/README.md)+통합 TODO(docs/TODO.md)로 정리
- 리팩터(행위 보존): Stage enum SSOT(taxonomy 소유·state_machine re-export), store recent/masked_turns 중복 제거, speaker_ko_label 단일 헬퍼, _list_sessions N+1 제거(state/turns 배치 + 턴평가 하이드레이션 배치)
- 검증: 백엔드 pytest 352 passed, _list_sessions E2E chromium-single-run 2 passed
This commit is contained in:
Yun Chan 2026-07-02 02:50:36 +09:00
parent 7c41c3ce79
commit 778e8526d4
108 changed files with 6457 additions and 455 deletions

View file

@ -15,12 +15,12 @@ Vignette 저장소의 모든 검증 수단(백엔드 단위 테스트, 웹 타
| 검증 | 작업 디렉터리 | 명령 | DB | API(8000) | 웹(5173) | 엔진GW(9099) | 브라우저 | 현재 통과 |
|---|---|---|---|---|---|---|---|---|
| 백엔드 단위 테스트 | `apps/api` | `python -m pytest app/ -q` | 불필요 | 불필요 | 불필요 | 불필요 | 불필요 | 현재 309 pass |
| 백엔드 단위 테스트 | `apps/api` | `python -m pytest app/ -q` | 불필요 | 불필요 | 불필요 | 불필요 | 불필요 | 현재 collect-only 349 tests, 최신 focused pass |
| 엔진 게이트웨이 테스트 | `apps/api` | `python -m pytest engine_gateway/ -q` | 불필요 | 불필요 | 불필요 | 불필요 | 불필요 | 현재 27 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` | **필요(+시드)** | **필요** | 자동기동 | 일부만 | **필요** | 166 tests / 18 files |
| Playwright E2E(전체) | `apps/web` | `npm run e2e` | **필요(+시드)** | **필요** | 자동기동 | 일부만 | **필요** | 194 tests / 19 files |
핵심 원칙: **단위 테스트(pytest)와 타입체크/빌드는 외부 서비스 없이 단독 실행된다.**
**E2E만 풀스택(DB+API+웹+브라우저)을 요구한다.** 아래 각 절에서 근거와 절차를 설명한다.
@ -42,14 +42,14 @@ Vignette 저장소의 모든 검증 수단(백엔드 단위 테스트, 웹 타
```sh
# apps/api
python -m pytest app/ -q # 앱 단위 테스트 현재 309 pass
python -m pytest app/ -q # 앱 단위 테스트: 현재 collect-only 349 tests, 최신 focused pass
python -m pytest engine_gateway/ -q # 게이트웨이 단위 테스트 현재 27 pass
```
수집만 빠르게 확인하려면:
```sh
python -m pytest app/ --collect-only -q # 현재: "305 tests collected"
python -m pytest app/ --collect-only -q # 현재: "349 tests collected"
python -m pytest engine_gateway/ --collect-only -q # 현재: "27 tests collected"
```
@ -154,9 +154,11 @@ GitHub Actions `.github/workflows/api-contract.yml`은 API/Web 계약 관련 파
### 3.1 의존성 — 왜 풀스택인가
E2E는 Playwright route fixture로 앱 데이터를 대체하지 않고, **Vite `/api` 프록시를 통해 실제
로컬 FastAPI를 호출한다**(`apps/web/e2e/README.md`). 특히 공용 헬퍼
`apps/web/e2e/support.ts``fetchAvailablePersonas()`
Playwright suite에는 **Vite `/api` 프록시를 통해 실제 로컬 FastAPI/DB를 호출하는 full-stack
E2E**와, 특정 UI/error 상태를 고정하는 **route fixture UI 회귀 테스트**가 함께 있다
(`apps/web/e2e/README.md`). DB-backed/real-API 증거는 해당 spec이 검증 대상 endpoint를
`page.route().fulfill()`로 대체하지 않고 실제 API 응답 또는 persisted read-model을 확인한
경우로 한정한다. 특히 공용 헬퍼 `apps/web/e2e/support.ts``fetchAvailablePersonas()`
`source === "database" && !degraded` 페르소나만 사용 가능으로 간주한다.
```ts
@ -258,17 +260,43 @@ VITE_API_BASE=http://127.0.0.1:8000 npm run e2e # 프록시 대신 API
### 3.6 실측 테스트 개수 (현재)
`npx playwright test --list` 기준 **총 166 tests / 18 files**.
`npx playwright test --list` 기준 **총 194 tests / 19 files**.
- **병렬 시나리오**: 130 tests (desktop 65 + mobile 65)
- **`@single-run` 직렬 시나리오**: 36 tests (DB 영속화·세션 MVP·음성 성공경로·회기말 평가 저장·교수자 사용자별 분석 등)
- **병렬 시나리오**: 150 tests (desktop 75 + mobile 75)
- **`@single-run` 직렬 시나리오**: 44 tests (DB 영속화·세션 MVP·음성 성공경로·회기말 평가 저장·교수자 명시 재평가 저장·교수자 턴 재평가 저장·교수자 UI 평가 재시도·교수자 사용자별 분석·source pack sync·이론모드 저장·동의 철회 후 voice 차단·브라우저 stream PII 마스킹·음성 transcript 저장 실패 UI 표면화 등)
- `e2e/voice-success.spec.ts`는 직접 `/voice/ws` 캐스케이드와 Session 마이크 UI를 함께 검증하며,
브라우저 `<audio>.play()`가 차단된 조건에서도 Web Audio buffer source 재생이 시작되는지와
Session 마이크 UI가 `audio_end`에 browser voice activity/silence 메타를 싣는지 확인한다.
- 2026-07-01 focused 검증: `PLAYWRIGHT_PORT=5205 npx playwright test e2e/session-persistence.spec.ts --project=chromium-single-run --workers=1` **7 passed**. 실제 브라우저 `openSessionStream()``/sessions/{id}/stream` → DB-backed `/review` 축어록 저장 경로, AI 튜터 코칭 이력 저장/재로딩, WebSocket `stt_result` 음성 비언어 메타데이터, Session 마이크 UI가 생성한 voice activity/silence 메타, Phase 3 pre/post 점수의 DB-backed 저장/재조회, 그리고 세션 종료 background deep 평가가 durable DB row로 저장되어 교수자 리뷰가 `평가 완료`로 전환되는지 검증한다.
- 2026-07-01 focused 검증: `PLAYWRIGHT_PORT=5205 npx playwright test e2e/session-persistence.spec.ts --project=chromium-single-run --workers=1` **7 passed**. 실제 브라우저 `openSessionStream()``/sessions/{id}/stream` → DB-backed `/review` 축어록 저장 경로, AI 튜터 코칭 이력 저장/재로딩, WebSocket `stt_result` 음성 비언어 메타데이터, Session 마이크 UI가 생성한 voice activity/silence 메타, Phase 3 pre/post 점수의 DB-backed 저장/재조회, 그리고 세션 종료 background deep 평가가 durable DB row로 저장되어 교수자 리뷰가 `평가 완료`로 전환되는지 검증한다. AI 튜터 코칭 이력 검증은 `POST /live-coach` 응답과 DB-backed history payload의 `status=ready`, `latency_ms>0`도 확인해 규칙 기반 `degraded` fallback 200 응답이 정상 AI 코칭으로 통과하지 못하게 한다.
- 2026-07-01 추가 DB-backed 검증: `PLAYWRIGHT_PORT=5238 npx playwright test e2e/session-persistence.spec.ts --project=chromium-single-run --workers=1 --grep "finishes session end evaluation"` **1 passed**. 같은 자동 종료 평가 row가 `/teacher/dashboard``recent_sessions`에서도 `evaluation_status=ready`, `review_ready=true`, `supervisor_state=평가 완료`로 반영되는지 실제 DB/API/엔진으로 검증한다.
- 2026-07-01 추가 DB-backed 검증: `PLAYWRIGHT_PORT=5237 npx playwright test e2e/session-persistence.spec.ts --project=chromium-single-run --workers=1 --grep "explicit teacher session reevaluation"` **1 passed**. 실제 DB/API/엔진에서 학습자 세션과 턴을 만든 뒤 교수자 `POST /eval/sessions/{id}/reevaluate`가 durable `app.session_evaluation` row를 저장하고, `/eval/.../evaluation``/review``평가 완료`로 반영되는지 검증한다.
- 2026-07-01 추가 DB-backed 검증: `PLAYWRIGHT_PORT=5246 npx playwright test e2e/session-persistence.spec.ts --project=chromium-single-run --workers=1 --grep "teacher turn reevaluation"` **1 passed**. 실제 DB/API/엔진에서 학습자 세션과 턴을 만든 뒤 교수자 `POST /eval/sessions/{id}/turn`이 기존 turn evaluation normalized row를 durable 교체하고, `/review`의 learner turn `techniques`가 재평가 응답과 같은 라벨로 hydrate되는지 검증한다.
- 2026-07-01 추가 DB-backed UI 검증: `PLAYWRIGHT_PORT=5245 npx playwright test e2e/session-persistence.spec.ts --project=chromium-single-run --workers=1 --grep "manual AI evaluation retry"` **1 passed**. admin engine config를 잠깐 실패 endpoint로 바꿔 실제 `app.session_evaluation` 실패 row를 만든 뒤 원복하고, 교수자 리뷰 UI의 `AI 평가 재시도` 버튼 클릭이 실제 `POST /eval/sessions/{id}/reevaluate` → durable ready row → `/review` `평가 완료``/teacher/dashboard` `evaluation_status=ready`까지 이어지는지 검증한다.
- 2026-07-02 focused 검증: `PLAYWRIGHT_PORT=5255 npx playwright test e2e/kb-source-packs.spec.ts --project=chromium-single-run --workers=1` **1 passed**. 관리자 전용 `POST /kb/live-coach/source-packs/sync`가 인증 없이 401, learner 403, admin 202로 동작하고, 0615 워크북·DSM·공식 상담 지침·자살위험 지침 source pack을 DB-backed evaluator RAG에 sync한 뒤 source-scoped `/kb/eval-grounding`이 503 skip 없이 200을 반환하고 같은 `source_id`만 반환하는지 검증한다.
- 2026-07-02 focused 검증: `PLAYWRIGHT_PORT=5259 npx playwright test e2e/session-mvp.spec.ts --project=chromium-desktop --workers=1 --grep "AI tutor|stale empty AI tutor quota|quota exhaustion|degraded AI tutor|voice conversation stop"` **7 passed**. route-fixture UI에서 AI 튜터의 stale empty quota가 서버 `GET /live-coach` 재조회 뒤 실제 잔여 기회로 복구되는지, 최신 quota exhaustion 오류가 이전 코칭 카드에 가려지지 않는지, 음성 `reply.conversation_stopped`가 빈 client reply 대신 109 안전 게이트와 socket close를 유지하고 live-coach를 호출하지 않는지 고정한다. 이 fixture들은 UI 회귀 증거이며 DB-backed 엔진 성공 증거가 아니다.
- 2026-07-01 focused 검증: `PLAYWRIGHT_PORT=5251 npx playwright test e2e/session-mvp.spec.ts --project=chromium-desktop --workers=1 --grep "AI tutor|runtime AI tutor"` **4 passed**. mock MVP 코칭 카드에서 `근거 보기` 모달이 source title, `source_pack` kind, `2026-06-15` version, citation을 표시하는지 검증하고, `status=degraded` route fixture는 카드·근거 모달·이력 모달·턴 `C` 마커가 `대체 코칭`/`AI 응답 대체`를 노출하는지 고정한다. 추가 route fixture는 `GET /live-coach` 실패가 빈 이력으로 보이지 않고 alert로 표시되는지, `persistence_source/source=runtime`이 임시 저장 경고로 표시되는지도 고정한다. 이 fixture들은 UI 회귀 증거이며 DB-backed 엔진 성공 증거가 아니다.
- 2026-07-01 focused 검증: `PLAYWRIGHT_PORT=5252 npx playwright test e2e/session-review.spec.ts --project=chromium-desktop --workers=1 --grep "pre/post"` **1 passed**. 기존 저장 pre/post 점수를 비운 입력이 `저장된 값 기준`으로 오인되지 않고 invalid 상태, alert, 저장 버튼 disabled로 표면화되는지 검증한다.
- 2026-07-01 focused 검증: `PLAYWRIGHT_PORT=5248 npx playwright test e2e/session-mvp.spec.ts --project=chromium-single-run --workers=1 --grep "pending voice transcript"` **1 passed**. mock WebSocket이 `transcript final``turn_persistence_unavailable` error를 보내면 Session UI가 임시 학습자 발화를 정상 턴으로 확정하지 않고 `저장 실패` 배지와 alert로 표면화하며, 대기 중 내담자 말풍선을 제거하고 텍스트 입력을 복구하는지 검증한다.
- 2026-07-01 추가 DB-backed 검증: `PLAYWRIGHT_PORT=5241 npx playwright test e2e/session-persistence.spec.ts --project=chromium-single-run --workers=1 --grep "AI tutor coaching history"` **1 passed**. 실제 DB/API/엔진 경로에서 AI 튜터 응답이 `status=ready`, `latency_ms>0`로 저장되고, 0615 source pack metadata가 `근거 보기` 모달, DB-backed `/live-coach` 이력, reload 후 `C` 마커 history dialog에 유지되는지 확인한다.
- 2026-07-01 fixture UI focused 검증: `PLAYWRIGHT_PORT=5229 npx playwright test e2e/session-review.spec.ts --project=chromium-desktop --workers=1 --grep "retry a failed AI session evaluation|retry fails"` **2 passed**. 교수자 리뷰의 `평가 실패` 상태에서 `AI 평가 재시도` 버튼이 보이고, 재시도 성공 시 완료로 바뀌며 재시도 실패 시 기존 실패 리뷰와 새 실패 사유가 유지되는지 검증한다.
- 2026-07-01 추가 fixture UI/mixed focused 검증: `PLAYWRIGHT_PORT=5236 npx playwright test e2e/session-review.spec.ts e2e/teacher.spec.ts --project=chromium-desktop --workers=1 --grep "manual AI retry|retry a failed AI session evaluation|retry fails|failed AI session evaluation"` **4 passed**. 평가가 아직 `평가 대기`로 자동 polling 중일 때는 수동 `AI 평가 재시도` 버튼을 숨기고, `평가 실패`일 때만 재시도 버튼을 노출하며, 교수자 pending queue에서도 `하린`/`P6` 종료 회기의 `평가 실패` AI 상태가 수동 `검토 대기` 상태에 묻히지 않는지 검증한다.
- 2026-07-01 추가 fixture UI focused 검증: `PLAYWRIGHT_PORT=5253 npx playwright test e2e/session-review.spec.ts --project=chromium-desktop --workers=1 --grep "long-running session evaluation"` **1 passed**. 긴 회기 평가가 기존 짧은 polling window를 넘겨 늦게 ready가 되어도 리뷰 화면이 `평가 대기`에 고착되지 않고 `평가 완료` 리뷰를 반영하는지 검증한다.
- 2026-07-02 추가 focused 검증: `C:\Users\encep\AppData\Local\Programs\Python\Python311\python.exe -X utf8 -m pytest -p no:cacheprovider apps/api/app/test_teacher_dashboard.py apps/api/app/test_session_turn_persistence.py -k "teacher_cannot_close_review_before_ai_session_evaluation_ready or teacher_can_mark_session_review_closed_with_note or end_session_does_not_reschedule_evaluation_for_already_ended_session or session_evaluation or stale_missing" -q` **8 passed** + `PLAYWRIGHT_PORT=5254 npx playwright test e2e/session-review.spec.ts --project=chromium-desktop --workers=1 --grep "manual AI retry|long-running session evaluation|retry a failed AI session evaluation|retry fails"` **4 passed**. 오래된 missing session evaluation이 교수자 대시보드에서도 `평가 실패`로 보이고, 이미 종료된 세션의 중복 `/end`가 평가를 재예약하지 않으며, AI 평가가 ready가 아니면 교수자 리뷰를 `검토 완료`로 닫아 큐에서 숨길 수 없도록 검증한다.
- 2026-07-01 focused 검증: `PLAYWRIGHT_PORT=5229 npx playwright test e2e/session-persistence.spec.ts --project=chromium-single-run --workers=1 --grep "selected CBT theory mode"` **1 passed**. 실제 Session UI에서 `CBT` 이론모드를 선택해 시작한 회기가 `POST /sessions` payload와 DB-backed `GET /sessions/{id}` 상세의 `theory_mode=cbt`로 보존되는지 검증한다.
- 2026-07-01 focused 검증: `PLAYWRIGHT_PORT=5231 npx playwright test e2e/voice.spec.ts --project=chromium-single-run --workers=1 --grep "consent withdrawal"` **1 passed**. 동의 철회 뒤 이미 생성된 `session_id``/api/voice/ws`를 열어도 `consent_required`로 닫히는지 실제 브라우저 WebSocket으로 검증한다.
- 2026-07-01 focused 검증: `py -3.11 -X utf8 -m pytest -p no:cacheprovider app/test_dataset_export.py app/test_phase3_artifact_checker.py -q` **16 passed**. X1 exporter가 consented/client-visible `text_masked` 턴만 고르고 raw `sc.text`를 선택하지 않는지, supervisor comment raw text를 JSONL에서 제거하는지, approved/dry-run JSONL shape·row count·privacy·PII gate가 `{}` false-positive를 막는지 검증한다.
- 2026-07-01 focused 검증: `py -3.11 -X utf8 -m pytest -p no:cacheprovider app/test_evaluation_persistence.py app/test_session_turn_persistence.py app/test_live_coach_privacy.py -q` **50 passed**. H4 live coach prompt가 recent turns와 fast-loop evaluation dict의 문자열 leaf를 다시 마스킹하고, turn evaluation persistence가 rationale/comment/alternative utterance의 raw 이름·기관·전화번호를 DB insert 전 제거하며, legacy DB row의 빈 `text_masked` fallback과 session-end deep evaluation payload/read-model도 raw PII를 재마스킹하는지 검증한다. 같은 라운드에서 `test_evaluation_persistence.py``IsolatedAsyncioTestCase`로 바꿔 async persistence 테스트가 실제 await되도록 고정했다.
- 2026-07-01 추가 backend focused 검증: `py -3.11 -X utf8 -m pytest -p no:cacheprovider app/test_session_turn_persistence.py -k "fast_loop_evaluation" -q` **2 passed** + `py -3.11 -X utf8 -m pytest -p no:cacheprovider app/test_learner_dashboard.py -k "failed_fast_loop" -q` **1 passed**. fast-loop 평가 훅 예외는 상담 턴 저장을 막지 않되 `evaluation.error`와 리뷰의 `턴 평가 실패` 노트로 표면화하고, evaluator가 반환한 error dict는 성장 점수·최근 피드백에서 제외해 실패가 neutral 0.5로 집계되지 않게 검증한다.
- 2026-07-01 추가 backend focused 검증: `py -3.11 -X utf8 -m pytest -p no:cacheprovider app/test_eval_routes.py app/test_evaluation_persistence.py -q` **29 passed**. 단일 턴 `reevaluate_turn`이 결과를 응답 바디에만 두지 않고 `feedback_scores`/`turn_technique`/`turn_client_state`/`supervisor_comment`/`alternative_utterance` row를 교체 저장하며, 저장 실패는 503, 평가 error는 저장 후 502/503으로 표면화하는지 검증한다. normalized evaluation table의 delete RLS policy도 함께 고정한다.
- 2026-07-01 추가 backend focused 검증: `py -3.11 -X utf8 -m pytest -p no:cacheprovider app/test_session_turn_persistence.py -k "session_evaluation or stale_missing" -q` **3 passed**. 오래된 종료 회기가 `app.session_evaluation` row 없이 무한 `평가 대기`로 남지 않고, timeout+grace 이후 교수자 read-model에서 `평가 실패``AI 평가 재시도가 필요합니다` 사유로 표면화되는지 검증한다.
- 2026-07-01 추가 backend focused 검증: `py -3.11 -X utf8 -m pytest -p no:cacheprovider app/test_notifications.py app/test_session_turn_persistence.py app/test_evaluation_persistence.py -k "session_evaluation or missing_session_evaluation or scheduled_session_evaluation" -q` **16 passed**. startup recovery가 종료됐지만 evaluation row가 없는 오래된 DB 세션을 evaluator AI context로 찾아 기존 session-end 평가를 재예약하고, 같은 session_id 중복 background 평가를 process-local in-flight set으로 막는지 검증한다.
- 2026-07-01 focused 검증: `PLAYWRIGHT_PORT=5228 npx playwright test e2e/session-persistence.spec.ts --project=chromium-single-run --workers=1 --grep "case worksheet|crisis safety"` **2 passed**. C1 학습자 워크시트 저장→교수자 수정요청 검수와 C2 위기 신호→`app.safety_events`→DB-backed 교수자 안전 알림 큐를 실제 브라우저/API/DB로 검증한다.
- 2026-07-01 추가 DB-backed 검증: `PLAYWRIGHT_PORT=5244 npx playwright test e2e/session-persistence.spec.ts --project=chromium-single-run --workers=1 --grep "Korean PII|crisis safety event"` **2 passed**. H4는 실제 Session UI stream으로 한국어 이름·기관·전화번호가 포함된 학습자 발화를 보내고, DB-backed `GET /sessions/{id}` 상세와 `/review` 모두 raw 값 없이 `[NAME]`/`[ORG]`/`[PHONE]`으로 마스킹되는지 검증한다. C2는 위기 발화가 교수자 검토용 learner turn으로 저장되지만 client AI 응답 turn은 저장되지 않고, DB-backed 교수자 안전 큐의 109 알림이 유지되는지 확인한다.
- 2026-07-01 focused 검증: `PLAYWRIGHT_PORT=5193 npx playwright test e2e/session-mvp.spec.ts --project=chromium-single-run --workers=1` **2 passed**. MVP 종료/리뷰 흐름과 위기 안전 게이트가 빈 내담자 응답 신호에 덮이지 않는지 검증한다.
- 2026-07-01 focused 검증: `PLAYWRIGHT_PORT=5215 npx playwright test e2e/teacher.spec.ts --project=chromium-single-run --workers=1` **9 passed**. 교수자 콘솔의 페르소나 검수, 검토 큐, 최근 회기 진입과 별도 `/teach/analysis` 학생 분석 메뉴, 학습자 검색 테이블, 행 펼침, 상세 드릴다운, 전체 회기 탭 전환을 검증한다.
- 2026-07-01 focused 검증: `PLAYWRIGHT_BASE_URL=http://localhost:5210 npx playwright test e2e/teacher.spec.ts --project=chromium-single-run --grep "shows selected learner analysis"` **1 passed** + `PLAYWRIGHT_BASE_URL=http://localhost:5210 npx playwright test e2e/layout-visual-gate.spec.ts --project=chromium-single-run --grep "professor (student analysis overview|learner detail analysis)"` **2 passed**. 실제 5210 로컬 스택에서 학생 분석 목록의 사용자 표시명, 상세 기본 페르소나별 회기 탭, 페르소나 행 펼침, 전체 회기 탭 전환, 7개 폭 레이아웃 containment를 검증한다.
- 2026-07-01 focused 검증: `C:\Users\encep\AppData\Local\Programs\Python\Python311\python.exe -X utf8 -m pytest -p no:cacheprovider app/test_teacher_dashboard.py -q` **10 passed**. 교수자 대시보드 API는 `session_persistence.list_all_sessions()`로 전체 담당 세션을 읽고, 학습자 일반 목록은 `list_recent_sessions()`로 최근 100개 제한을 명시해 한 학생의 최신 회기가 다른 학생 분석 목록을 밀어내지 않는지 검증한다. 또한 ended session summary가 `app.session_evaluation``evaluation_status`, `review_ready`, `supervisor_state`, `evaluation_error`를 별도로 싣고, 평가 실패가 수동 review status와 섞이지 않는지 검증한다.
- 2026-07-01 focused 검증: `npx playwright test e2e/admin.spec.ts --project=chromium-desktop --project=chromium-mobile --workers=1 --grep "approved admin without onboarding"` **2 passed**. 승인된 `role=admin` 사용자의 세션에 `admin_access=false`, `onboarding_completed_at=null`이 남아도 `/admin``/onboarding`으로 우회하지 않고 관리 콘솔의 `/admin/*` API를 호출하는지 fixture로 고정한다.
레이아웃·시각 회귀 게이트(핵심 합격선):