콘텐츠로 이동

검증

architecture에서 잡은 FSD 슬라이스에 코드를 채워 넣은 다음, 그 코드가 사용자가 적은 시나리오대로 도는지 한 번 묻는 단계가 필요합니다. base는 그 질문에 정적 검사와 동작 검사 두 갈래로 답합니다.

도구무엇을 보나
review-extension정적 여섯 관점을 봅니다. FSD 경계, MV3 금기 패턴, 어댑터 분리, 의존 cycle, 메시지 타입 일관성, lint와 typecheck
/test-extension동작 관점을 봅니다. docs/design/extension-spec.md §2 시나리오를 실제 함수 호출로 검증

둘은 같은 코드를 다른 각도에서 봅니다. review가 “코드가 깨끗한가”를 본다면 test는 “코드가 사용자 의도대로 도는가”를 봅니다. 한쪽만 통과해서는 실제 통과가 아닙니다.

implement-* (영역별 구현)
↓
/test-extension (spec → 테스트 → 실행)
↓
review-extension (정적 여섯 관점)
↓
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.ts의 test.environment가 jsdom으로 잡힙니다. 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를 따라가는 것보다 비용이 적습니다.

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

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

정적 여섯 관점은 별도 페이지로 빼지 않고 한 단락으로 짚습니다. 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가 각각 무엇을 만들어 내는지를 봅니다.