검증
architecture에서 잡은 FSD 슬라이스에 코드를 채워 넣은 다음, 그 코드가 사용자가 적은 시나리오대로 도는지 한 번 묻는 자리가 필요합니다. base는 그 질문에 정적과 동작 두 갈래로 답합니다.
| 도구 | 보는 자리 |
|---|---|
review-extension | 정적 6차원. FSD 경계, MV3 금기 패턴, 어댑터 분리, 의존 cycle, 메시지 타입 일관성, lint와 typecheck |
/test-extension | 동작 차원. docs/design/extension-spec.md §2 시나리오를 실제 함수 호출로 검증 |
둘은 같은 코드를 다른 각도에서 봅니다. review가 “코드가 깨끗한가”를 본다면 test는 “코드가 사용자 의도대로 도는가”를 봅니다. 한쪽만 통과해서는 진짜 통과가 아닙니다.
흐름의 자리
섹션 제목: “흐름의 자리”implement-* (영역별 구현) ↓/test-extension (spec → 테스트 → 실행) ↓review-extension (정적 6차원) ↓build-and-packageimplement 직후에 test가 먼저 도는 이유는 단순합니다. 정적 검사는 동작 결과 없이도 통과할 수 있는데, 동작이 깨졌으면 review가 무엇을 통과시켜도 의미가 없습니다. 동작 신호를 먼저 받고 그 위에 정적 차원을 얹는 순서입니다.
spec-driven, 셋업 boilerplate와의 차별점
섹션 제목: “spec-driven, 셋업 boilerplate와의 차별점”일반 boilerplate는 “Vitest 셋업”을 줍니다. 사용자가 직접 테스트 코드를 짜야 합니다. /test-extension은 같은 자리에 한 단계를 더 둡니다.
extension-spec.md §2 시나리오 (사용자 작성) ↓/test-extension 가 파싱·매핑 ↓시나리오 단위 → Vitest 테스트 코드 (AI 생성) ↓src/{slice}/__tests__/ 안에 co-located사용자는 시나리오 한 줄을 적습니다. “GitHub 이슈 페이지에서 제목, 본문, 라벨을 추출해 Notion에 새 페이지로 저장.” 이 한 줄을 스킬이 검증 가능한 단위 셋으로 분해해 테스트 함수를 만듭니다.
시나리오 → 테스트 매핑
섹션 제목: “시나리오 → 테스트 매핑”분해는 FSD 슬라이스를 기준으로 합니다.
| 시나리오 조각 | 테스트 단위 | 자리 |
|---|---|---|
| 이슈 제목, 본문, 라벨 추출 | extractPageContent | src/features/analyze-page/__tests__/ |
| Notion에 새 페이지로 저장 | notionAdapter.send(payload) | src/shared/api/backend/__tests__/ |
| 재시도 시 같은 데이터 다시 전송 | retry 로직 | src/features/send-to-backend/__tests__/ |
테스트 패턴은 세 갈래가 박혀 있습니다.
- content 또는 feature 테스트: jsdom에서
document.body.innerHTML을 깔고 추출 함수를 호출, 결과 객체의 필드를 검증 - 어댑터 테스트:
vi.stubGlobal('fetch', mock)으로 fetch를 가로채 페이로드 모양과 헤더를 검증 - entities 도메인 테스트: discriminated union 검증 함수처럼 외부 의존이 없는 순수 단위
테스트 파일은 슬라이스 안 __tests__/에 co-located로 떨어집니다. FSD public API를 통과하지 않고 슬라이스 내부를 직접 import 가능합니다. 테스트가 슬라이스의 일부지 외부 소비자가 아니라는 신호입니다.
chrome.* mock과 jsdom
섹션 제목: “chrome.* mock과 jsdom”content script 테스트는 DOM이 필요하므로 vitest.config.ts의 test.environment가 jsdom으로 잡힙니다. background와 popup 테스트가 chrome.storage 또는 chrome.runtime을 호출하면 vitest.setup.ts의 최소 mock으로 받습니다.
global.chrome = { storage: { local: { get: vi.fn().mockResolvedValue({}), set: vi.fn().mockResolvedValue(undefined), }, }, runtime: { sendMessage: vi.fn(), onMessage: { addListener: vi.fn() } },} as any처음부터 두꺼운 fake로 깔지 않습니다. 테스트가 늘어날수록 어떤 chrome API가 필요한지 명확해지므로, 필요한 시점에 setup.ts에 한 줄 더 추가하는 편이 fake를 따라가는 것보다 쌉니다.
시나리오 언어로 결과 합성
섹션 제목: “시나리오 언어로 결과 합성”raw Vitest 출력은 사용자에게 그대로 가지 않습니다. 스킬이 결과를 시나리오 단위로 다시 합성합니다.
총 시나리오: 5개 — 4개 통과, 1개 실패
✓ 페이지 컨텐츠 추출 — GitHub 이슈에서 제목, 본문, 라벨 정상 추출✓ 백엔드 전송 (server-relay) — Bearer 토큰 + POST 정상✗ PageContent 검증 — 빈 title 거절: 빈 title이 들어왔는데 통과시켰다 파일: src/entities/page-content/__tests__/model.test.ts:8 고치는 곳: model/validate.ts 의 isValidPageContentraw를 그대로 보여주지 않는 자리는 의도된 것입니다. 비개발자 또는 도메인 PM 입장에서 expected 'foo' to be 'bar' at line 32보다 “이 시나리오는 통과했고 이 시나리오는 실패했다”가 의사 결정에 직접 닿습니다. 동시에 raw 로그는 .claude/state/last-test.log에 통째로 남아 있어 깊이 파야 할 때 손이 닿습니다.
정적 측 review-extension
섹션 제목: “정적 측 review-extension”정적 6차원은 별 페이지로 빼지 않고 한 단락으로 짚습니다. review-extension 스킬이 lint와 typecheck 위에서 봅니다. FSD 경계 위반, MV3 금기 패턴 (예: executeScript의 inline code, eval 의존), 어댑터 인터페이스 위반, 메시지 union 누락, 같은 페이로드 모양의 중복 정의, AI_AUTOMATION.md의 도메인 규칙 위반. 점수가 두 번 연속 changes_requested로 떨어지면 stage가 blocked으로 바뀌고 /recover-from-blocked가 트리거 자리에 들어갑니다. 그 흐름은 복구 슬래시 명령 단락에 있습니다.
비개발자 호흡
섹션 제목: “비개발자 호흡”비개발자 입장에서 같은 자리를 한 줄 안내로 정리한 곳이 비개발자용 04장 끝에 있습니다. 시나리오를 적는 사람은 spec 작성자고, 검증 코드는 AI가 만들고, 보고는 시나리오 언어로 돌아온다는 같은 원리입니다.
다음 섹션은 build-and-package입니다. architecture가 잡은 FSD 트리가 manifest.config.ts와 어떻게 만나서 chrome이 로드 가능한 dist/가 되는지, pnpm run build와 pnpm run package가 각각 무엇을 떨구는지를 봅니다.