# 테스트·검증 가이드 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` | 불필요 | 불필요 | 불필요 | 불필요 | 불필요 | 2026-08-28 전체 실행 1002 passed | | 엔진 게이트웨이 테스트 | `apps/api` | `python -m pytest engine_gateway/ -q` | 불필요 | 불필요 | 불필요 | 불필요 | 불필요 | 2026-08-28 전체 실행 68 passed | | 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` | **필요(+시드)** | **필요** | 자동기동 | 일부만 | **필요** | 현재 수집 1138 tests / 62 files · 현 작업트리 전체 GREEN 미검증 | 핵심 원칙: **단위 테스트(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 # 앱 단위 테스트: 2026-08-28 전체 실행 1002 passed python -m pytest engine_gateway/ -q # 게이트웨이 단위 테스트: 2026-08-28 전체 실행 68 passed ``` 수집만 빠르게 확인하려면: ```sh python -m pytest app/ --collect-only -q python -m pytest engine_gateway/ --collect-only -q ``` 수집량은 위 명령의 현재 출력으로 확인한다. `--collect-only` 숫자는 통과 수가 아니므로 실행 결과와 섞어 기록하지 않는다. > 참고: 실행 중 `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 경계 | | `app/test_session_digest_worker.py` | M2 session digest worker 요청 계약, accepted-only 적용, raw text 차단, 재실행 방지 | | `scripts/check-dev-dashboard-ssot.py --json` | `docs/dev_dashboard.html` 상태 카운트·M2 검증 수치·DONE/GATE stale 문구 guard | #### 개선관리 워크북 focused 증거 (2026-08-27~28) 아래는 전체 1002/68과 별도로 해당 계약을 좁혀 실행한 확정 증거다. 같은 묶음의 테스트가 여러 요구사항 경계를 함께 검증할 수 있으므로 숫자를 요구사항별 전체 합계로 더하지 않는다. | 항목 | focused 명령/파일 | 확인 결과 | |---|---|---| | C-001 기술 사전검증 | `scripts/check-clinical-crisis-review.py`, `run-clinical-crisis-technical-observations.py`, checker/client reply/source pack/state machine/session focused pytest | 핵심 42 passed + client/session 55 passed + 상태·메모리·페르소나·orchestrator 인접 55 passed(경고는 Starlette 1건) — P1 생성·스트림 실제 경로 6/6, 강한 양성·부정·메타·타인·혼합문 확장 경계 50건, 런타임 패키지 SHA 일치를 검증했다. DB direct load·baseline·carry-over·observed 값을 `1..3`으로 정규화하고 과상한 출력은 재생성한다. 수련생 실제 위기는 엔진 전 중단·109, 내담자 수단 상세는 차단한다. 6시트 개선관리 완료본은 수식 오류 0이며 외부 사례 판정·승인 증거가 없으면 `검토`를 유지한다. 현재 `pending_external_review`; 외부 임상 승인을 대체하지 않음 | | C-002 | `app/test_first_session_checklist.py` | 3 passed — 첫 회기 라포 반영·한 초점 열린 질문, evidence turn, 이후 회기 not-applicable | | C-003 | `app/test_learner_dashboard.py` | 11 passed — 종료 회기 4건 전에는 insufficient, 이후 dominant share 0.75 경계 | | REQ-001 | `app/test_auth_providers.py`, `app/test_access_logging.py`, OAuth UI focused E2E, `e2e/admin.spec.ts`/`e2e/uc-admin-console.spec.ts` | auth 41 + access-log redaction 6 pytest, OAuth UI desktop/mobile 4, route-mock browser 2, 실제 DB/browser 1 passed — 모든 provider-verified Google 이메일을 도메인·사전등록 없이 learner·approved로 허용하고 suspended는 보존. callback query는 Uvicorn access log에서 제거. 관리자 사전등록 create는 pending 고정이며 승인 전 `/personas` 403·`/learn→/pending`, 별도 PATCH 승인 뒤 같은 세션 `/personas` 200, `learner_feedback_enabled=false` 영속 재조회, 비활성화·활성 세션 0 | | REQ-002·005 | `app/test_protocol_registry.py app/test_persona_session_contract.py` | 26 passed(경고 1); persona pin·ideation runtime clamp 계약 단독 4 passed(경고 1) | | REQ-002 실제 DB | `e2e/persona-db-lifecycle.spec.ts`의 protocol lifecycle | 1 passed(10.1s) — draft 생성→RAG 활성화→폐기와 DB 정리 | | REQ-003·005 실제 DB | `e2e/persona-db-lifecycle.spec.ts`의 persona lifecycle | 1 passed(14.4s), mock/skip 0 — 교수자 작성→검수 승인→catalog v1, complaint 우선 주호소·surface 미노출, 학습자 prestart, exact persona ID/version session pin, controlled 실제 SSE, UI exact reply, DB `2|counselor,client`; 종료 후 사용자·페르소나·세션·설정 marker 0과 엔진 설정 exact 원복 | | REQ-003 | `app/test_persona_review.py` | 34 passed — complaint 우선 read model과 surface 오인 방지 포함 | | REQ-004 | `app/test_feedback_policy.py` 포함 직접 정책 subset, 관련 route/read-model, `e2e/session-review.spec.ts` desktop·mobile | 85 + 163 pytest, browser 2 passed — learner OFF 직접 API 403, 과거 OFF source가 섞인 calibration 집계의 learner-input-only redaction, deliberate-practice 원천 snapshot OFF 집계·제출 403, alliance self-scores-only, 파생 endpoint별 요청 0회, 입력·privacy 보존, 삭제 조작면 44px·overflow 0 | | REQ-006 | `app/test_pii_masking_eval.py app/test_live_coach_sources.py app/test_client_reply_quality.py` | 24 passed — raw prompt와 구조화 결과의 counselor/client role masking | | REQ-007 | session persistence/evaluation 관련 API 9 files, live/session 묶음, 실제 DB persistence E2E | 163 + 50 pytest, DB E2E 1 passed — 실패한 옛 평가 뒤 새 회기 가능 | | REQ-008 | `app/test_engine_health_contract.py`와 session stream 회귀 | health contract 1 passed; incomplete EOF는 live/session 50 묶음에 포함 | | 학습자 성장 패널 헤더 | `uc-learner-home-dashboard.spec.ts --grep "성장 지표 라포 헤더"` + `check:design-ssot` + `check:cosmetic-filter-safety` + typecheck | chromium desktop/mobile 2 passed — 선택된 라포 문구의 부모는 plain `div`, 배경 transparent·border/radius/padding 0이며 주변 panel 구조는 유지 | | REQ-001 공개 운영 | `https://vignette.chanpaca.net/login` + Google account chooser/callback | 로그인 DOM ‘모든 Google 계정’, `allowed_email_domains=[]`, twentyoz.kr/Gmail 동시 노출, 기존 Google 계정 callback→`/admin` 성공. 신규 Gmail 선택은 계정 생성 행동시점 확인 뒤 별도 실행 | | REQ-005·007·008 공개 운영 | P20 회기 `03dddadd-adf3-4922-acbf-31002470da52` | 생성→자기점검 원장 잠금→실제 학습자/AI 내담자 2턴→종료→AI 평가 완료→P20 1회 기록 영속→다음 회기 버튼 재노출 | | 운영 배포·복구 | live clean `2a396361…89c`, Pages `1f1ddf18-9d5c-4d83-95a7-8cd71245e646`; recovery task `99779a6a…fbc8` | local/public prod·db·engine·voice·web 정상. recovery root/tree/script pin, 60분·IgnoreNew·mutex·hard-down 즉시복구 계약 50 passed; watchdog·boot 수동 실행 결과 0. 실제 Windows 재부팅 smoke와 새 Pages 승격은 미실행 | 로컬 Playwright는 기존 포트의 서버를 기본 재사용하지 않는다. 공개 preview나 다른 checkout의 stale bundle을 현 작업트리 증거로 오인하지 않도록 충돌 시 fail-closed하며, 동일 dev server를 의도적으로 공유할 때만 `PLAYWRIGHT_REUSE_EXISTING_SERVER=1`을 명시한다. Windows/npm 11에서는 Vite host/port를 등호형 인자로 전달한다. REQ-005의 `persona_id/persona_version` API 계약과 영속 pin은 focused 테스트와 실제 DB 브라우저 lifecycle에서 모두 확인했다. 통합 실행은 승인 catalog의 exact ID/version, controlled 실제 SSE, UI 응답, DB 2턴과 cleanup을 함께 고정한다. ### 1.4 `engine_gateway/` 테스트 | 파일 | 검증 영역 | |---|---| | `engine_gateway/test_gateway_model.py` | 게이트웨이 모델 선택·공유 engine contract·SSE 요청/응답 계약(ENGINE_MODE별) | | `engine_gateway/test_provider_registry.py` | Claude/Anthropic/Codex/Agy capability 정규화, 기본값, CLI 인자, 잘못된 모델·추론 강도 차단 | --- ### 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 런타임이어야 한다. ```sh # 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_seq`와 `app.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 여부 확인 - `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 # cosmetic-filter namespace gate → 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 의존성 — 왜 풀스택인가 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 // 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 # 전체: fixture 병렬 단계 → DB/engine 직렬 단계 npm run e2e:list # 두 단계의 수집 목록·개수만 확인 npm run e2e:parallel # desktop/mobile route-fixture, workers=4 npm run e2e:single-run # DB/engine @single-run, workers=1 npm run e2e:headed # 브라우저 표시 npm run e2e:ui # Playwright UI 모드 ``` 특정 스펙/그룹만: ```sh npx playwright test e2e/session-layout.spec.ts npx playwright test --project=chromium-single-run --workers=1 --grep "검색어" npx playwright test --grep-invert "@single-run" # 병렬 시나리오만 npm run e2e: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`로 다음 프로젝트를 정의한다. fixture 프로젝트는 `fullyParallel:true`로 실행한다. `npm run e2e`는 로컬 단일 API/DB/engine 자원을 보호하기 위해 desktop/mobile 단계를 `workers=4`로 먼저 끝내고, `chromium-single-run`을 `fullyParallel:false`, `workers=1`로 두 번째 실행한다. | 프로젝트 | 뷰포트/디바이스 | 대상 grep | |---|---|---| | `chromium-desktop` | 1440×900 | `@single-run`·`@public-auth` 제외 전부 | | `chromium-mobile` | Pixel 5 | `@single-run`·`@public-auth` 제외 전부 | | `chromium-single-run` | 1280×800 | `@single-run`만(별도 2단계, workers=1 직렬) | | `chromium-public-auth` | 1440×900 | `E2E_PUBLIC_AUTH=1`일 때만 활성, 공개 사이트 대상 | - CI(`process.env.CI`)에서는 `retries: 2`, `workers: 1`, `list`+`html` 리포터를 쓴다. - 로컬 fixture 단계의 기본 worker는 4다. 전체 러너에서만 `E2E_PARALLEL_WORKERS`로 조정할 수 있고, 12-worker 전면 병렬은 단일 FastAPI/DB가 포화되어 서로 무관한 요청 timeout을 만들므로 기준선으로 쓰지 않는다. - `npx playwright test`를 직접 실행하면 로컬에서 세 프로젝트가 같은 worker pool에 섞일 수 있다. 전체 게이트는 반드시 `npm run e2e`, focused DB/engine 게이트는 프로젝트와 worker를 명시한 `npx playwright test --project=chromium-single-run --workers=1 --grep "..."`을 사용한다. - 실패 시 trace(첫 재시도)·스크린샷·비디오를 `node_modules/.tmp/`에 남긴다. ### 3.6 실측 테스트 개수 (현재) 2026-08-28 `npx playwright test --list` 기준 **현재 수집 1138 tests / 62 files**다 (유스케이스 16테마 `uc-*.spec.ts` 239 시나리오 포함). 이 숫자는 수집량이지 통과량이 아니다. 현 작업트리 전체 1109개 완주는 아직 증거가 없으며, 과거 전체 GREEN 기록과 이번 focused/release gate 결과를 구분해 적는다. NAS-origin 112건은 전부 실 API/DB 회기가 아니다. release receipt는 `session-layout`·`session-persistence` 22건을 `real_api_db_specs`, 나머지 route fixture 90건을 `route_fixture_specs`로 분리한다. 학생 동일 계정의 홈 추천→브라우저 회기 생성→실제 SSE→review ready→G4/G5 POST·reload→0→1→1은 `scripts/run-periodic-learner-e2e.py --execute`가 전용 disposable engine/DB/API/Web에서만 실행한다. 이 runner는 clean HEAD/tree, 로컬 npipe Docker, loopback 동적 포트, sentinel과 exact cleanup을 강제하며 공개 8001·DB 55432·engine 9099와 NAS를 구조적으로 거부한다. 최초 세 full run은 하네스 결함, 4차는 reload 뒤 persisted episode를 UI에 hydrate하지 못하는 제품 결함을 각각 fail-closed로 검출했다. 5·6차는 핵심 동일 학습자 SSE·review·G4/G5 `0→1→1`을 연속 통과했지만 fixture 공유와 mobile success-state locator를 각각 검출했다. 보정 후 7차 clean `aa81af29…`는 same-learner SSE·review·G4/G5 `0→1→1`, 별도 zero-state returned desktop/mobile 4/4, route mock 0, exact cleanup 0으로 첫 전체 GREEN receipt를 만들었다. `94666192…`와 `f97e7fad…` run은 disposable DB init에서 멈췄고, 두 번째 receipt의 cleanup 전 로그는 detached `infra/db/init/99_app_role.sh`가 CRLF라 `/usr/bin/env: ‘bash\r’: No such file or directory`로 exit 127이 난 것을 확정했다. Windows `core.autocrlf=true`에서도 shell entrypoint를 LF로 checkout하도록 `.gitattributes`에 `*.sh text eol=lf`를 뒀다. 공통 톱바 desktop/mobile 44px을 포함한 clean `b02bee26…`·tree `8f677bba…`의 single full run은 487.3초에 GREEN이었다. 최신 receipt `periodic-learner-e2e-20260812-171547.json`·SHA-256 `8c11a136…af8e3`은 same-learner SSE·review· G4/G5 `0→1→1`, returned desktop/mobile 4/4, route mock 0, 보호 runtime 접촉 0과 exact cleanup 0을 증명한다. hourly heartbeat는 최신 GREEN이 6시간 이상 오래됐거나 material milestone이 바뀔 때만 다시 실행한다. - **병렬 시나리오**: 166 tests (desktop 83 + mobile 83) - **`@single-run` 직렬 시나리오**: 49 tests (DB 영속화·세션 MVP·음성 성공경로·인증 시각 테마·회기말 평가 저장·교수자 명시 재평가 저장·교수자 턴 재평가 저장·교수자 UI 평가 재시도·교수자 사용자별 분석·source pack sync·이론모드 저장·동의 철회 후 voice 차단·브라우저 stream PII 마스킹·음성 transcript 저장 실패 UI 표면화 등) - 2026-08-06 Outcome & Alliance OS G0 검증: `test_measurement_contract.py` + runtime schema SSOT **11 passed**, 기존 세션·평가·메모리·RBAC backend **100 passed**, auth cohort 계약 **39 passed**, Python 생성 계약 `--check`와 TypeScript measurement/API 계약·typecheck 통과. `scripts/check-measurement-ledger.sql`을 실제 PostgreSQL owner 연결에서 실행해 learner/client/evaluator 가시 행이 각각 1/1/2, 교차 view 누수 0, append-only UPDATE/DELETE guard 2건임을 확인했다. DB-backed `session-persistence`의 학습자 턴→교수자 대시보드, 워크시트 저장→수정요청 검수, 종료 deep 평가→durable 교수자 리뷰 focused E2E는 각각 **1/1 passed**. 명시 cohort가 없는 새 DB에서도 이 증거가 재현되도록 dev-login E2E 요청만 공통 `e2e-hanshin` cohort를 고정하며 생산 OAuth/SAML·관리 사용자 cohort는 우선 보존한다. - 2026-08-06 Outcome & Alliance OS G1 검증: alliance service/calibration/routes/runtime/contract focused backend **40 passed**. `scripts/smoke-alliance-pulse-api.py`는 실제 DB/API/엔진에서 `202 → awaiting_agents → ready`, 공개 전 외부 관점 비노출, terminal 3관점×3축 9개, 동일 cohort 교수자 조회, 교수자 재평정 2회의 append-only supersession, 시도별 model-run provenance를 단언한다. `e2e/alliance-pulse.spec.ts`는 데스크톱· 모바일 **6/6**, 기존 `session-review.spec.ts`는 같은 두 프로젝트 **12/12**를 통과했다. prompt `1.2.0` 합성 gold 비교는 방향 정확도 **9/9**, 최종 실패 **0**, 근거 recall **88.9%**이며 첫 시도 실패 2건의 오류·재시도 provenance도 보존했다. 비교 산출물은 `ops/alliance-calibration-report-prompt-1.2.0-2026-08-06.md`다. - 2026-08-07 G1 실제 회기 checkpoint 보강: 최신 source API와 dev PostgreSQL에서 첫 발화 전 pre는 `degraded/insufficient_transcript`로 fail-closed, 첫 실제 왕복의 durable turn UUID 2개를 쓰는 mid와 DB 종료 뒤 post는 `ready`로 통과했다. 같은 응답 안의 `awaiting_agents`/external-event read skew를 차단하고 degraded terminal/audit DB 제약을 교정했다. post는 최초 202, 동일 payload 안정 replay, 변경 payload 409, RLS/cohort와 교수자 3축 supersession을 재확인했다. focused G1 **43 passed**와 Ruff 통과, 기계 판독 결과는 `ops/evidence/alliance-pulse-pre-mid-post-live-2026-08-07.json`이다. - 2026-08-06 Outcome & Alliance OS G2 검증: contract/store/schema와 G3 통합 focused backend **90 passed**, durable review UUID 회귀 **37 passed**. `scripts/smoke-outcome-trajectory-api.py`는 실제 DB/API/엔진에서 한 case의 S1~S5 연속성, 15개 독립 축, total score 부재, 동일 제출 measurement ID 멱등성, 변경 payload 409, 같은 cohort 교수자 조회, 다른 학습자·교차 cohort 차단, 역할별 관계 메모리, 합성·비임상 고지를 단언했다. `e2e/outcome-trajectory.spec.ts` desktop/mobile은 체크인 포함 **12/12**, G1 Alliance Pulse와 기존 Session Review 회귀는 **15/15**를 통과했다. 상세 metadata는 `ops/outcome-trajectory-live-integration-evidence-2026-08-06.md`가 소유한다. - 2026-08-07 G2 악화/false-alert 실 DB 보강: `scripts/smoke-outcome-trajectory-alerts.py`는 합성 교육용 두 case의 실제 10회기와 production observation producer가 쓴 30개 `measurement_event`를 evaluator가 소비하는지 확인한다. 악화 case는 S3 `off_track`·S4~S5 `deteriorating`, 단발성 noise control은 S3 `watch` 뒤 S4~S5 `on_track`이며 alert 승격 0건이다. case별 revision 5개·supersession 4개와 최신 observation 30개→measurement 30개 provenance, learner/teacher projection 일치, 합성·비임상 표시를 단언하고 결과 JSON에는 source score를 기록하지 않는다. G2 focused **40 passed**, Ruff·`py_compile`·실 DB smoke 통과. 동시 G8 변경 수렴 뒤 전체 API도 **855 passed**를 통과했다. - 2026-08-06 Outcome & Alliance OS G3 검증: 11-case/9-type 합성 benchmark의 rupture type macro-F1 **1.0**, repair status accuracy **1.0**, critical miss·judge gaming·memorized phrase false resolution **0**. Scenario Director와 runtime/store/session focused **107 passed**, G0~G8 계약·store·runtime 묶음 **251 passed**. 실제 DB/API 회기 smoke는 durable turn UUID 2개, 독립 내부 토큰 write, 동일 payload 멱등 ID, 변경 payload 409, learner/cohort RLS, 교수자 append-only correction과 최종 resolved를 확인했다. G1~G3 및 기존 리뷰 desktop/mobile 회귀는 **50/50 passed**. 상세는 `ops/rupture-repair-live-integration-evidence-2026-08-06.md`가 소유한다. - 2026-08-07 G3 자동 runtime 보강: `scripts/smoke-rupture-runtime-auto.py`는 외부 client/evaluator만 결정론 seam으로 교체하고 실제 `sessions.submit_turn`·공용 `finalize_completed_turn` hook을 호출한다. rupture 저장 endpoint를 호출하지 않은 채 로컬 PostgreSQL에 missed/partial/resolved 3 episode, observation **10개**, reconciliation **3개**, structured learner evaluation **5개**, 세션 밖 evidence 참조 **0건**, 세션별 fast/deep model-run **2개**를 단언하며 learner/teacher read projection도 검증한다. focused G3 runtime/store/core/scenario는 **70 passed**다. 재현 JSON은 `ops/evidence/rupture-runtime-auto-live-2026-08-07.json`이다. 이 실 DB 실행은 텍스트 learner-turn transport를 증명하며 SSE·음성 개별 transport live smoke까지 증명했다고 확장하지 않는다. - 2026-08-06 Outcome & Alliance OS G4 검증: deliberate-practice core/store/API focused **54 passed**, 5개 연습 모드와 약점·망각 우선 선택, 익숙한 성공/자기 성공 주장만으로 mastery를 열지 않는 전이 게이트를 고정했다. 실제 DB/API 회기 smoke는 durable turn UUID 2개를 prescription/attempt evidence로 사용하고 내부 토큰 처방 write, 동일 제출 안정 ID, 변경 payload 409, learner/cohort RLS, 교수자 append-only correction, 총점·XP 없음, `clinical_claim_allowed=false`를 확인했다. G4 desktop/mobile은 **8/8**, G1~G4와 기존 회기 리뷰 통합 회귀는 **58/58 passed**. 상세는 `ops/deliberate-practice-live-integration-evidence-2026-08-06.md`가 소유한다. - 2026-08-06 Outcome & Alliance OS G5 검증: calibration-transfer core/store/API focused **41 passed**, 4-case benchmark expectation accuracy **1.0**, 잠금 뒤 오염 거부 **1**, 반복 보정 MAE **0.667→0.267**, unseen varied transfer 4/4, 암기 문구 false verification **0**, 합성 subgroup drift gap **0.667**을 확인했다. 실제 DB/API smoke는 durable turn prediction revision→불가역 lock→독립 runtime observation reveal, 동일 제출 안정 ID·변경 payload 409, post-lock revision 422, learner/supervisor projection과 교차 역할/cohort 차단을 검증했다. G5 learner/teacher desktop/mobile **4/4**, G1~G5+기존 리뷰 **62/62**, typecheck·build 통과. 상세는 `ops/calibration-transfer-live-integration-evidence-2026-08-06.md`가 소유한다. - 2026-08-07 G4/G5 production caller 실 DB 검증: `scripts/smoke-session-learning-producer.py`는 실제 종료 회기·durable turn UUID 2개·ready session evaluation을 만들고 `session_learning_producer`만으로 G4/G5 파생 원장을 생성한다. 잠금 전 G4 submission/card/prescription/snapshot/decision 각 1개와 G5 observation 0개, prediction lock callback 뒤 failed 독립 observation·turn/session provenance model-run 각 1개를 확인했다. 재실행은 G4 deterministic submission/card replay이며 G5·model-run 중복 0이다. episode/attempt, mastery/pass, calibration assessment, transfer suite/trial/assessment는 모두 0이다. learner/teacher projection 일치와 `clinical_claim_allowed=false`도 단언한다. focused **100 passed**, 전체 API **855 passed**, Ruff· `py_compile`·실 PostgreSQL smoke 통과. 상세는 `ops/outcome-os-g4-g5-production-caller-evidence-2026-08-07.md`가 소유한다. - 2026-08-07 학생 자기주도 전체 루프 UI 검증: `self-directed-learning-loop.spec.ts`는 실제 `src` 화면에서 학습 홈 추천→목표 선택→텍스트/SSE 회기→종료·리뷰→G4 처방 CTA→새 회기 사전 설정·시작을 하나의 상태 흐름으로 묶고 desktop/mobile **2/2**를 통과했다. 미소유 `/api/**`는 전부 fixture 404이고 `auth/me`·세션·리뷰·practice read model도 route fixture이므로 실제 로그인/API/DB 증거가 아니다. 390×844·320×568 활성 회기를 포함한 9장 시각 QA, 내부 criterion/counterevidence/UUID 비노출, 홈 CTA 44px·4.5:1 대비와 source API/DB 증거의 분리 기준은 `ops/outcome-os-self-directed-learning-loop-ui-evidence-2026-08-07.md`가 소유한다. - 2026-08-06 Outcome & Alliance OS G6 검증: supervision/research core/store/API/runtime-schema focused **28 passed**, 6-signal attention queue 순서 정확, item별 drilldown 1/상한 3, metadata-only calibration dataset raw transcript row 0, version drift `drift_flagged`, Phase 3 네 도메인 manifest를 고정했다. 실제 DB/API smoke는 두 개의 durable practice attempt UUID를 원장 pointer로 사용해 내부 토큰, 동일 제출 안정 ID, 변경 payload 409, supervisor/research AI-view 분리, teacher/learner/cross-cohort RLS, teacher audit를 검증했다. role-safe research read model은 model/prompt/instrument version, subgroup metrics, artifact provenance를 hydrate한다. G6 desktop/mobile **6/6**, typecheck·build 통과. 상세는 `ops/supervision-research-live-integration-evidence-2026-08-06.md`가 소유한다. - 2026-08-07 G6 scheduled producer 보강: G0~G5 원장 기반 파생과 repo-approved synthetic version evaluator focused **37 passed**. 실제 dev DB에서 6 cohort cycle 실패 0, measurement anchor 12개, baseline/candidate batch 2·drift report 1·subgroup 2, replay 중복 0, evaluator/prompt/instrument provenance hydrate를 확인했다. 원문 transcript와 임상 주장은 0이다. 상세는 `ops/outcome-os-g6-ledger-producer-evidence-2026-08-07.md`와 `ops/outcome-os-g6-version-comparison-producer-evidence-2026-08-07.md`가 소유한다. - 2026-08-06→07 Outcome & Alliance OS G7 검증: 동의→단일 오디오 시계→독립 text/voice/fusion→철회· tombstone의 실제 HTTP/DB 경계와 역할 안전 UI를 통과했다. 공개 배포 뒤 G7 API route, TLS 1.3 WSS handshake와 비인증 1008 차단을 확인했다. Deepgram streaming adapter/interim/final/word timestamp, HMAC word pseudonym, 1초 동의 재검사, 음성 recovery UX와 `ready` provider/model 계약은 code/internal 완료다. public preflight는 마이크 없이 authenticated ready/ping과 기대 STT/TTS provider/model 네 값만 검사한다. 실제 Deepgram key·quota live, 명시 동의 물리 마이크, authenticated public ready, 실행된 50분 soak와 운영 TTS 상업 이용권 증거는 외부 게이트이며 synthetic/preflight 증거로 대체하지 않는다. - 2026-08-06→07 Outcome & Alliance OS G8 검증: source→독립 red-team→benchmark→human gate, baseline/threshold/provenance/rollback 4종 증거, lifecycle과 incident DAG를 실제 API/DB에서 통과했다. clean release backend **362 passed**, 회기·Outcome desktop/mobile **126 passed / 2 skipped**, layout visual gate **15/15**, typecheck·API contract·build를 통과한 뒤 API 8001과 Cloudflare Pages production에 배포했다. 공개 OpenAPI **119 paths**에서 G1~G8 route, prod/db/engine health, auth 401, Google OAuth 302+state cookie, custom-domain 새 asset과 브라우저 로그인 렌더를 확인했다. 상세는 `ops/outcome-os-public-deployment-evidence-2026-08-07.md`가 소유한다. - 2026-08-07 Outcome & Alliance OS 배포 프리플라이트: Compose profile과 별도로 `direct-runtime` profile을 추가했다. `scripts/provision-outcome-os-runtime-secrets.py`는 G3~G8 토큰 6개만 원자적으로 생성·회전하고 값을 출력하지 않는다. 운영 `apps/api/.env`에서 필수키와 `DATABASE_URL`, `current_user=vignette` non-owner app-role 검사를 통과했다. 상세는 `ops/outcome-os-deploy-preflight-evidence-2026-08-07.md`가 소유한다. - 2026-08-07 G7 로컬 `/voice/ws` short soak: loopback Uvicorn의 실제 FastAPI WebSocket transport에서 8.015초·83/83 연결, 성공 발화 79, 정상/인증 거부/provider degraded close, 같은 socket 철회와 재연결 차단, 4KiB 실행 한정 overflow 뒤 복구를 확인했다. 관련 42 tests와 API/gateway 전체 **805 passed**. 외부 provider·실DB·물리 마이크·public WSS는 사용하지 않았고 50분 옵션은 제공만 했으므로 B2는 유지한다. 상세는 `ops/outcome-os-g7-wss-soak-evidence-2026-08-07.md`가 소유한다. - 2026-08-07 G7 후속 hardening: voice service/WS/G7 store·API focused **73 passed**, public runner **12 passed**. `scripts/soak-public-voice-websocket.py --preflight-only`는 auth cookie·소유 회기와 아래 네 기대값을 필수로 받고 WSS ready/ping까지만 수행한다. 마이크 장치 열거·캡처가 없으므로 물리 음성 증거로 판정하지 않는다. ```powershell C:\Users\encep\AppData\Local\Programs\Python\Python311\python.exe -X utf8 ` scripts\soak-public-voice-websocket.py --preflight-only ` --expected-stt-provider local_whisper --expected-stt-model small ` --expected-tts-provider melotts --expected-tts-model melotts-korean ``` 50분 유효 공통창 full soak는 같은 기대값에 `--duration-seconds 3120`, `--microphone-device <명시-장치>`, `--confirm-physical-capture`를 추가하고 사용자가 실행 직전에 물리 캡처에 명시 동의한 경우에만 실행한다. - 2026-08-07 G7 external-proof readiness: Session 첫 음성 사용은 서버 30일·원음 미보존 consent ledger가 성공하기 전 `getUserMedia`와 voice WebSocket을 시작하지 않는다. controlled provider와 synthetic stream을 사용한 `voice-success.spec.ts` single-run **2/2**에서 원장 지연 중 둘 다 0회, 성공 뒤 각각 정확히 1회, 지연 권한 뒤 pause 시 track stop·WS 0을 확인했다. API voice/G7 focused **92 passed**, external evidence runner/checker와 release gate contract **31 passed**, typecheck·API type contract·Ruff·py_compile을 통과했다. `soak-public-voice-websocket.py` v4, `/admin/voice-runtime`, `capture-g7-runtime-evidence.py`, `capture-g7-topology-evidence.py`, independent human-held-out evaluator를 `check-g7-external-proof.py`가 같은 public transport host·겹치는 50분 시간창으로 묶는다. 실제 마이크·운영 local provider·실제 참가자 evidence는 없으므로 G7은 external GATE다. 상세는 `ops/outcome-os-g7-external-proof-readiness-2026-08-07.md`가 소유한다. - 2026-08-09 G7 external-proof P0: production runner exit를 canonical checker 0·`gate_closed=true`에 묶고, browser Origin allowlist와 API/WSS/admin/topology host·scheme을 분리했다. capture 최소 3,120초와 세 runtime artifact 공통 3,000초, detached-clean commit/tree·runner/collector/checker SHA·`psutil==6.1.1`, fresh API/cloudflared PID provenance receipt를 fail-closed로 고정했다. runner/checker/topology 86/86, launcher/sidecar 80/80, G7 통합 166/166, API 921, gateway 58을 통과했다. 이 수치는 실제 물리 마이크·human pack을 대체하지 않으므로 G7은 external GATE다. - 2026-08-09 G7 clean public promotion·rehearsal: detached-clean commit `b34623f3…`·tree `32cc85c7…`에서 API/cloudflared를 fresh 승격하고 receipt `9f8d1941…a21bf`를 발행했다. 공개 health는 `status=ok·db=true·engine=true`, OpenAPI 126과 `/admin/voice-runtime`, `local_whisper/small`·`melotts/melotts-korean`·WS queue 4를 확인했다. watchdog·로그온 task도 같은 stable root/commit/tree/script SHA에 pin해 명시 실행 결과 0·Ready를 확인했다. Python 기본 User-Agent가 Cloudflare에서 403이 되는 경계는 browser-compatible header와 focused 4/4로 고정했다. `--rehearse` 30초는 인증 WSS ready/ping/close1000, runtime 7 samples, Windows topology 7 samples를 모두 통과했고 `physical_capture=false`, UUID/email literal 0이다. current G7 six-suite는 87/87, runtime sampler는 4/4다. 이 리허설은 실제 마이크 3,120초·공통 3,000초 high-water·독립 human pack·canonical checker exit 0을 대체하지 않는다. - 2026-08-09 G7 human pack preflight: production runner는 캡처 전에 category·categorical κ≥0.70, preregistration 선행, 50회기/150축 complete pairing, 양 조건 결측 ITT를 검증한다. 독립 검수는 `python -X utf8 -B scripts/check-g7-human-voice-gain.py --input `이며 `--print-schema`도 지원한다. 부적합 pack은 마이크를 열기 전에 exit 2다. `prepare-g7-human-voice-gain-intake.py`는 non-evidence 빈 템플릿과 strict CSV compiler를 제공하며 production gate 통과 전에는 pack을 쓰지 않는다. API evaluator 11/11 + G7 script 86/86 + intake compiler 6/6, 합계 103/103을 통과했다. 실제 사람 데이터가 없으므로 external GATE는 열려 있다. - 2026-08-12 current G7 재검증: runtime/checker 6파일 94/94, intake 6/6, standalone validator 4/4로 총 104/104를 통과했다. 빈 템플릿은 `template_is_evidence=false`, compile exit 1, `pack_written=false`이고 임시 intake와 pack 잔여는 0이었다. 마이크·공개 세션·운영 DB는 사용하지 않았다. - 2026-08-07 G7/G8 시각 QA: 데스크톱·Pixel 5에서 focused **16/16**, typecheck·build·cosmetic-filter를 통과했다. G8 승인 차단 이유를 `title` 의존에서 상시 문구·`aria-describedby`·semantic form/Enter 제출로 바꾸고, G7 이벤트 최소 24×24px와 시간축 키보드 포커스를 수치 회귀로 고정했다. 상세는 `ops/outcome-os-g7-g8-visual-qa-2026-08-07.md`가 소유한다. - 2026-08-07 G0~G8 release-only assembly: dirty **257파일**을 related 235/mixed 4/excluded 18로 전건 분류했다. `api.gen.ts`는 clean HEAD + related API 소스에서 공식 재생성하고 Windows checkout에서도 선언 해시를 다시 확인했다. 최초 공개 승격 patch는 229 files, SHA-256 `d9ac3c85...34d41`로 두 번 동일하게 조립돼 clean HEAD/canonical index 적용 검사를 통과했다. 최종 SSOT 반영 뒤 다시 결정적 조립·검사한다. - 2026-07-15 전체 검증: `npm run e2e:parallel` **166/166 passed**(2.5분), `npm run e2e:single-run` **49/49 passed**(23.2분). 합계 **215/215 passed**. AI 튜터 DB 영속화는 정상 provider 응답이 2분을 넘을 수 있어 해당 테스트만 같은 실엔진 묶음의 장시간 기준인 240초를 사용한다. - `e2e/voice-success.spec.ts`는 직접 `/voice/ws` 캐스케이드, Session 텍스트 턴의 `POST /voice/speech`, Session 마이크 UI를 함께 검증한다. 브라우저 `