콘텐츠로 이동

검증

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-package

implement 직후에 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 슬라이스를 기준으로 합니다.

시나리오 조각테스트 단위자리
이슈 제목, 본문, 라벨 추출extractPageContentsrc/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 가능합니다. 테스트가 슬라이스의 일부지 외부 소비자가 아니라는 신호입니다.

content script 테스트는 DOM이 필요하므로 vitest.config.tstest.environmentjsdom으로 잡힙니다. background와 popup 테스트가 chrome.storage 또는 chrome.runtime을 호출하면 vitest.setup.ts의 최소 mock으로 받습니다.

vitest.setup.ts
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 의 isValidPageContent

raw를 그대로 보여주지 않는 자리는 의도된 것입니다. 비개발자 또는 도메인 PM 입장에서 expected 'foo' to be 'bar' at line 32보다 “이 시나리오는 통과했고 이 시나리오는 실패했다”가 의사 결정에 직접 닿습니다. 동시에 raw 로그는 .claude/state/last-test.log에 통째로 남아 있어 깊이 파야 할 때 손이 닿습니다.

정적 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 buildpnpm run package가 각각 무엇을 떨구는지를 봅니다.