격리된(quarantined) 테스트는 미래 커버리지에 대한 대출과 같으며, 대부분의 팀은 이 이자율을 기록하지 않습니다.
에이전트 패치가 테스트를 실패하게 만들어 그 테스트를 동결할 때, 당신은 아무것도 고치지 않은 것입니다. 단지 침묵을 구매했을 뿐입니다. 이 침묵에는 대가가 있습니다. 동결된 테스트가 지키던 코드를 건드리는 이후의 모든 패치는 검증되지 않은 상태로 배포되며, 스위트(suite)가 녹색(green)이기 때문에 아무도 알아차리지 못합니다.
이 글은 그 대출을 명확하게 만들기 위한 세 가지 아티팩트를 제공합니다. 즉, 동결하기 전에 플래크의 원인을 규명하는 4칸 재현 매트릭스, 동결 비용을 기록하는 원장(ledger) 항목, 그리고 더 이상 가지고 있지 않은 커버리지를 패치가 소모하는 것을 막는 CI 게이트입니다.
에이전트 패치가 이 벽에 먼저 부딪히는 이유
사람이 작성한 플래키 테스트는 천천히 발견됩니다. 한 엔지니어가 화요일에 실패를 보고, 재실행하고, 어깨를 으쓱하며 넘어갑니다. 하지만 에이전트 패치 루프는 그 시간표를 압축합니다. 패치가 누군가 빌드를 검토하는 것보다 빠르게 도착하기 때문에, 가장 저렴한 로컬 조치는 항상 테스트를 플래키로 표시하고 계속 진행하는 것입니다.
이러한 행동은 순간적으로는 합리적이지만 한 달에 걸쳐는 부식성이 강합니다. 각 동결은 유효 스위트에서 단언(assertion)을 제거하지만, 보고되는 테스트 수는 그대로 유지됩니다. 그 결과, 전체가 완벽해 보이는 스위트를 갖게 되지만 실제로는 감소하는 부분의 비율만을 검증하게 됩니다.
해결책은 동결 자체를 금지하는 것이 아닙니다. 동결이 승인되기 전에 원인 규명(attribution)과 대체품을 요구하는 것입니다.
1단계: 실제로 비교할 수 있는 기록을 포착하기
무엇이든 실행하기 전에, 실패 지점을 고정해야 합니다. 테스트 이름만으로는 증거가 될 수 없습니다. 노드 ID, 시드(seed), 그리고 마지막 출력 라인이 필요합니다.
# 고정된 시드와 짧은 트레이스백으로 빨간 빌드를 재현합니다.
pytest -q --no-header -p randomly --randomly-seed=7 \
tests/test_orders.py::test_settles_invoice \
...
만약 테스트가 재시도에서 통과한다면, 그것은 실패(failure)가 아니라 플래크입니다. 만약 매번 실패한다면, 여기서 멈추세요. 그것은 깨진 테스트이며, 깨진 테스트는 격리되는 것이 아니라 수정되거나 삭제되어야 합니다.
2단계: 4칸 재현 매트릭스 실행하기
당신이 답하려는 질문은 '불안정한가(flaky)' 여부가 아닙니다. 그것은 '무엇에 의존하는지'입니다. 공통적인 원인을 분리하는 네 가지 환경이 있습니다:
- solo — 노드 단독 실행, 오더링 플러그인 비활성화. 테스트의 격리성을 검증합니다.
- order — 여러 무작위 시드(random seeds) 하에서 범위가 지정된 스위트(scoped suite). 순서 의존성(ordering sensitivity)을 테스트합니다.
- isolate — 신선한 프로세스 내의 노드 실행, 따라서 이전 테스트가 상태를 누설할 수 없습니다.
- clock — 고정된 시간대와 멈춘 벽시계. 시간 의존성을 테스트합니다.
#!/usr/bin/env python3
# flake_matrix.py - 격리(quarantine)하기 전에 불안정한 pytest 노드에 속성을 부여합니다.
import json, os, subprocess, sys
...
--forked는 pytest-forked가 필요하며, 클럭 셀은 스위트가 고정 가능한 이음매(freezable seam)를 통해 시간을 읽을 때만 의미가 있습니다. 두 셀 모두 통과하는 척하는 대신 rc: null로 저하됩니다. 그 구분이 핵심입니다. 테스트되지 않은 셀은 녹색 셀이 아닙니다.
단계 3: 분위기가 아닌 판결로서 매트릭트를 읽기
| solo | order | isolate | clock | 귀속(attribution) | 첫 번째 조치(first move) |
|---|
| pass | fail on any seed | pass | pass | 순서 또는 공유 상태 의존성 | 공유 Fixture 재설정; CI에서 해당 모듈을 --forked로 실행 |
| ... | | | | | |
네 번째 행이 팀에게 가장 많은 비용을 초래하는 부분입니다. 순서에 의존하는 테스트와 상태를 누설하는 테스트는 CI 환경에서 동일하게 보일 수 있으며, 이들은 서로 다른 수정책이 필요합니다. 둘 다 flaky라는 주석으로 격리하면 어느 것도 수정되지 않습니다.
단계 4: 원장(ledger)에 대출 조건을 기록하기
기록된 의무가 없는 동결은 기본적으로 영구적입니다. 격리되는 모든 테스트에는 보호했던 불변성(invariant), 현재 이를 보호하는 대체 검사(substitute check), 그리고 이전에 지키는 데 사용했던 파일이 명시된 항목이 추가됩니다.
# quarantine.toml - 만료 시 검토할, 동결된 테스트당 하나의 항목.
[[quarantine]]
test = 'tests/test_orders.py::test_settles_invoice'
...
‘substitute’ 필드는 필수적이며 이슈 링크가 아닙니다. 속성 검사(property check), 불변성 단언(invariant assertion) 또는 더 좁은 결정론적 테스트(deterministic test)가 해당됩니다. 막연히 조사해 보겠다는 약속은 그렇지 않습니다.
단계 5: 시계만 통제하는 것이 아니라, 차이(diff)를 통제하라
만료 날짜는 추가하기 쉽고 무시하기도 쉽습니다. 아래의 게이트는 세 가지 규칙을 강제하며, 세 번째 규칙이 가장 흥미롭습니다. 즉, 패치(patch)가 현재 격리된 테스트(quarantined test)가 보호하고 있는 파일을 수정할 수 없다는 것입니다.
# gate_quarantine.py - unpaid quarantine loan에 대해 CI를 실패시킨다.
import datetime, pathlib, subprocess, sys, tomllib
...
이것을 스위트(suite)가 실행되는 것과 같은 작업에서 실행합니다. 1초 정도 걸립니다. 이 게이트는 패치가 올바른지 판단하려고 시도하지 않습니다. 단지 현재 잠들어 있는 테스트 뒤에 숨는 것을 거부할 뿐입니다.
이 게이트는 의도적으로 엄격합니다. 때로는 가장 적절한 답은 그 자리에서 테스트를 격리 해제하는 것일 수 있으며, 이는 바로 사용자가 강제하고 싶었던 인간의 결정입니다.
무료 등급(free tier)이 진정으로 적합한 곳
이 루프에서 두 단계는 반복적이고 판단력이 필요하지 않아 오프로드하기에 합리적입니다.
첫째, 대체 필드를 초안 작성하는 것입니다. 실패 기록(failure transcript)과 테스트 대상 함수를 모델에 공급하고 수정 사항(fix)을 요청하는 것이 아니라 후보 불변성(candidate invariants)을 요청합니다. 그러면 사람이 수락하거나 거부할 수 있는 속성 목록—덧셈 총합, 단조 증가 시간 스탬프(monotonic timestamps), 반복 가능한 재시도(idempotent retries)—을 얻게 됩니다. 초안 작성은 느린 부분이고, 검토하는 것이 주의를 기울일 가치가 있는 부분입니다.
둘째, 리플레이 루프입니다. 아무도 멈춘 테스트를 여러 시드(seed) 하에 재실행하지 않기 때문에 격리 항목들은 오래되어 버립니다. 각 항목을 리플레이하고 시드와 결과(outcome)를 원장(ledger)에 추가하는 작은 예약 작업이 expires를 누군가 추측한 날짜에서 실행 기록으로 뒷받침되는 결정으로 바꿉니다.
MonkeyCode의 무료 모델 액세스는 첫 번째 작업을 커버하며, 무료 서버 옵션은 두 번째 작업을 커버합니다. 이 두 가지 작업 모두에 대해 비용 요청이 필요하지 않습니다. 공개 고지: 본 문서는 MonkeyCode의 제품 홍보의 일환으로 작성되었습니다. 저는 이것을 위에서 언급한 정확히 두 가지 목적에 사용하며, 모델이 여러분의 fixture가 state를 누설하는지 여부를 알려줄 수 없기 때문에 attribution matrix를 대체할 수는 없습니다.
제한 사항 및 건너뛰어야 할 경우
신뢰하기 전에 매트릭스를 보정하세요. 5개의 시드(seeds)는 40회 실행 중 한 번만 재현되는 플래크(flake)를 놓칠 수 있습니다. 결과표의 다섯 번째 행은 '깨끗함(clean)'이 아니라 '재현되지 않음(unreproduced)'으로 간주해야 합니다. 만약 클럭 셀(clock cell)이 지원하지 않음을 보고한다면, 여러분은 시간 의존성(time dependence)에 대해 아무것도 배우지 못했으므로, 이를 원장 기록(ledger entry)에 명시해야 합니다.
생성된 속성 후보(property candidates)는 자체적인 실패 모드를 가지고 있습니다. 구조적으로 참인 그럴듯한 어설션(assertion)은 아무것도 검증하지 못합니다. 모든 대체품에는 하나의 부정적 확인(negative check)이 필요합니다. 값을 변형시키고, 해당 속성이 빨간색으로 바뀌는지 확인해야 합니다. 검토되지 않은 대체품은 그것을 대체한 동결(freeze)보다 더 나쁜 대출입니다.
다음 중 하나라도 사실이라면 이 전체 접근 방식을 건너뛰세요.
- 스위트가 약 50개의 테스트 미만인 경우. fixture를 직접 수정하세요. 원장 기록은 문제점 자체보다 오버헤드가 큽니다.
- 모든 패치에 대해 실행되는 CI 작업이 없는 경우. 게이트 없이 게이트(quarantine gate)는 단순한 텍스트 파일일 뿐입니다.
- 이미 몇 개가 아닌 여러 테스트가 동결된 경우. 시스템적인 fixture 문제가 있는 것이며, 여기에 프로세스를 추가하는 것은 단지 부패를 문서화할 뿐입니다.
배포 체크리스트
- 현재 빨간색 노드(red node)에서
flake_matrix.py를 실행하고 JSON을 원장 기록에 붙여넣으세요.
- 행동에 대한 문장으로 '의무(obligation)'를 작성하고, 테스트 이름에 관한 것이 아니어야 합니다.
- 오늘 지목할 수 있는 대체품의 이름을 지정하세요. 할 수 없다면, 동결은 준비되지 않은 것입니다.
guards는 테스트가 가져오거나 어설션하는 파일로 설정하세요.
- 기존 테스트 작업에
gate_quarantine.py를 연결하고 빌드를 실패시키세요.
- 만료 시 모든 항목을 검토하고, 새로운 재현 증거(replay evidence)만 해당 항목에 추가하여 확장하세요.
만약 이것을 실제 에이전트-패치 루프(agent-patch loop)에 적용해 본다면, 격리된 테스트 하나로 시작하여 diff 게이트가 경고 전용 모드(warn-only mode)로 일주일 동안 실행되도록 하세요. 그것이 차단했을 패치의 수는 당신이 검증되지 않은 상태로 배포했던 수이며, 그 숫자가 이것을 유지하는 데 필요한 유일한 근거입니다.