Skip to content

Latest commit

 

History

History
572 lines (404 loc) · 34.7 KB

File metadata and controls

572 lines (404 loc) · 34.7 KB

테스트 가이드

ReadMates 테스트는 frontend lint/unit/build, Playwright E2E, backend Gradle lane, 공개 릴리즈 후보 점검, 배포 smoke로 나뉩니다.

.github/workflows/ci.yml의 주요 job:

Job 내용
scripts agent guidance, deploy workflow contract, Flyway 불변성, ShellCheck, AI PII/설정, Prometheus/Tempo/Grafana 검증
public-release 공개 후보 build + scanner
frontend Node.js 24 + pnpm@11.13.1로 lint, test:coverage, build, Zod fixture freshness
frontend-visual-regression pnpm test:ct:docker
design-system pnpm design:check
backend JDK 25로 ./scripts/server-ci-check.sh (unit, architecture, ktlint, detekt, JaCoCo)
backend-integration ./gradlew integrationTest (Testcontainers)
E2E MySQL service + Playwright 3개 shard
  • 검증은 변경 surface와 위험도에 맞춰 고릅니다.
  • 완료 보고에는 실행한 명령, 실패·스킵한 명령과 이유, 남은 리스크를 남깁니다. 실패한 검증을 완료로 적지 않습니다.
  • 배포 smoke 결과는 운영 상태일 수 있으니 결과 전문, 운영 domain, provider 응답을 문서나 Git에 붙이지 않습니다.
  • 로컬 pnpm 버전이 다르거나 CI parity가 필요하면 corepack pnpm --dir front ...로 실행합니다. corepack이 PATH에 없으면 npx --yes corepack@0.35.0 pnpm --dir front ...를 씁니다.

Pre-Push Aggregate

CI에서 자주 실패하는 게이트를 로컬에서 먼저 묶어 확인할 때는 아래 스크립트를 사용합니다.

./scripts/pre-push-check.sh

기본 모드는 agent guidance·deploy workflow contract 검사, git diff --check, frontend lint·coverage unit test·build, Zod fixture freshness, ./scripts/server-ci-check.sh, production AI config 검증을 실행합니다. .github/, deploy/, docs/, scripts/, AGENTS.md, README.md, .env.example, .gitleaks.toml이 바뀌면 공개 후보 build와 scanner도 실행합니다.

릴리즈나 태그 배포 직전에는 integration, E2E, observability 설정 검증까지 포함합니다.

./scripts/pre-push-check.sh --full --release

명령 목록만 확인하려면 --dry-run을 사용합니다. Public release scanner를 수동으로 생략할 때는 --no-release를 쓰되, release-sensitive 경로 변경에서는 완료 보고에 생략 이유를 남깁니다.

Frontend

의존성 설치:

corepack pnpm install --frozen-lockfile

루트 pnpm workspace가 front, design/system, design/docs를 함께 관리합니다. Frontend 명령은 pnpm --dir front ... 형태입니다.

Lint:

pnpm --dir front lint

Unit test:

pnpm --dir front test

Vitest는 front/vitest.config.ts의 project split으로 순수 Node 단위 테스트와 jsdom/React/BFF 성격 테스트를 나눠 실행합니다. 새 테스트를 추가할 때 DOM, Web API, Testing Library가 필요하면 jsdom project include 대상에 두고, 순수 model/contract 계산이면 Node project에 남깁니다.

신규 단위 테스트는 source 파일 옆에 *.test.{ts,tsx} 형식으로 co-locate합니다(예: features/host/ui/host-foo.tsx → features/host/ui/host-foo.test.tsx). vitest.config.ts의 include가 src/**/*.test.{ts,tsx}, features/**/*.test.{ts,tsx}, shared/**/*.test.{ts,tsx}와 기존 tests/unit/**를 모두 매치합니다. 기존 front/tests/unit/는 server testcontainer가 readmates.frontend.fixtures.dir system property로 참조하므로 이동하지 않습니다. 새 fixture가 서버에서도 사용되는 경우에만 tests/unit/__fixtures__/에 둡니다.

Coverage 게이트:

pnpm --dir front test:coverage

@vitest/coverage-v8 threshold는 lines 80, statements 79, functions 80, branches 75입니다(front/vitest.config.ts). CI frontend job이 이 게이트를 강제하고 front-coverage artifact를 올립니다. 올릴 때는 안정 측정치에서 2pp를 뺀 정수로 정합니다.

Frontend unit suite에는 front/tests/unit/frontend-boundaries.test.ts도 포함됩니다. 이 테스트는 route-first 구조의 shared/feature/model/route/ui import 경계, shared/ui의 src/app import 금지, 제거된 shared/api/readmates compatibility import, ui가 있는 feature의 components public import 금지, route-owned action type 노출 여부를 확인합니다. Legacy boundary exception 목록은 비어 있어야 합니다.

API contract schema나 fixture를 바꿨다면 export 결과가 최신인지 확인합니다. CI frontend job도 같은 검사를 실행합니다.

pnpm --dir front zod:export-fixtures
git diff --exit-code front/tests/unit/__fixtures__/zod-schemas/

Frontend 경계만 빠르게 확인하려면 Vitest를 직접 실행합니다.

pnpm --dir front exec vitest run tests/unit/frontend-boundaries.test.ts

BFF/OAuth proxy header forwarding을 바꿨다면 Cloudflare Functions unit test를 먼저 실행합니다. 이 테스트는 browser-supplied internal header를 trusted BFF/server-derived 값으로 덮어쓰는지, _shared/proxy.ts helper가 upstream response의 internal x-readmates-* header를 제거하는지, OAuth proxy의 forwarded header policy가 authorization start와 callback에서 드리프트하지 않는지 확인합니다.

pnpm --dir front exec vitest run tests/unit/cloudflare-bff.test.ts tests/unit/cloudflare-oauth-proxy.test.ts

Production build:

pnpm --dir front build

Lighthouse Diagnostic

Lighthouse 진단은 CI gate가 아닌 품질 기준선입니다. Public, member, host, platform-admin dev-seed route를 검사하고 .tmp/lighthouse/에 결과를 남깁니다. Route 진입 실패와 Lighthouse finding을 구분합니다.

작은 smoke부터 실행합니다.

pnpm --dir front lighthouse:diagnose -- --group public --limit 2
pnpm --dir front lighthouse:diagnose -- --group member --limit 1

Playwright E2E와 같은 로컬 MySQL, Spring dev profile, Vite 경로가 정상이면 전체 desktop baseline을 실행합니다.

pnpm --dir front lighthouse:diagnose

결과의 summary.md, findings.json으로 후속 작업 범위를 정합니다.

Playwright E2E

pnpm --dir front test:e2e

front/playwright.config.ts는 E2E 실행 중 backend와 frontend dev server를 함께 띄웁니다.

  • 기본 frontend port는 PLAYWRIGHT_PORT가 없으면 3100입니다.
  • 기본 backend origin은 READMATES_API_BASE_URL이 없으면 http://127.0.0.1:18080입니다.
  • READMATES_E2E_DB_NAME이 없으면 E2E database 이름은 현재 운영 migration과 dev seed SQL 내용의 fingerprint를 붙인 readmates_e2e_<hash> 형태로 정합니다. migration 파일이 바뀌면 기본 schema 이름도 바뀌므로 오래된 로컬 readmates_e2e schema의 Flyway checksum history가 기본 명령을 막지 않습니다.
  • E2E backend의 Actuator management port는 READMATES_MANAGEMENT_PORT=0으로 실행해 로컬 8081 점유 상태와 충돌하지 않게 합니다. Playwright readiness는 여전히 backend API origin의 /internal/health를 기준으로 확인하므로 backend startup failure는 숨기지 않습니다.
  • E2E backend는 SPRING_PROFILES_ACTIVE=dev, READMATES_FLYWAY_LOCATIONS=classpath:db/mysql/migration,classpath:db/mysql/dev, BFF secret placeholder, IP hash base secret placeholder로 실행됩니다. 이는 non-production blank IP hash secret도 명시 opt-in 없이는 실패하는 server validation과 맞춥니다.
  • 테스트에서 운영 migration 경로를 지정할 때도 classpath:db/mysql/migration을 사용합니다. dev profile 또는 E2E seed가 필요한 경우에만 classpath:db/mysql/dev를 뒤에 추가합니다.
  • E2E database 연결은 READMATES_E2E_DB_HOST, READMATES_E2E_DB_PORT, READMATES_E2E_DB_USER, READMATES_E2E_DB_PASSWORD, READMATES_E2E_DB_NAME으로 조정할 수 있습니다. 공개 문서에서는 정확한 로컬 DB 이름 대신 placeholder를 사용합니다.
  • Playwright config는 mysql CLI로 E2E database를 생성하므로 로컬 MySQL server와 MySQL client가 필요합니다. E2E user에 CREATE DATABASE 권한이 없다면 admin 계정으로 별도 E2E schema를 미리 만들고 해당 schema에만 E2E user 권한을 부여한 뒤 READMATES_E2E_DB_NAME으로 지정합니다. 직접 지정한 기존 schema에서 Flyway checksum mismatch가 나면 repair나 drop 대신 READMATES_E2E_DB_NAME을 비우거나 새 schema 이름을 지정하는 편이 안전합니다.

기본 Playwright worker 수는 seeded database state를 공유하는 현재 E2E 흐름 때문에 1로 유지합니다. state isolation을 확인한 뒤 병렬 실행을 실험할 때만 명시적으로 opt-in합니다.

PLAYWRIGHT_WORKERS=2 pnpm --dir front test:e2e

반복 실행에서 database cleanup 충돌이 없다는 증거가 쌓이기 전에는 worker 병렬화를 기본값으로 바꾸지 않습니다.

기본 compose.yml의 MySQL을 쓴다면 먼저 실행합니다.

docker compose up -d mysql

다른 MySQL을 쓴다면 E2E 환경 변수를 맞춥니다.

READMATES_E2E_DB_HOST=127.0.0.1 \
READMATES_E2E_DB_PORT=3306 \
READMATES_E2E_DB_USER='<e2e-db-user>' \
READMATES_E2E_DB_PASSWORD='<e2e-db-password>' \
READMATES_E2E_DB_NAME='<e2e-db-name>' \
pnpm --dir front test:e2e

front/tests/e2e/dev-login-session-flow.spec.ts는 호스트가 DRAFT 세션을 만들고 게스트 접근을 GUEST_READABLE로 바꾼 뒤(/access-scope), 멤버 홈 표시와 OPEN 전환을 검증합니다. CLOSED/PUBLISHED lifecycle은 backend DB test와 frontend unit test가 더 촘촘히 봅니다.

Member/host reading-loop route smoke:

pnpm --dir front test:e2e -- tests/e2e/dev-login-session-flow.spec.ts

세션 기록 JSON 가져오기 흐름은 frontend model unit test와 backend DB integration test가 1차 검증합니다.

pnpm --dir front exec vitest run features/host/model/session-import-model.test.ts
./server/gradlew -p server integrationTest --tests com.readmates.sessionimport.api.HostSessionImportControllerDbTest

멤버 표시 이름과 권한 경계만 빠르게 확인하려면 관련 E2E spec을 직접 지정할 수 있습니다.

pnpm --dir front test:e2e -- member-profile-permissions

플랫폼 admin today 화면만 확인하려면 아래 spec을 씁니다. Public-safe route mock으로 OWNER의 queue 흐름과 SUPPORT의 mutation 차단을 검증합니다.

pnpm --dir front test:e2e -- tests/e2e/admin-today.spec.ts

화면 증거(desktop/mobile screenshot)가 필요할 때:

pnpm --dir front test:e2e -- tests/e2e/admin-analytics.spec.ts
pnpm --dir front test:e2e -- tests/e2e/host-club-operations.spec.ts
pnpm --dir front test:e2e -- tests/e2e/member-reading-momentum.spec.ts

Public-safe mock이나 dev fixture로 Playwright test-results에 screenshot을 남깁니다. 이 파일은 증거용이며 커밋하지 않습니다.

시각 회귀 (컴포넌트 하니스)

목적: shared/ui primitive의 렌더링 회귀를 화면 흐름 E2E와 분리해, 컴포넌트 단위 스냅샷으로 빠르게 잡습니다. 위의 host/member/admin visual evidence가 화면 흐름 증거(커밋하지 않는 산출물)인 것과 달리, 컴포넌트 하니스의 baseline 스냅샷은 Git에 커밋해 회귀 게이트로 사용합니다.

Route-critical UI coverage: shared primitive baseline은 작은 UI 부품을 보호하고, feature-level CT baseline은 route에서 반복적으로 깨지기 쉬운 운영/공개 UI 조각을 보호합니다. 현재 feature-level 후보는 host closing board, platform admin support/audit 판단 패널, public records/session 카드처럼 props만으로 렌더링할 수 있고 API/BFF/auth 흐름을 직접 검증하지 않아도 되는 presentation surface입니다. Route loader, auth guard, BFF proxy, BrowserRouter ordering은 CT가 아니라 route test 또는 E2E로 확인합니다.

설정 파일: front/playwright-ct.config.ts. E2E용 front/playwright.config.ts와 별개이며, backend나 frontend dev server를 띄우지 않습니다(@playwright/experimental-ct-react가 컴포넌트를 직접 렌더).

테스트 위치: front/shared/ui/**/*.ct.tsx로 source 옆에 co-locate합니다. .ct.tsx 확장자라 Vitest *.test.{ts,tsx}나 E2E tests/e2e/**와 충돌하지 않습니다.

스냅샷 경로: baseline은 front/__screenshots__/shared/ui/ 또는 front/__screenshots__/features/ 아래에 생성되며 커밋 대상입니다(testDir="."). Shared primitive는 ReadmatesBrandMark, BookCover, AvatarChip, MobileHeader, TopNav 등을 덮고, feature-level baseline은 host/admin/public route-critical UI 조각을 덮습니다.

명령:

pnpm --dir front test:ct
pnpm --dir front test:ct:docker
pnpm --dir front test:ct:update
pnpm --dir front test:ct:update:docker
  • test:ct는 host renderer 차이를 만들지 않도록 test:ct:docker에 위임합니다. snapshot update flag를 사용하지 않습니다.
  • test:ct:docker는 macOS/renderer drift 회피용 검증 명령입니다. front/scripts/run-ct-docker.ts가 루트 package.json의 packageManager를 읽고 Docker 컨테이너 안에서 Corepack으로 같은 pnpm을 활성화한 뒤 Playwright CT를 실행합니다. 이 명령은 snapshot을 갱신하지 않습니다.
  • test:ct:update는 실수로 host renderer baseline을 만들지 않도록 test:ct:update:docker에 위임합니다.
  • test:ct:update:docker가 baseline 생성의 유일한 정규 경로입니다. 같은 helper에 --update를 넘겨 mcr.microsoft.com/playwright:v1.61.1-jammy 이미지 안에서 실행합니다.
  • 앱은 Pretendard 가변 웹폰트를 자체 번들링합니다. CT는 production body class의 computed family와 실제 Pretendard Variable face 로드를 함께 검증해 macOS/Linux system font fallback 차이를 차단합니다. 배포 폰트에 동반되는 SIL OFL 1.1 사본은 front/public/licenses/Pretendard-OFL-1.1.txt에 있으며 production build에도 그대로 복사됩니다.

Docker CT는 repository를 /work에 mount하되 root node_modules, front/node_modules, pnpm store를 Docker named volume으로 분리합니다. 그래서 Linux optional dependency install이 host node_modules를 덮어쓰는 일을 피합니다. Docker volume이 오래되어 의존성 상태가 의심되면 다음처럼 CT 전용 volume만 지웁니다.

docker volume rm readmates-ct-root-node-modules readmates-ct-front-node-modules readmates-ct-pnpm-store

darwin(macOS) 제약: macOS host Chromium은 같은 웹폰트라도 Linux와 픽셀 안티앨리어싱이 다르고, Vite 8의 @rolldown/binding-darwin-arm64 네이티브 바인딩 상태에 따라 CT suite가 부팅하지 못할 수도 있습니다. 따라서 정규 검증과 baseline 생성은 모두 Docker 경로를 사용합니다. test:ct와 test:ct:update도 각각 Docker 명령에 위임하므로 macOS/Linux에서 같은 판정 기준을 사용합니다.

flake 정책: 애니메이션과 caret을 끄고, 고정 viewport 480x360, maxDiffPixelRatio: 0.02로 픽셀 노이즈를 흡수합니다. baseline update는 package script가 Docker renderer로만 연결합니다.

CI gate: .github/workflows/ci.yml의 frontend-visual-regression job은 pull request와 main push에서 pnpm test:ct:docker를 실행합니다. 이 job은 baseline을 갱신하지 않고 drift만 검증하며, 실패 시 front/test-results와 front/playwright-report를 artifact로 업로드합니다. 의도한 UI 변경이면 먼저 product diff를 리뷰한 뒤 Docker update command로 PNG baseline을 갱신하고, 의도하지 않은 diff면 UI/fixture를 고칩니다.

Public release safety: front/__screenshots__는 repo에 커밋되는 regression baseline이지만 clean public release candidate에는 포함하지 않습니다. CT baseline 경로, release candidate copy rule, public scanner rule을 바꾸면 아래 명령으로 screenshot exclusion이 유지되는지 확인합니다.

./scripts/build-public-release-candidate.sh
./scripts/public-release-check.sh .tmp/public-release-candidate

experimental API 주의: @playwright/experimental-ct-react는 experimental이고 Vite 8 / React 19 조합은 bleeding-edge입니다. 부팅이 실패하면 임시 우회를 강제하지 말고 이슈로 기록한 뒤 진행합니다.

Backend

Backend test는 JDK 25에서 돕니다. server/build.gradle.kts가 Gradle Test JVM을 Java 25 toolchain으로 고정하므로, 로컬 JDK를 찾지 못하면 JDK 25를 설치하거나 JAVA_HOME을 맞춥니다.

Backend PR-level gate와 full Testcontainers lane:

./scripts/server-ci-check.sh
./server/gradlew -p server integrationTest

기본 Gradle test task는 비활성화되어 있습니다(enabled = false). 태그 필터 없이 모든 테스트를 중복 실행하기 때문입니다. 그래서 test나 test --tests ...는 아무것도 실행하지 않습니다. 개별 테스트는 태그에 맞는 lane(unitTest, integrationTest, architectureTest)에 --tests를 붙여 실행합니다. check는 unitTest, architectureTest, ktlint, detekt, JaCoCo를 한 번씩 실행하고, integration은 Docker가 필요해 따로 호출합니다.

Backend fast lanes:

./server/gradlew -p server unitTest
./server/gradlew -p server integrationTest
./server/gradlew -p server architectureTest
Lane 대상
unitTest integration, container, architecture tag가 없는 테스트
integrationTest integration 또는 container tag (@SpringBootTest, Testcontainers)
architectureTest architecture tag (ArchUnit 경계, baseline·inventory contract)

Fast lane은 개발 중 피드백용이며 PR-level wrapper를 대체하지 않습니다.

SQL plan, API contract, query budget을 건드리는 release-risk 검토에서는 아래 lane을 명시적으로 실행합니다.

./server/gradlew -p server integrationTest \
  --tests com.readmates.contract.FrontendZodSchemaContractTest \
  --tests com.readmates.performance.ServerQueryBudgetTest \
  --tests com.readmates.performance.MySqlQueryPlanTest

:unitTest는 JUnit5 클래스 단위 병렬 + Gradle maxParallelForks=availableProcessors()/2(기본)로 실행합니다. CI에서는 READMATES_TEST_FORKS env로 fork 수를 명시할 수 있고, sweep harness(scripts/bench/sweep-forks.sh)로 머신별 최적값을 측정할 수 있습니다. Backend Test JVM heap은 기본 1536m이며, 로컬/CI 메모리 상황에 따라 -PtestMaxHeap=2g 또는 READMATES_TEST_MAX_HEAP=2g로 조정할 수 있습니다.

PR-level quality gate는 단일 check task로 통합되어 있습니다.

./scripts/server-ci-check.sh

check는 다음 게이트를 한 번에 검증합니다.

  • Compiler warning: production compileKotlin은 allWarningsAsErrors입니다. Test source warning은 gate가 아닙니다.
  • ktlint: plugin 12.1.1 + ktlint 1.7.1. server/config/ktlint/baseline.xml current 171건. 자동 정리는 ./server/gradlew -p server ktlintFormat.
  • detekt: 2.0.0-alpha.5 + server/config/detekt/detekt.yml. baseline.xml current 437건 + retired 24건 = approved 461건. v2.5.0에서 rule threshold를 완화했지만(LongMethod 100, LargeClass 1000 등) baseline은 바꾸지 않았습니다.
  • JaCoCo: unitTest가 만든 build/jacoco/unitTest.exec로 LINE COVEREDRATIO 최소 0.43을 강제합니다.
  • Architecture no-growth: server/config/architecture/boundary-import-baseline.txt(current 0)와 feature-dependency-baseline.txt(current 37)는 현재 source inventory와 정확히 같아야 합니다. current + retired는 approved seed(39, 41)를 겹침 없이 나눕니다.

Debt를 없앨 때는 source를 고치고, 같은 변경에서 current baseline의 identity를 지우고, retired ledger에 그대로 옮깁니다. Retired identity 삭제와 approved seed 증가는 허용하지 않습니다.

CI backend job은 ./scripts/server-ci-check.sh 한 번만 호출하고 report artifact는 항상 올립니다.

integrationTest는 Testcontainers로 MySQL/Redis/Kafka를 직접 띄우므로 Docker가 필요하고, docker compose up을 먼저 할 필요는 없습니다. Colima socket이 있고 Docker env가 비어 있으면 Gradle이 DOCKER_HOST와 TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE를 설정합니다.

Flyway migration 불변성

Production migration은 repository history gate와 Flyway runtime checksum을 함께 검증합니다. 전자는 명시한 base의 merge base에 있던 SQL과 현재 index/worktree를 비교해 수정·삭제·rename·이동, 잘못된 catalog와 불완전한 history를 merge 전에 fail closed로 차단합니다. 후자는 이미 적용된 database의 flyway_schema_history checksum mismatch를 startup에 거부합니다.

로컬에서는 complete Git history와 trusted base ref를 사용합니다. --self-test는 Git repository나 network 없이 실행할 수 있고 clean public release candidate에서도 같은 entry point를 검증합니다.

python3 -B scripts/check-flyway-migration-immutability.py --self-test
python3 -B scripts/check-flyway-migration-immutability.py \
  --check-workflow .github/workflows/ci.yml
python3 -B scripts/check-flyway-migration-immutability.py \
  --base-ref <trusted-base-ref>

CI의 scripts job만 fetch-depth: 0을 사용합니다. Pull request는 github.event.pull_request.base.sha, main push는 github.event.before를 immutable base로 선택합니다. Push before가 all-zero이면 local HEAD^가 실제로 resolve될 때만 fallback하고, 빈 값·unresolved base·missing merge base·shallow history는 검사 생략이 아니라 실패입니다. 실패 증거는 violation category, repository-relative path, exact merge-base와 다음 허용 version으로 제한하며 SQL 본문과 로컬 절대 경로는 출력하지 않습니다.

Runtime evidence는 integrationTest lane에서 봅니다. MySqlFlywayMigrationTest는 41개 production migration(V1, V9~V48)의 clean install과 populated V42/V44 schema의 upgrade를 검증합니다.

./server/gradlew -p server integrationTest \
  --tests com.readmates.support.FlywayChecksumImmutabilityTest \
  --tests com.readmates.support.MySqlFlywayMigrationTest \
  --rerun-tasks --no-build-cache --no-configuration-cache

Historical migration의 edit/delete/rename, flyway repair, baseline 증가, 낮거나 재사용한 version은 remediation이 아닙니다. Checker가 보고한 base 최고 version보다 큰 새 V{N}__{lower_snake_case_description}.sql forward-only migration으로 보정합니다. 이 불변성 gate 자체는 production schema를 변경하지 않습니다.

Admin health 장애 격리

admin.health의 transport, executor, single-flight, stale snapshot, scheduler, metric, additive response contract를 변경하면 아래 focused lane을 먼저 실행합니다. transport test는 응답 body를 보류하는 local HTTP server를 사용하며 live Prometheus/provider를 호출하지 않습니다. scheduler, executor, frontend browser fixture도 local 또는 fake infrastructure만 사용합니다.

./server/gradlew -p server unitTest \
  --tests 'com.readmates.admin.health.*' \
  --rerun-tasks --no-build-cache --no-configuration-cache
./server/gradlew -p server architectureTest \
  --tests com.readmates.architecture.ServerArchitectureInventoryTest \
  --tests com.readmates.architecture.ServerArchitectureBoundaryTest \
  --rerun-tasks --no-build-cache --no-configuration-cache
corepack pnpm --dir front exec vitest run \
  features/platform-admin/ui/admin-health-grid.test.tsx \
  features/platform-admin/route/admin-health-route.test.tsx
corepack pnpm --dir front exec playwright test tests/e2e/admin-health.spec.ts

회귀 matrix는 executor 생성 전 invalid typed property의 startup failure, 실제 read timeout, caller-runs 없는 bounded-queue rejection, lazy/scheduled single-flight, timeout/error/rejection, stale last-known-good, initial unavailable와 recovery, bounded metric label, scheduler-to-input-port 방향, additive metadata/UI state를 포함해야 합니다. ship 전에는 ./scripts/server-ci-check.sh, ./server/gradlew -p server integrationTest, full frontend lint/test/build/E2E lane, 변경 scope에 맞는 public-release candidate check를 실행합니다.

로컬 image 재현성 검증은 ./server/gradlew -p server bootJar 후 docker build -t readmates-server:local server를 사용하며, 이 명령은 server/Dockerfile을 사용합니다. Release workflow는 CI가 jar를 빌드한 뒤 server/Dockerfile.release로 이미지를 만들고, 같은 digest를 scan한 다음 promote합니다. Java 25 test JVM은 Netty가 포함된 classpath(ALL-UNNAMED)의 native access를 명시적으로 허용하고 그 밖의 module에서 발생하는 illegal native access를 거절하며, ProtobufJava25CompatibilityTest가 제거 예정인 sun.misc.Unsafe 메모리 접근 없이 OTLP payload를 직렬화하는 fallback을 고정합니다.

Testcontainers 재사용 (로컬 전용)

MySQL / Redis / Kafka Testcontainer는 모두 withReuse(true)로 표시되어 있어, 개발자가 한 번 옵트인하면 후속 backend test 실행에서 컨테이너 시작 단계를 건너뜁니다. 다음 줄을 추가해 활성화합니다.

echo "testcontainers.reuse.enable=true" >> ~/.testcontainers.properties

이 설정은 holder 머신 단위라 CI에는 영향이 없습니다(매 runner가 새 환경). 컨테이너 상태가 stale로 의심되면 다음으로 수동 정리합니다.

docker rm -f $(docker ps -a --filter "label=org.testcontainers.session-id" -q)

Notification Operations

알림 event outbox, Kafka relay/consumer, OCI Email Delivery adapter 설정, retry delay 설정, 운영 metrics, host dashboard/notification operations UI, 멤버 알림 설정과 알림함을 바꿨다면 아래 targeted command를 먼저 실행합니다. Kafka notification integration test는 Testcontainers Kafka를 사용하므로 Docker 또는 Colima가 실행 중이어야 합니다.

./server/gradlew -p server unitTest --tests 'com.readmates.notification.*'
./server/gradlew -p server integrationTest --tests 'com.readmates.notification.*'
./server/gradlew -p server integrationTest --tests com.readmates.archive.api.MemberArchiveReviewControllerTest
./scripts/server-ci-check.sh
./server/gradlew -p server integrationTest
pnpm --dir front exec vitest run tests/unit/host-dashboard.test.tsx
pnpm --dir front exec vitest run tests/unit/host-notifications.test.tsx
pnpm --dir front exec vitest run tests/unit/host-session-notifications.test.tsx
pnpm --dir front exec vitest run tests/unit/member-notifications.test.tsx
pnpm --dir front exec vitest run tests/unit/my-page.test.tsx
pnpm --dir front lint

수동 알림 발송의 세션 선택, preview/confirm, duplicate resend, 멤버별 포함/제외, in-app 수신 확인까지 바꿨다면 아래 E2E spec도 함께 확인합니다. 이 spec은 Playwright가 띄우는 dev backend와 E2E MySQL schema가 필요합니다.

pnpm --dir front test:e2e -- manual-notifications

Redis-Backed Server Features

Redis-backed 기능은 Redis가 꺼진 기본 상태와 Redis가 켜진 adapter test 양쪽에서 확인합니다.

./scripts/server-ci-check.sh
./server/gradlew -p server integrationTest
pnpm --dir front test:e2e

Targeted Redis adapter test는 Testcontainers Redis를 직접 띄우므로 수동 Redis server가 필요하지 않습니다. Testcontainers가 로컬 localhost를 반환하면 test helper는 Redis URL host를 127.0.0.1로 정규화해 IPv6 localhost에서 다른 로컬 서비스와 port가 겹치는 flake를 피합니다. Rate limit, auth session cache, public cache, notes cache, read-cache invalidation을 바꾸면 관련 Redis*AdapterTest, application cache test, ServerArchitectureBoundaryTest를 함께 확인합니다.

ServerArchitectureBoundaryTest는 inbound adapter가 legacy repository, JdbcTemplate, outbound adapter에 직접 의존하지 않는지, application package가 adapter·Spring JDBC/DAO·Spring Web/HTTP에 의존하지 않는지 확인합니다. ServerArchitectureInventoryTest는 boundary/feature baseline을 현재 source와 대조합니다. Application service에서는 ResponseStatusException, HttpStatus 대신 feature error를 던지고 adapter.in.web에서 매핑합니다. 세션/노트 쓰기 흐름을 바꿨다면 아래를 먼저 실행합니다.

./server/gradlew -p server architectureTest \
  --tests com.readmates.architecture.ServerArchitectureBoundaryTest
./server/gradlew -p server unitTest \
  --tests com.readmates.auth.adapter.in.security.CurrentMemberArgumentResolverTest \
  --tests com.readmates.session.application.service.SessionMemberWriteServiceTest
./server/gradlew -p server integrationTest \
  --tests com.readmates.session.api.CurrentSessionControllerDbTest \
  --tests com.readmates.session.api.HostSessionControllerDbTest \
  --tests com.readmates.session.api.HostDashboardControllerTest \
  --tests com.readmates.note.api.MemberActionControllerDbTest

목록 조회, archive/notes/host/public detail query, 또는 cursor pagination SQL을 수정했다면 query budget과 EXPLAIN guardrail을 먼저 확인합니다. ServerQueryBudgetTest는 주요 HTTP flow의 query 수가 관찰된 budget을 넘지 않는지 확인하고, MySqlQueryPlanTest는 핵심 목록/detail SQL이 의도한 index plan을 유지하는지 확인합니다.

Notes feed 대용량 검증은 LargeReadPathFixture의 합성 데이터를 씁니다. 성능 점수가 아니라 N+1, index 유실, 첫 page 급격한 회귀를 잡는 용도입니다.

./server/gradlew -p server integrationTest \
  --tests com.readmates.performance.ServerQueryBudgetTest \
  --tests com.readmates.performance.MySqlQueryPlanTest

ServerQueryBudgetTest는 admin analytics overview(/api/admin/analytics/overview?window=30d)의 query 수도 고정합니다.

Host closing board(/api/host/sessions/{sessionId}/closing-status, sessionclosing, closing board UI, admin closing-risk link)를 바꿨다면 query budget, EXPLAIN, CT baseline을 함께 확인합니다.

./server/gradlew -p server integrationTest \
  --tests com.readmates.performance.ServerQueryBudgetTest \
  --tests com.readmates.performance.MySqlQueryPlanTest
pnpm --dir front test:ct

front/test-results/**의 E2E screenshot은 커밋하지 않습니다. front/__screenshots__/**의 CT baseline은 Docker renderer로 만든 뒤 커밋하는 regression gate입니다.

멤버 프로필이나 표시 이름 검증을 수정했다면 아래 focused command로 controller, application, migration 경계를 먼저 확인할 수 있습니다.

./server/gradlew -p server integrationTest \
  --tests com.readmates.auth.api.MemberProfileControllerTest \
  --tests com.readmates.auth.api.HostMemberApprovalControllerTest \
  --tests com.readmates.support.MySqlFlywayMigrationTest

세션 공개 범위, 예정 세션, OPEN -> CLOSED -> PUBLISHED lifecycle, 공개 기록 노출을 수정했다면 아래 focused command가 가장 빠른 1차 확인입니다.

./server/gradlew -p server unitTest \
  --tests com.readmates.session.application.service.HostSessionServicesTest
./server/gradlew -p server integrationTest \
  --tests com.readmates.session.api.HostSessionControllerDbTest \
  --tests com.readmates.session.api.HostSessionBffSecurityTest \
  --tests com.readmates.session.api.HostDashboardControllerTest \
  --tests com.readmates.publication.api.PublicControllerDbTest \
  --tests com.readmates.archive.api.ArchiveControllerDbTest \
  --tests com.readmates.archive.api.ArchiveAndNotesDbTest \
  --tests com.readmates.support.MySqlFlywayMigrationTest \
  --tests com.readmates.support.ReadmatesMySqlSeedTest

알림 이메일 템플릿 copy, subject, club name 렌더링, HTML preview를 수정했다면 아래 focused command로 순수 템플릿 테스트와 preview report 생성을 먼저 확인합니다. Report는 테스트 산출물이며 Git에 커밋하지 않습니다.

./server/gradlew -p server unitTest \
  --tests com.readmates.notification.application.model.NotificationEmailTemplatesTest \
  --tests com.readmates.notification.application.model.NotificationEmailTemplatePreviewTest

공개 릴리즈 후보 점검

공개 저장소로 낼 수 있는 후보 tree를 만들고 검사합니다.

./scripts/build-public-release-candidate.sh
./scripts/public-release-check.sh .tmp/public-release-candidate

현재 private working tree를 직접 검사할 수도 있습니다.

./scripts/public-release-check.sh

Release helper script의 scanner pattern을 바꿨다면 fixture 검증도 실행합니다.

./scripts/verify-public-release-fixtures.sh

세부 정책은 공개 저장소 보안 문서와 scripts 문서를 참고합니다. 이 검사는 secret/path 실수를 줄이는 guardrail이며, 운영 secret rotation이나 GitHub 공개 전환을 대신하지 않습니다.

배포 연동 Smoke

배포 후 Cloudflare Pages marker와 OAuth start redirect URI를 실제 배포 origin에 대해 확인합니다. Secret은 필요 없지만 결과는 운영 상태이므로 문서나 Git에 붙이지 않습니다.

READMATES_SMOKE_BASE_URL=https://<pages-origin> \
READMATES_SMOKE_AUTH_BASE_URL=https://<pages-origin> \
./scripts/smoke-production-integrations.sh

Primary auth domain이나 registered club host도 확인할 때:

READMATES_SMOKE_BASE_URL=https://<pages-origin> \
READMATES_SMOKE_AUTH_BASE_URL=https://<primary-domain> \
READMATES_SMOKE_CLUB_HOST=https://<registered-club-host> \
./scripts/smoke-production-integrations.sh

READMATES_SMOKE_STRICT_GOOGLE=true는 Google 응답 본문에서 redirect_uri_mismatch를 추가로 찾으려는 옵션입니다. Google 로그인 화면 응답은 계정 상태와 지역에 따라 달라질 수 있으므로 기본 판정은 ReadMates가 생성한 provider redirect URL의 redirect_uri를 기준으로 합니다.

권장 확인 순서

작은 frontend 변경:

pnpm --dir front lint
pnpm --dir front test
pnpm --dir front build

인증, route, BFF, 화면 흐름 변경:

pnpm --dir front lint
pnpm --dir front test
pnpm --dir front build
pnpm --dir front test:e2e

Backend API, authorization, database 변경:

./scripts/server-ci-check.sh
./server/gradlew -p server integrationTest
pnpm --dir front test:e2e

공개 배포 또는 public repo 후보 점검:

./scripts/build-public-release-candidate.sh
./scripts/public-release-check.sh .tmp/public-release-candidate

Release baseline:

./scripts/server-ci-check.sh
./server/gradlew -p server integrationTest
pnpm --dir front lint
pnpm --dir front test
pnpm --dir front build

배포 후 OAuth/domain 연동 점검:

READMATES_SMOKE_BASE_URL=https://<pages-origin> \
READMATES_SMOKE_AUTH_BASE_URL=https://<pages-origin> \
./scripts/smoke-production-integrations.sh