Back to posts

Superpowers - TDD & debugging

빨간불에서 시작하는 품질 루프 — Red-Green-Refactor, 4-phase 디버깅, 증거 기반 완료 선언의 내부 구조.


들어가며: 테스트가 실패했다

서브에이전트가 구현을 마치고 DONE을 보고했다. 컨트롤러가 spec reviewer를 디스패치하기 전에 테스트를 돌린다. 빨간불이 뜬다.

FAIL src/lib/search.test.ts
  ✕ returns empty array when no matches found
    Expected: []
    Received: undefined

일반적인 반응은 코드를 열어서 return []를 추가하는 것이다. 30초면 끝난다. 하지만 에이전트는 코드를 열지 않는다. 먼저 에러 메시지를 읽는다. undefined가 반환된다는 것은 함수가 명시적 반환 없이 끝났다는 뜻이다. 왜 명시적 반환이 없는가? 함수의 early return 조건을 확인한다. 검색어가 빈 문자열일 때의 분기가 누락되어 있다. 빈 문자열이 들어오면 검색 로직을 타지 않고 함수가 끝나버린다.

return []를 추가하면 이 테스트는 통과한다. 하지만 빈 문자열 처리가 누락된 것이 근본 원인이라면, 다른 입력 검증에서도 같은 패턴이 반복될 수 있다. 에이전트는 증상이 아닌 원인을 추적한다.

이 행동을 만드는 세 개의 스킬이 있다. test-driven-development, systematic-debugging, verification-before-completion. 이 글은 세 스킬의 원문을 역추적하며, 실패를 전제로 설계된 품질 루프의 내부 구조를 분석한다.


1. TDD의 Iron Law와 Red-Green-Refactor

테스트 없는 코드는 존재하지 않는다

test-driven-development 스킬의 Iron Law:

NO PRODUCTION CODE WITHOUT A FAILING TEST FIRST

이 규칙은 테스트의 순서를 강제한다. 코드를 먼저 쓰고 테스트를 나중에 쓰는 것이 아니다. 테스트를 먼저 쓰고, 그 테스트가 실패하는 것을 확인한 뒤에, 코드를 쓴다. 순서가 뒤바뀌면 어떻게 되는가?

원문이 이유를 설명한다:

If you didn't watch the test fail, you don't know if it tests the right thing.

테스트가 실패하는 걸 안 봤으면, 그 테스트가 올바른 것을 검증하는지 모른다. 테스트를 나중에 쓰면 즉시 통과한다. 즉시 통과하는 테스트는 아무것도 증명하지 않는다. 이미 있는 코드의 동작을 반복 서술할 뿐이다.

코드를 먼저 쓰고 나서 테스트를 붙이면? 원문의 규칙은 단호하다:

Write code before the test? Delete it. Start over.

No exceptions:
- Don't keep it as "reference"
- Don't "adapt" it while writing tests
- Don't look at it
- Delete means delete

코드를 "참고용"으로 남겨두는 것도 금지다. 남겨두면 그 코드를 기반으로 테스트를 쓰게 되고, 그러면 테스트가 구현을 검증하는 것이 아니라 구현을 추인하는 것이 된다. "Delete means delete" -- 삭제는 문자 그대로 삭제다.

Red-Green-Refactor 사이클

Iron Law를 실현하는 구체적 프로세스가 Red-Green-Refactor다.

RED (실패하는 테스트 작성)
  → Verify RED (실패 확인)
    → GREEN (최소 코드 작성)
      → Verify GREEN (통과 확인)
        → REFACTOR (정리)
          → Verify GREEN (여전히 통과 확인)
            → 다음 RED

각 단계를 원문 인용과 함께 분석한다.

RED: 실패하는 테스트를 먼저 쓴다

Write one minimal test showing what should happen.

테스트는 하나의 동작만 검증한다. 원문이 좋은 테스트의 조건을 명시한다:

  • One behavior -- 하나의 동작
  • Clear name -- 이름이 동작을 설명
  • Real code (no mocks unless unavoidable) -- 실제 코드를 테스트 (mock은 불가피할 때만)

RED 단계에서 테스트를 쓴 뒤에는 반드시 실행해서 실패를 확인한다. 원문이 이 확인을 MANDATORY로 지정한다:

Verify RED - Watch It Fail

MANDATORY. Never skip.

Confirm:
- Test fails (not errors)
- Failure message is expected
- Fails because feature missing (not typos)

테스트가 "실패"하는 것과 "에러"를 내는 것은 다르다. 실패는 assertion이 통과하지 못한 것이고, 에러는 코드 자체가 실행되지 않은 것이다. 테스트가 에러를 내면, 기능이 없어서가 아니라 테스트 코드에 오타가 있어서일 수 있다. 그래서 실패 이유까지 확인한다: 기능이 없어서 실패해야지, 오타 때문에 에러가 나면 안 된다.

테스트가 즉시 통과하면? "Test passes? You're testing existing behavior. Fix test." -- 이미 있는 동작을 테스트하고 있다는 뜻이다. 테스트를 고쳐야 한다.

GREEN: 최소 구현만 한다

Write simplest code to pass the test.

핵심은 "simplest"다. 테스트를 통과하는 가장 간단한 코드를 쓴다. 원문이 과잉 구현의 예를 직접 대비한다.

좋은 예 -- 재시도를 3번 하는 함수:

async function retryOperation<T>(fn: () => Promise<T>): Promise<T> {
  for (let i = 0; i < 3; i++) {
    try {
      return await fn();
    } catch (e) {
      if (i === 2) throw e;
    }
  }
  throw new Error('unreachable');
}

나쁜 예 -- 같은 함수에 옵션을 추가한 것:

async function retryOperation<T>(
  fn: () => Promise<T>,
  options?: {
    maxRetries?: number;
    backoff?: 'linear' | 'exponential';
    onRetry?: (attempt: number) => void;
  }
): Promise<T> {
  // YAGNI
}

원문의 코멘트가 "YAGNI" (You Aren't Gonna Need It)다. 테스트가 요구하지 않는 기능을 미리 만들지 않는다. maxRetries 옵션, backoff 전략, onRetry 콜백 -- 전부 현재 테스트가 요구하지 않는 것들이다. 필요하면 나중에 새 테스트를 쓰고 그때 추가한다.

GREEN 단계에서도 검증은 필수다:

Test fails? Fix code, not test.

Other tests fail? Fix now.

테스트가 실패하면 코드를 고친다. 테스트를 고치지 않는다. 다른 테스트가 깨지면 즉시 고친다. "나중에 고치겠다"는 허용되지 않는다.

REFACTOR: 초록불일 때만

After green only:
- Remove duplication
- Improve names
- Extract helpers

Keep tests green. Don't add behavior.

리팩토링은 모든 테스트가 통과한 상태에서만 한다. 빨간불 상태에서 코드를 정리하면, 정리가 문제를 일으킨 것인지 원래 문제인지 구분할 수 없다. 초록불이 안전망이다. 리팩토링 후에도 테스트가 통과하면, 리팩토링이 동작을 바꾸지 않았다는 증거가 된다.

"Don't add behavior" -- 리팩토링 단계에서 새 기능을 추가하지 않는다. 새 기능이 필요하면 다시 RED로 돌아가서 테스트를 먼저 쓴다.

합리화 방지 테이블

test-driven-development 스킬은 에이전트가 TDD를 건너뛰려 할 때 떠올릴 수 있는 합리화 패턴을 명시적으로 차단한다. 핵심 항목을 인용한다.

ExcuseReality
"Too simple to test"Simple code breaks. Test takes 30 seconds.
"I'll test after"Tests passing immediately prove nothing.
"Tests after achieve same goals"Tests-after = "what does this do?" Tests-first = "what should this do?"
"Deleting X hours is wasteful"Sunk cost fallacy. Keeping unverified code is technical debt.
"TDD will slow me down"TDD faster than debugging. Pragmatic = test-first.
"Need to explore first"Fine. Throw away exploration, start with TDD.

이 테이블에서 가장 교묘한 합리화는 "Tests after achieve same goals"다. 원문이 이 합리화를 별도로 반박한다:

No. Tests-after answer "What does this do?" Tests-first answer "What should this do?"

Tests-after are biased by your implementation. You test what you built, not what's
required. You verify remembered edge cases, not discovered ones.

Tests-after는 구현에 편향된다. 내가 만든 것을 테스트하지, 요구된 것을 테스트하지 않는다. Tests-first는 구현 전에 "무엇이 되어야 하는가"를 정의하므로, 구현 편향이 없다.


2. systematic-debugging의 4-phase 프로세스

추측하지 말고 읽어라

systematic-debugging 스킬의 Iron Law:

NO FIXES WITHOUT ROOT CAUSE INVESTIGATION FIRST

도입 시나리오로 돌아가 보자. 테스트가 실패했을 때 return []을 추가하고 싶은 충동이 있다. 이 충동이 정확히 원문이 금지하는 것이다:

Random fixes waste time and create new bugs. Quick patches mask underlying issues.

ALWAYS find root cause before attempting fixes. Symptom fixes are failure.

무작위 수정은 시간을 낭비하고 새로운 버그를 만든다. 빠른 패치는 근본 문제를 가린다. 증상 수정은 실패다.

Phase 1: Root Cause Investigation

BEFORE attempting ANY fix:

1. Read Error Messages Carefully
   - Don't skip past errors or warnings
   - They often contain the exact solution
   - Read stack traces completely
   - Note line numbers, file paths, error codes

2. Reproduce Consistently
   - Can you trigger it reliably?
   - What are the exact steps?
   - Does it happen every time?
   - If not reproducible → gather more data, don't guess

3. Check Recent Changes
   - What changed that could cause this?
   - Git diff, recent commits
   - New dependencies, config changes
   - Environmental differences

원문의 첫 번째 지시가 "Read Error Messages Carefully"다. 에러 메시지를 읽어라. 건너뛰지 말라. 이것이 도입 시나리오에서 에이전트가 코드를 열기 전에 에러 메시지부터 읽은 이유다.

에러 메시지에는 종종 답이 들어 있다. Expected: [], Received: undefined -- 이 메시지 자체가 "함수가 값을 반환하지 않았다"는 진단이다. 스택 트레이스를 완전히 읽으면 어디서 문제가 발생했는지 알 수 있다. 줄 번호, 파일 경로, 에러 코드를 기록한다.

재현이 안 되면 추측하지 않는다. "If not reproducible → gather more data, don't guess" -- 데이터를 더 모으되, 추측은 하지 않는다.

다중 컴포넌트 시스템에서는 진단 계측을 추가한다:

For EACH component boundary:
  - Log what data enters component
  - Log what data exits component
  - Verify environment/config propagation
  - Check state at each layer

Run once to gather evidence showing WHERE it breaks
THEN analyze evidence to identify failing component
THEN investigate that specific component

각 컴포넌트 경계에서 입력과 출력을 로깅한다. 한 번 실행해서 어디서 깨지는지 증거를 수집한다. 증거를 분석해서 실패하는 컴포넌트를 특정한다. 그 컴포넌트만 조사한다. 전체를 동시에 디버깅하지 않는다.

Phase 2: Pattern Analysis

1. Find Working Examples
   - Locate similar working code in same codebase
   - What works that's similar to what's broken?

2. Compare Against References
   - If implementing pattern, read reference implementation COMPLETELY
   - Don't skim - read every line
   - Understand the pattern fully before applying

3. Identify Differences
   - What's different between working and broken?
   - List every difference, however small
   - Don't assume "that can't matter"

동작하는 유사 코드를 찾아서 비교한다. 원문이 강조하는 것은 "Don't skim - read every line"과 "Don't assume 'that can't matter'"다. 대충 훑지 말고 한 줄씩 읽어라. "이건 상관없을 거야"라고 가정하지 말라. 작은 차이가 버그의 원인인 경우가 많다.

Phase 3: Hypothesis and Testing

1. Form Single Hypothesis
   - State clearly: "I think X is the root cause because Y"
   - Write it down
   - Be specific, not vague

2. Test Minimally
   - Make the SMALLEST possible change to test hypothesis
   - One variable at a time
   - Don't fix multiple things at once

3. Verify Before Continuing
   - Did it work? Yes → Phase 4
   - Didn't work? Form NEW hypothesis
   - DON'T add more fixes on top

과학적 방법을 따른다. 가설을 하나 세운다. 가설을 검증할 최소한의 변경을 한다. 한 번에 하나의 변수만 바꾼다.

"Don't fix multiple things at once" -- 여러 개를 동시에 고치면 어떤 수정이 효과가 있었는지 알 수 없다. 하나를 고치고 확인한다. 안 되면 새 가설을 세운다. 기존 수정 위에 추가 수정을 쌓지 않는다.

"When You Don't Know" 항목도 있다:

- Say "I don't understand X"
- Don't pretend to know
- Ask for help
- Research more

모르면 모른다고 한다. 아는 척하지 않는다.

Phase 4: Implementation

1. Create Failing Test Case
   - Simplest possible reproduction
   - Automated test if possible
   - MUST have before fixing

2. Implement Single Fix
   - Address the root cause identified
   - ONE change at a time
   - No "while I'm here" improvements
   - No bundled refactoring

3. Verify Fix
   - Test passes now?
   - No other tests broken?
   - Issue actually resolved?

수정 전에 실패하는 테스트 케이스를 만든다. 여기서 TDD 스킬과 직접 연결된다. 원문도 이 연결을 명시한다: "Use the superpowers:test-driven-development skill for writing proper failing tests". 버그 수정도 TDD 사이클의 일부다.

"No 'while I'm here' improvements" -- "여기 온 김에" 개선하지 않는다. 하나의 버그에 하나의 수정. 리팩토링이나 개선은 별도 작업이다.

3번 수정 실패 시 아키텍처 재검토

Phase 4에 조건부 규칙이 있다:

If Fix Doesn't Work:
- STOP
- Count: How many fixes have you tried?
- If < 3: Return to Phase 1, re-analyze with new information
- If ≥ 3: STOP and question the architecture
- DON'T attempt Fix #4 without architectural discussion

3번 수정을 시도해도 안 되면, 더 이상 같은 접근으로 시도하지 않는다. 원문이 이 상황을 진단한다:

Pattern indicating architectural problem:
- Each fix reveals new shared state/coupling/problem in different place
- Fixes require "massive refactoring" to implement
- Each fix creates new symptoms elsewhere

STOP and question fundamentals:
- Is this pattern fundamentally sound?
- Are we "sticking with it through sheer inertia"?
- Should we refactor architecture vs. continue fixing symptoms?

Discuss with your human partner before attempting more fixes

This is NOT a failed hypothesis - this is a wrong architecture.

수정할 때마다 다른 곳에서 새 문제가 나타나고, 수정에 대규모 리팩토링이 필요하고, 각 수정이 새 증상을 만든다면 -- 이것은 가설이 틀린 게 아니라 아키텍처가 틀린 것이다. "Are we 'sticking with it through sheer inertia'?" -- 관성으로 버티고 있는 건 아닌가?

3이라는 숫자의 설계 의도는 명확하다. 1-2번은 가설이 틀릴 수 있다. 새 정보로 다시 분석하면 된다. 하지만 3번째도 실패하면, 개별 수정의 문제가 아니라 구조적 문제일 가능성이 높다. 더 시도하는 것은 시간 낭비다.

합리화 방지

systematic-debugging도 합리화 테이블이 있다:

ExcuseReality
"Issue is simple, don't need process"Simple issues have root causes too. Process is fast for simple bugs.
"Emergency, no time for process"Systematic debugging is FASTER than guess-and-check thrashing.
"Just try this first, then investigate"First fix sets the pattern. Do it right from the start.
"I see the problem, let me fix it"Seeing symptoms ≠ understanding root cause.
"One more fix attempt" (after 2+ failures)3+ failures = architectural problem. Question pattern, don't fix again.

"Emergency, no time for process" -- 급할수록 프로세스를 따라야 한다. 원문이 이 직관에 반하는 주장의 근거를 제시한다:

Systematic approach: 15-30 minutes to fix
Random fixes approach: 2-3 hours of thrashing
First-time fix rate: 95% vs 40%
New bugs introduced: Near zero vs common

체계적 접근은 15-30분, 무작위 수정은 2-3시간. 첫 시도 수정율은 95% 대 40%. 새 버그 발생은 거의 없음 대 흔함. 급할 때 프로세스를 건너뛰면 오히려 더 오래 걸린다.


3. verification-before-completion: 증거 없이 완료를 선언하지 않는다

주장에는 반드시 증거

verification-before-completion 스킬의 Iron Law:

NO COMPLETION CLAIMS WITHOUT FRESH VERIFICATION EVIDENCE

"완료"라고 말하려면, 이 메시지 안에서 검증 명령을 실행하고 결과를 확인해야 한다. 이전 실행 결과나 "아마 될 것이다"는 증거가 아니다.

원문이 이 스킬의 핵심 가치를 한 문장으로 정의한다:

Claiming work is complete without verification is dishonesty, not efficiency.

검증 없이 완료를 주장하는 것은 효율이 아니라 부정직이다.

5단계 게이트

원문의 Gate Function:

BEFORE claiming any status or expressing satisfaction:

1. IDENTIFY: What command proves this claim?
2. RUN: Execute the FULL command (fresh, complete)
3. READ: Full output, check exit code, count failures
4. VERIFY: Does output confirm the claim?
   - If NO: State actual status with evidence
   - If YES: State claim WITH evidence
5. ONLY THEN: Make the claim

Skip any step = lying, not verifying

각 단계의 역할:

IDENTIFY -- 무엇을 실행해야 이 주장을 증명할 수 있는지 파악한다. "테스트가 통과한다"를 주장하려면 테스트 명령이 필요하다. "빌드가 성공한다"를 주장하려면 빌드 명령이 필요하다.

RUN -- 명령을 실행한다. "fresh, complete" -- 이전 캐시나 부분 실행이 아닌, 새로운 전체 실행이어야 한다.

READ -- 출력을 전부 읽는다. exit code를 확인한다. 실패 횟수를 센다. 출력 중간에 경고가 있을 수 있다. 마지막 줄만 보면 안 된다.

VERIFY -- 출력이 주장을 확인하는가? 아니면 실제 상태를 증거와 함께 보고한다. 맞으면 증거와 함께 주장한다.

ONLY THEN -- 그제서야 주장할 수 있다.

"Skip any step = lying, not verifying" -- 어떤 단계라도 건너뛰면 검증이 아니라 거짓말이다.

"should", "probably", "seems to" 금지

원문의 Red Flags:

- Using "should", "probably", "seems to"
- Expressing satisfaction before verification ("Great!", "Perfect!", "Done!", etc.)
- About to commit/push/PR without verification
- Trusting agent success reports
- Relying on partial verification
- ANY wording implying success without having run verification

"should", "probably", "seems to" -- 이 단어들이 등장하면 검증을 안 했다는 신호다. "테스트가 통과할 것이다"(should pass)는 "테스트를 돌렸더니 통과했다"(output shows 34/34 pass)와 다르다.

"Great!", "Perfect!", "Done!" -- 검증 전에 만족을 표현하는 것도 금지다. 만족은 증거를 본 후에만 표현할 수 있다.

"Trusting agent success reports" -- 서브에이전트가 "완료했습니다"라고 보고해도 그 보고를 신뢰하지 않는다. 3편에서 분석한 spec reviewer의 "CRITICAL: Do Not Trust the Report" 규칙과 같은 맥락이다. 보고가 아닌 독립적 검증이 필요하다.

원문이 올바른 패턴과 잘못된 패턴을 대비한다:

Tests:
✅ [Run test command] [See: 34/34 pass] "All tests pass"
❌ "Should pass now" / "Looks correct"

Build:
✅ [Run build] [See: exit 0] "Build passes"
❌ "Linter passed" (linter doesn't check compilation)

린터가 통과했다고 빌드가 통과하는 것이 아니다. 린터는 문법과 스타일을 검사하고, 빌드는 컴파일과 번들링을 수행한다. 각 주장에는 그 주장에 맞는 검증 명령이 필요하다.

합리화 방지

ExcuseReality
"Should work now"RUN the verification
"I'm confident"Confidence ≠ evidence
"Just this once"No exceptions
"Linter passed"Linter ≠ compiler
"Agent said success"Verify independently
"Partial check is enough"Partial proves nothing

"Confidence ≠ evidence" -- 확신은 증거가 아니다. 아무리 확신해도 검증 명령을 실행해야 한다. "Partial proves nothing" -- 부분적 검사는 아무것도 증명하지 않는다.


4. 품질 루프: TDD → debugging → verification

세 스킬은 독립적으로 존재하지 않는다. 하나의 루프를 형성한다.

[TDD: RED]
   테스트를 쓴다, 실패를 확인한다
       ↓
[TDD: GREEN]
   최소 코드를 쓴다, 통과를 확인한다
       ↓
[TDD: REFACTOR]
   정리한다, 통과를 다시 확인한다
       ↓
  테스트 실패 발생?
       ↓ yes
[DEBUGGING: Phase 1]
   에러를 읽는다, 재현한다, 원인을 추적한다
       ↓
[DEBUGGING: Phase 2-3]
   패턴을 비교한다, 가설을 세운다, 최소 변경으로 검증한다
       ↓
[DEBUGGING: Phase 4]
   실패하는 테스트를 만든다 → TDD RED로 연결
       ↓
[VERIFICATION]
   검증 명령을 실행한다, 출력을 읽는다, 증거를 확인한다
       ↓
   완료를 선언한다

이 루프의 연결 지점을 살펴보면:

TDD → debugging: TDD의 GREEN 단계에서 테스트가 실패하면, systematic-debugging의 Phase 1으로 진입한다. 코드를 고치는 것이 아니라 에러를 먼저 읽는다.

debugging → TDD: systematic-debugging의 Phase 4에서 "실패하는 테스트 케이스를 만든다"고 했다. 이것이 TDD의 RED 단계와 동일하다. 원문이 이 연결을 명시한다:

Use the superpowers:test-driven-development skill for writing proper failing tests

버그 수정도 TDD 사이클의 일부다. test-driven-development 스킬도 이를 역으로 명시한다:

Bug found? Write failing test reproducing it. Follow TDD cycle. Test proves fix
and prevents regression.

Never fix bugs without a test.

버그를 발견하면 재현하는 실패 테스트를 쓴다. TDD 사이클을 따른다. 테스트가 수정을 증명하고 회귀를 방지한다.

TDD/debugging → verification: 모든 테스트가 통과하고 버그가 수정된 후, 완료를 선언하기 전에 verification 게이트를 통과해야 한다. systematic-debugging도 이 연결을 명시한다:

superpowers:verification-before-completion - Verify fix worked before claiming success

수정이 성공했다고 주장하기 전에 검증하라. 세 스킬이 서로를 참조하며 하나의 루프를 형성한다.


마무리: 테스트 통과, 그 다음은?

세 스킬의 품질 루프를 정리하면:

  1. test-driven-development -- 실패하는 테스트를 먼저 쓰고, 최소 코드로 통과시키고, 초록불에서만 정리한다. 순서를 어기면 처음부터 다시 한다.
  2. systematic-debugging -- 에러를 읽고, 재현하고, 패턴을 비교하고, 단일 가설로 검증한다. 3번 실패하면 아키텍처를 의심한다.
  3. verification-before-completion -- 증거 없이 완료를 주장하지 않는다. 검증 명령을 실행하고, 출력을 읽고, 그제서야 주장한다.

도입 시나리오로 돌아가 보자. 서브에이전트가 구현을 완료하고, 테스트가 실패했다. 에이전트는 systematic-debugging으로 원인을 추적하고, test-driven-development로 수정 사이클을 돌리고, verification-before-completion으로 결과를 검증한다. 모든 테스트가 통과하고, 빌드가 성공하고, 증거가 확보되었다.

그 다음은 무엇인가? 코드가 동작하는 것과 코드가 병합 가능한 것은 다르다. 코드 리뷰를 받아야 하고, 브랜치를 정리해야 하고, 최종적으로 통합해야 한다. 다음 편에서는 requesting-code-review, receiving-code-review, finishing-a-development-branch 스킬을 해부한다.