CRUD 라운드트립 E2E
배포 후 "저장이 안 된다 / 완료 기록 조회가 안 된다 / 수정이 안 된다" 회귀를 머지 전에 잡기 위한 검사다. 단위 테스트(가짜 RPC 클라이언트)와 pgTAP(DB 단독)이 커버하지 못하는 이음새 — 실제 프론트 리포지토리 코드가 실제 DB의 v4 RPC·화면 RPC에 대고 완주하는 경로 — 를 실행한다.
무엇을 검증하나
e2e/crudRoundtrip.e2e.mjs가 실제 barbelicRepository(어댑터·계약 검증 포함)로 운영 canary 체크리스트를 그대로 자동화한다:
- 실 계정 생성·로그인 (GoTrue) + 카탈로그 종목 조회 (RLS 경유)
- 저장:
createCompletedSession→ v4 RPC → 영수증(serverRevision 1) - 조회:
get_session_detail읽기 복원 (제목·세트·리비전) - 조회: stats refresh job 처리(실패 0 강제) 후 달력 일/월 요약·홈 대시보드 노출
- 수정:
updateCompletedSession(OCC) → 리비전 2 + 읽기 반영 - 수정 충돌: stale revision → 40001 거부 + 데이터 불변
- 삭제:
deleteCompletedSession→ 상세 조회 거부 + 달력에서 제거
실행 방법
CI: migration-smoke job이 supabase start + 마이그레이션 replay + pgTAP 후 자동 실행.
로컬 (Docker 필요):
supabase start
eval "$(supabase status -o env)"
E2E_SUPABASE_URL="$API_URL" E2E_SUPABASE_ANON_KEY="$ANON_KEY" \
E2E_SUPABASE_SERVICE_ROLE_KEY="$SERVICE_ROLE_KEY" npm run test:e2e-local기본 npm test에는 포함되지 않는다 (DB가 필요하므로). 파일은 e2e/ 루트에 있고 *.e2e.mjs 이름을 쓴다 — tests/ 아래 두면 node 테스트 러너가 자동 발견하므로 금지.
운영 post-deploy smoke (credential 모드)
같은 스크립트가 운영 Supabase에 대해 전용 테스트 계정으로 실행된다 (.github/workflows/prod-smoke.yml — 성공한 Vercel Production 배포마다 + 수동 트리거). 리포 안 검증이 잡지 못하는 "배포된 프론트 계약 ↔ 배포된 DB" 스큐(실사례: 2026-07-30 get_exercise_pr_records 42883 undefined function)를 잡는 층이다.
안전 모델:
- CI에는 anon key(어차피 번들에 공개됨)와 테스트 계정 자격증명만 들어간다. service_role 키는 운영 smoke에 절대 사용하지 않는다.
- 테스트 계정은 일반 사용자이므로 RLS가 모든 쓰기를 자기 행으로 격리한다. 피해 반경 = 그 계정의 자기 데이터.
- 매 실행이 세션 1개를 만들고 마지막에 삭제한다(제목·source_ref에
smoke마커). 삭제 실패 시 경고만 남기고 통과시키며, 잔여 행은 테스트 계정 안에만 남는다. - 통계 materialization은 운영 cron/edge function이 담당하므로 수동 드레인 없음. 달력/홈 단언은 live-session 읽기 경로 기준.
1회 셋업 (관리자):
- Supabase 대시보드 → Authentication → Users → Add user로 전용 계정 생성 (예:
smoke@barbelic.internal, 강한 비밀번호, Auto Confirm 체크). 이메일 로그인 프로바이더가 켜져 있어야 한다 (기본값 on). - GitHub 리포 시크릿 4개 등록 (Settings → Secrets and variables → Actions, 또는
gh secret set <이름>— 값은 프롬프트에 직접 입력):PROD_SUPABASE_URL,PROD_SUPABASE_ANON_KEY,PROD_SMOKE_EMAIL,PROD_SMOKE_PASSWORD - 평상시에는 Production 배포 뒤 자동 CRUD를 확인한다. integration release에서는 Actions 탭 → Production smoke → Run workflow에서 branch를
main으로 선택하고full_browser=true로 1회 수동 실행해 CRUD와 numbered browser 모두 그린인지 확인한다.main이 아닌 ref의 수동 실행은 checkout이나 운영 시크릿 접근 전에 명시적으로 실패한다.
시크릿이 없으면 자동 lightweight CRUD는 notice만 남기고 no-op으로 통과한다. 반면 수동으로 full_browser=true를 요청한 numbered browser gate는 시크릿 누락을 오류로 처리한다.
빈 계정 여정 (e2e/emptyAccountJourney.e2e.mjs)
CRUD 라운드트립은 저장한 뒤에야 화면 RPC를 부른다. service 모드가 매번 새 유저를 만들긴 하지만 0건 상태의 읽기 경로는 한 번도 실행되지 않으므로, "신규 사용자만 겪는" 회귀는 구조적으로 잡히지 않았다. 이 파일이 그 구멍을 메운다.
0건 계정에서만 깨지는 지점은 서버가 빈 데이터를 밀도 있게 합성해야 하기 때문에 생긴다: 주간 활동 7행, current_periods 5키 스텁, user_stats_refresh_state 행이 없는 유저의 freshness(깨지면 홈·PR·볼륨·PR상세가 동시 사망), 미훈련 종목의 available_years, 빈 커서 페이지의 next_cursor: null. 전부 기록이 있으면 통과한다.
또한 CRUD 라운드트립이 한 번도 부르지 않는 화면 RPC들을 0건 상태로 훑는다 — 여기에는 2026-07-30 운영 장애를 낸 get_exercise_pr_records도 포함된다. 어댑터가 응답을 엄격 검증하므로 호출이 곧 계약 검사다.
npm run ci:local -- --only empty고정 날짜의 스냅샷을 예약 rollover가 덮지 않도록 검사 수명 동안 전용 sandbox의 통계 cron을 격리하고, 정리 후 기존 상태로 복원한다. 직접 실행할 때는 BARBELIC_DB_SANDBOX와 그 sandbox의 API·anon·service 환경값을 함께 전달한다. GitHub-hosted CI는 해당 job의 checkout을 명시하며, 일반 로컬 개발·원격 DB에는 이 예외를 적용하지 않는다. 고정 날짜·스냅샷 준비 기록을 참고한다.
service 모드 전용이다. 진짜 "한 번도 쓰지 않은 계정"은 service-role 키로 일회용 유저를 만들 때만 보장된다. 운영 smoke는 공용 고정 계정을 쓰므로 0건을 가정할 수 없고, 0건을 만들려면 그 계정 데이터를 전부 지워야 해서 안전 모델(운영에 service-role 키 금지)에 어긋난다. credential 모드에서는 안내만 남기고 no-op으로 통과한다.
유산소 최소 1원자 보증 (e2e/cardioMinOneField.e2e.mjs)
2026-08-30 실 사용자 저장 차단 사고(이슈 #936 — 시간만 기록한 유산소 세트를 앱의 저장 직전 최종 검사가 서버 도달 전에 거부)의 상주 회귀 게이트다. 시간·거리로 기록하는 종목은 한 원자만 입력해도 다섯 저장 경로 전부에서 기록된다는 것을 실제 repository 코드로 보증한다: ① 계획 저장·재로드 ② 완료 기록 저장(신규+수정) ③ 운동 진행(진행중 초안의 서버 체크포인트 왕복) ④ 화이트보드 작성·재로드 ⑤ 화이트보드 운동(보드 계보 완료가 완료 인원 뷰에 시간·거리와 함께 선다). 그룹·보드 픽스처가 일회용 유저 삭제(CASCADE)로만 안전하게 정리되므로 service 모드 전용이며(credential 모드 no-op 통과), CI에서는 CRUD 라운드트립·빈 계정 여정에 이어 npm run test:e2e-cardio로 실행된다.
세 E2E는 e2e/support/의 하네스를 공유한다 (e2eConfig.mjs = 순수 로직, harness.mjs = 글로벌 셰임·repository 로드·클라이언트·클린업). 순수 로직은 DB 없이 tests/react/e2eHarnessConfig.test.mjs가 npm test에서 상시 검증한다 — 특히 "credential 모드에는 service-role 키가 새어 들어가지 않는다"는 안전 판정.
한계와 후속
- 브라우저 레벨(UI 클릭) 검증은 포함하지 않는다. 컨트롤러 → repository 아래 계층만 커버한다. UI 배선 회귀는 이 층이 구조적으로 못 잡는다 — 2026-08-01 데스크톱 홈 CTA가 폐기된 작성기를 열었던 사고가 그 증거다. 진입점이 "어느 화면을 여는가"는
tests/react/desktopWorkoutEntryContract.test.mjs(등록된 data-lg 마커 기준)와tests/react/recordComposerOwnership.test.mjs(작성기 상태 단일 소유)가 담당한다. - 운영 lightweight CRUD는 성공한 Vercel Production
deployment_status에 맞춰 실행한다. numbered browser suite는 integration release에서main을 선택해 수동 실행하며, 같은 run의 CRUD가 Green일 때만 시작한다. DB-first 배포 원칙상 두 경로 모두 운영 DB 마이그레이션 정렬 뒤에 실행되어야 한다.