PR Preflight Policy — 사전 테스트 게이트 정책
기능 작업의 작성·소유·등재·증거 정본은
모든 PR 은 변경된 패키지 범위에서 unit + widget + integration 테스트가 PASS 한 뒤에만 생성될 수 있다. 이 문서는 구속력 있는 정책 이며, 스킬/커맨드/훅이 모두 이를 준수한다.
1. 원칙#
기능 작업의 작성·소유·등재·증거 정본은 feature-verification이다. 모든 UI feature는 unit, widget, golden, Widgetbook, BDD 5종을 작성·배선한다. 일반 테스트 PASS만으로 나머지를 통과시키지 않고 변경 기능과 관련 조립에 대해 golden compare·gallery 입력, 카탈로그 생성/렌더·Knob, AC↔BDD 모드별 실행을 확인한다. UI 없는 역할의 N/A는 근거를 기록한다. 승인된 skip는 DEFERRED이며 완료/PASS가 아니다.
-
모든 PR 은
/cc-dev:pr:preflightPASS 필수 — 자동(/cc-dev:run) 또는 수동 실행 - Unit + 적용 가능한 Widget 레이어는 항상 실행 — skip 금지. 순수 headless 역할의 UI N/A는 소스 근거로 판정하며 --skip-widget 면제와 구분한다.
-
Integration 레이어는 조건부 실행 가능
- 로컬:
@smoke또는 명시 요청 - CI: 전량
- 로컬:
- 변경된 패키지만 대상 — 전체 레포 강제 실행 금지 (속도 저하)
- Skip 사용은 감사 기록 —
.claude/preflight-skip.log
2. 레이어별 실행 정책#
| 레이어 | 로컬 | CI | Skip 정책 |
|---|---|---|---|
| Unit | ✅ 필수 | ✅ 전량 | 절대 불가 |
| Widget | ✅ 필수 | ✅ 전량 | 절대 불가 |
| Golden | capture·같은 OS의 compare 또는 명시적 CI 대기 | 동일 Linux 이미지 compare 필수 | capture/skip를 compare PASS로 보고 금지 |
| Widgetbook | 기능 등록·생성·렌더/Knob smoke | 조립·렌더·웹 빌드 및 등록 대조 | 카탈로그 앱 존재만으로 feature 등재 판정 금지 |
| BDD | 실제 co_bdd 위젯/legacy 러너·step/assertion 검사 | 선언된 실행 대상 필수 | 디바이스 부재는 BLOCKED/CI 대기, 작성은 필수 |
Integration (@smoke) |
⚠️ 권장 (디바이스 있을 때) | ✅ 필수 | 디바이스 없음 시 자동 skip |
| Integration (full) | ⚠️ 선택 (--with-integration=full) |
✅ 필수 | 로컬 skip 허용 |
3. 변경 패키지 감지 규칙#
포함 대상#
feature/*/— feature 모듈app/*/— 앱 패키지package/*/— 공용 패키지 (단, 테스트 파일 있을 때만)backend/*/— 서버팟 패키지 (serverpod-test-agent위임 범위)-
기능
test/src/golden·test/src/bdd·독립widgetbook/패키지와 역의존 gallery/catalog 조립
부수 변경 (단독이면 scope 미추가)#
-
**/*.g.dart,**/*.freezed.dart,**/*.gr.dart,**/*.config.dart,**/*.gen.dart,**/*.mocks.dart
⛔ **/*.patrol_test.dart 는 이 목록에서 빠졌습니다. 코드 생성기가 없으므로 이제 손으로
쓴 테스트 소스입니다 — 다른 실제 소스 변경과 똑같이 scope 에 포함해야 합니다. 자동 생성물
취급하면 scenarios/*_test.dart 만 바뀐 PR 이 Integration 레이어를 건너뛰게 됩니다.
제외 대상#
shared/*/중 순수 설정 —--include-shared없으면 제외-
docs/,*.md,.github/,.claude/,.cursor/,README.md,CHANGELOG.md i18n/*.json(언어 리소스만 변경 시 테스트 불필요, 다만.dart생성물 변경 시는 포함)
4. Skip 예외 규정#
정책상 거부 옵션#
--skip-unit→ 항상 거부 (exit code 2)--skip-widget→ 항상 거부 (exit code 2)
조건부 허용#
--skip-integration→ 로컬 기본값 (감사 기록 불필요)-
--skip-all→ hotfix 전용, 다음 조건 모두 만족--reason사유 명시- 커밋 메시지에
hotfix:,fix!:,revert:접두사 중 하나 - 감사 로그 기록
감사 로그 포맷 (.claude/preflight-skip.log)#
2026-04-23T12:34:56Z | user=dongwoo | branch=hotfix/payment-crash | base=development | skip=all | reason="프로덕션 결제 크래시 응급 패치"
- 이 파일은 커밋 대상 (감사 추적)
- CI 가 파일 존재 시 강한 게이트로 대체 실행 (전량 unit + widget + integration)
5. /cc-dev:run 과의 통합#
/cc-dev:run 의 Step 8 (테스트 작성 및 검증) 은 본 정책의 구현체:
Step 8: 테스트 작성 및 검증 (필수 게이트) ⚠️
├── [테스트 코드 작성] ← cc-flutter:test 에이전트
├── [PR Preflight 실행] ← /cc-dev:pr:preflight --auto-scope
│ ├── Unit (변경 패키지)
│ ├── Widget (변경 패키지)
│ ├── Golden compare·gallery 수집 증거
│ ├── Widgetbook 생성·등록·렌더/Knob
│ ├── BDD 모드별 실제 실행/단언
│ └── Integration @smoke (조건부; 미실행 상태 별도)
└── ⚠️ 실패 시 Step 9 (PR 생성) 진행 불가
/cc-dev:run --skip-tests 는 본 정책의 --skip-all 과 동일하게 취급 (사유 필수).
6. lefthook 훅 통합 (프로젝트 레포)#
프로젝트 lefthook.yml 의 pre-push 에 권장 블록:
pre-push:
parallel: false
commands:
# 기존: format-check, analyze
# 추가: preflight (unit + widget 만 — integration 은 /cc-dev:pr:preflight 로 수동)
preflight-fast:
tags: test
run: |
if [ "$LEFTHOOK_SKIP_PREFLIGHT" = "1" ]; then
echo "⚠️ LEFTHOOK_SKIP_PREFLIGHT=1 감지 — preflight 건너뜀"
exit 0
fi
# 환경에 claude-code-cli 가 있으면 /cc-dev:pr:preflight, 없으면 melos 직접
if command -v claude-code-cli >/dev/null; then
claude-code-cli run /cc-dev:pr:preflight --from-hook --skip-integration
else
# 폴백
BASE=$(git rev-parse --abbrev-ref '@{u}' 2>/dev/null | sed 's|.*/||')
BASE=${BASE:-development}
CHANGED=$(git diff --name-only "$BASE"..HEAD | grep -E '^(feature|app|package|backend)/' | cut -d/ -f1-3 | sort -u)
if [ -n "$CHANGED" ]; then
# pubspec.yaml name 추출 후 melos scope 실행
melos run test --scope="..."
fi
fi
주의: integration 은 hook 에 두지 않음 (push 속도 저하). 개발자가 /cc-dev:pr:preflight --with-integration
명시 실행.
7. CI 연계#
CI 워크플로우 요구사항:
- 준비 완료 PR(초안 아님) 트리거 시 전량 unit + widget + integration 실행 (변경 패키지 한정 아님 — CI 는 방어선). 초안 PR 은 가벼운 층(포맷 · 분석 · 변경 패키지 단위)만 돈다 — 로컬 preflight 가 이미 같은 범위를 돌았다. 같은 git 트리를 이미 검증한 기록이 있으면(머지 뒤 push · 승격 PR) 다시 돌지 않는다 (ci-receipts — 검증 기록 · 2층 CI)
-
.claude/preflight-skip.log최근 엔트리 감지 시 강한 게이트:- 전체 레포 unit + widget + integration 실행
- 실패 시 머지 차단
- Nightly 는 skip 로그 · 검증 기록을 무시하고 전량 실행(트리 밖 입력의 드리프트를 잡는다)
예시 .github/workflows/preflight-gate.yml:
name: preflight-gate
on:
pull_request:
branches: [development, main]
# 초안 → 준비 완료(ready_for_review) 때 무거운 층이 돈다. types 를 적으면 기본 셋이 사라지므로 함께 적는다
types: [opened, synchronize, reopened, ready_for_review]
jobs:
unit-widget:
# 초안에서는 로컬 preflight 가 같은 범위를 이미 돌았다 — 준비 완료부터 전량
if: ${{ !github.event.pull_request.draft }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: subosito/flutter-action@v2
- run: melos bootstrap
- run: melos run test
integration-smoke:
if: ${{ !github.event.pull_request.draft }}
runs-on: macos-latest
steps:
# Patrol 시뮬레이터 실행
8. 실패 시 조치#
Unit FAIL#
flutter test <실패 파일>재실행cc-flutter:test에이전트 위임- 수정 → 재커밋 →
/cc-dev:pr:preflight재실행
Widget FAIL#
flutter test <실패 파일>재실행- Mock 설정 재검토
cc-flutter:test에이전트 위임
Integration FAIL (Patrol)#
-
scenarios/*_test.dart는 손으로 쓴 파일이다 — 코드 생성기가 없으므로.feature를 고쳤으면scenarios/*_test.dart도 직접 고쳤는지 먼저 확인(dart analyze로 step 이름 불일치 검출) patrol develop+patrol_mcp run/screenshot/native-tree로 원인 분석- TestDriver step 검토: K 상수 적용 여부 (BDD 는
PatrolTestDriver로만 실행) cc-e2e:step-implement재진입- staging 서버 상태 확인 (네트워크/데이터 의존)
9. 금지 사항#
- ❌ Unit/Widget skip 허용
- ❌ 전체 레포 전량 실행 기본화 (
--full은 명시 요청 시만) - ❌ 감사 로그(
preflight-skip.log)를.gitignore - ❌ hook 안에서 integration 자동 실행 (push 속도 저하)
- ❌ 변경 없는 패키지를 scope 에 포함
10. 관련 문서#
- 스킬 본체:
cc-dev/skills/pr/SKILL.md - 커맨드:
cc-dev/commands/pr/preflight.md - 개발 사이클:
cc-dev/commands/run.md(Step 8) - E2E 저작:
cc-e2e/skills/feature-test-bundle,scenario-lock -
테스트 표준:
cc-flutter/skills/unit-testing/SKILL.mdcc-flutter/skills/widget-testing/SKILL.mdcc-flutter/skills/bdd-testing/SKILL.mdcc-flutter/skills/integration-testing/SKILL.mdcc-flutter/rules/bdd-test-patterns.md