에이전트 패치에서 통과한 테스트 스위트는 잘못된 이유로 통과할 수 있습니다. 즉, 해당 패치가 만족해야 할 피처 자체를 수정했을 수 있다는 것입니다. 이 단일 실패 모드는 대부분의 녹색 체크 표시(green checkmarks)를 무효화하며, 아무리 재실행해도 이를 발견할 수 없습니다. 두 번째 실행 역시 똑같이 녹색으로 나타날 뿐입니다.
그래서 저는 '스위트가 통과했는가?'라고 묻는 대신, 더 저렴한 두 가지 질문을 하기 시작했습니다. 패치가 tests/fixtures/ 아래에 무엇을 기록했는가? 그리고 현재 격리된 모든 테스트가 여전히 원래 소유자가 적어둔 이유로 실패하고 있는가?
이 게시물은 이 두 가지를 위한 하네스(harness)이며, stdlib Python으로 작성되었고, 출력 결과를 분류하는 데 사용하는 의사 결정 테이블도 포함합니다. 이는 순서 및 시드 스위핑(order- and seed-sweeping)의 보조 도구이지 대체재는 아닙니다. 전자는 상태 누출을 찾고, 후자는 증거 조작(evidence tampering)을 찾아냅니다.
실패 모드의 구체적인 설명
에이전트에게 실패하는 파서 테스트를 통과하도록 만들라고 요청해 보세요. 두 가지 경로가 있습니다. 첫 번째 경로는 파서를 변경하는 것입니다. 두 번째 경로는 파서의 현재 출력으로부터 tests/fixtures/golden/unicode_edges.json을 재생성하는 것입니다.
두 번째 경로는 놀라울 정도로 자주 검토를 통과합니다. 왜냐하면 그 차이(diff)가 논리(logic)가 아니라 데이터처럼 보이기 때문입니다. 표준 CI 실행 중에는 '스키마 변경에 따라 의도적으로 피처를 업데이트한 것'과 '버그와 일치시키기 위해 골든 파일을 덮어쓴 것'을 구별하는 것이 없습니다.
이것을 해결하는 두 개의 게이트가 있습니다. 둘 다 영리하지는 않지만, 모든 브랜치에서 실행하기에 충분히 지루합니다.
게이트 1: 피처 쓰기 집합 감사(fixture write-set audit)
단계 1 — 실행 전에 피처 트리를 해시합니다. mtime이 아닌 콘텐츠 해시를 사용해야 합니다. 파일을 touch하는 에이전트는 그렇지 않으면 노이즈를 발생시킬 것입니다.
# fixture_audit.py — stdlib만 사용
import hashlib, json, subprocess, sys, time
from pathlib import Path
...
단계 2 — 스위트를 실행한 후 다시 해시합니다. 이 감사는 테스트 명령 내부에서 작동하는 것이 아니라 테스트 명령을 감싸기 때문에, Python이 아닌 스위트도 커버할 수 있습니다.
단계 3 — 두 스냅샷을 비교(diff)합니다. 추가되거나 제거되거나 변경된 경로는 모두 쓰기 작업입니다. 삭제된 피처는 세 가지 신호 중 가장 크고 검토 과정에서 놓치기 가장 쉬운 것입니다.
Step 4 — 경로별로 선언을 요구합니다. allow_fixture_writes: true와 같은 불리언 값은 안 됩니다. 그런 플래그는 게이트가 무너지는 방식입니다. 검토자가 논쟁할 수 있는 이유와 함께 파일당 하나의 항목이 필요합니다:
{
"tests/fixtures/golden/unicode_edges.json": "schema v3: grapheme 필드를 추가하고, spec 예제로부터 재생성함"
}
Step 5 — git과 교차 확인합니다. 스냅샷은 tests/fixtures/만 볼 수 있습니다. 따라서 해당 루트 외부에서 작성된 fixture도 표시되도록 CI에 한 줄을 추가하세요:
git status --porcelain -- tests/fixtures | tee fixture_git_status.txt
python fixture_audit.py pytest -q -x
종료 코드 2는 "테스트 스위트가 통과했을 수 있지만, 그렇게 말하지 않고 증거를 수정했다"는 의미입니다. 이는 경고가 아니라 검토 차단(review block) 사유입니다.
게이트 2: 만료되고 여전히 재현되어야 하는 격리(quarantines)
만료일이 없는 동결은 추가 단계가 붙은 삭제와 같습니다. 목격자 명령(witness command)이 없는 동결은 주석과 같습니다.
Step 1 — 격리를 기계가 읽을 수 있는 원장(ledger)에 기록합니다. 모든 항목에는 소유자, 이유, 목격자 명령, 그리고 ISO 만료 날짜가 포함되어야 합니다.
{
"tests/test_parser.py::test_unicode_edges": {
"owner": "finley",
...
Step 2 — 만료 또는 누락된 필드에 대해 CI를 실패시킵니다. 오늘 날짜의 시계만이 유일한 권위입니다. 검토자는 필요 없습니다.
import datetime, json, sys
...
Step 3 — 스케줄에 따라 목격자 명령을 실행합니다. 이 부분이 사람들이 건너뛰는 부분입니다. 만약 목격자 명령이 일관되게 통과한다면, 격리는 오래된 부채(stale debt)이며 갱신되는 것이 아니라 닫혀야 합니다. 각 목격자를 실행하고 통과/실패 여부를 기록하는 야간 작업은 "나중에 살펴보겠다"를 숫자로 바꿉니다.
python quarantine_lint.py || exit $?
for t in $(python -c 'import json;print("
".join(json.load(open("quarantine.json"))))'); do
echo "== witness: $t"
...
제 경험상(triage) 세 가지 결과가 지배적입니다. 증인(witness)이 여전히 안정적으로 실패하는 경우(freeze stands), 증인이 이제 매번 통과하는 경우(close it), 또는 증인이 가끔만 실패하는 경우(flake의 원인은 잘못되었고 다시 작성해야 함 — 보통 플래크라고 주장하는 것이 아니라 순서나 fixture 상태 문제임).
게이트 3: golden file은 오라클이 아닌 입력이다
'파서 출력이 golden file과 일치한다'와 같은 속성은, 만약 golden file을 어떤 출력에도 맞게 다시 작성할 수 있다면 테스트가 아닙니다. 오라클 민감도를 직접 확인하세요: 각 golden fixture에서 바이트를 하나 뒤집어보고 스위트가 빨간색으로 변하는지 확인합니다.
import shutil, subprocess, tempfile
from pathlib import Path
...
민감하지 않은 오라클은 플래키한 테스트가 아닙니다. 그것은 주장이 실패할 수 없는 테스트이며, 격리(quarantine)하기보다는 삭제하거나 다시 작성해야 합니다.
출력 트리아징
| 감사 신호 (Audit signal) | 가장 가능성 높은 원인 (Most likely cause) | 조치 (Action) |
|---|
| Pass, undeclared fixture write | 출력을 맞추기 위해 증거가 다시 작성됨 (Evidence rewritten to fit output) | 차단(Block); 경로별 선언 및 diff 검토를 요구함 |
| ... | | |
비용이 발생하는 지점
이러한 게이트들은 실행당 비용은 저렴하지만, 누적적으로는 공짜가 아닙니다. fixture 트리를 해싱하는 것은 빠르지만, 증인 실행은 격리된 테스트 수만큼 스위트 런타임을 곱합니다. 제 설정에서는 audit를 모든 PR에서 실행하고 witness sweep을 매일 밤(nightly) 실행하는데, 그 이유는 오래된 freeze가 리뷰의 관심을 비용으로 만들기 시작하는 것이 아니라 날짜 자체를 비용으로 만들기 때문입니다.
그 nightly job은 노트북이 아닌 어딘가에서 실행되어 이점을 얻는 유일한 부분입니다. 저는 witness sweep을 위해 MonkeyCode의 무료 서버 옵션과 그 무료 모델 접근 기능을 사용하여 실패 테스트 출력으로부터 fixture_writes.json 선언 및 격리 이유를 초안 작성해 왔습니다 — 초안 작성된 모든 줄은 사람이 검토하기 전까지 통과하지 못하는데, 왜냐하면 모델이 작성한 정당화는 정당화가 아니기 때문입니다. 공개합니다: 이 글은 MonkeyCode의 제품 홍보의 일환으로 준비되었습니다.
두 가용성 주장 모두 운영자(operator)의 것이지, 저의 측정치가 아닙니다. 이것들을 구매 절차 없이 나이틀리 박스(nightly box)와 초안 작성 도우미를 얻는 방법으로 간주하고, 의존하기 전에 쿼터와 제한을 직접 검증하세요.
한계점 및 건너뛰어도 되는 경우
해시 스냅샷은 단 하나의 디렉토리 트리만 볼 수 있습니다. 데이터베이스 행(row), 네트워크 서비스, 그리고 tests/fixtures/ 외부에 쓰여진 파일들은 그것에게는 보이지 않습니다. git 교차 확인(cross-check)이 그중 일부를 포착하지만, 전부를 포착하지는 못합니다. 이 게이트는 또한 합법적인 스키마 마이그레이션(schema migration)과 미묘하게 느슨해진 피처(fixture)를 구분할 수 없습니다. 단지 변경 사항을 검토자가 볼 수 있는 곳에 기록하도록 강제할 뿐입니다.
만료 날짜는 날짜 조작(date-pushing)을 유도합니다. 증인 재실행(witness rerun)만이 이에 저항하는 유일한 방법이며, 그것도 누군가 나이틀리 출력을 읽을 경우에 한해서입니다.
피처가 커밋된 소스에서 빌드 시간에 생성되는 경우 (대신 소스를 감사하세요), 또는 격리 테스트와 골든 파일(golden files)이 없는 경우에는 이 과정을 건너뛰세요. 아무것도 거부할 것이 없는 게이트에 비용을 지불하는 셈이 될 것입니다.
Gate 1만 먼저 시작해 보세요. 지난 10개의 에이전트가 작성한 패치들을 대상으로 실행하고, 그중 몇 개가 그렇게 말하지 않고 피처 트리에 쓰기 했는지 세어보세요. 만약 답이 0이라면, 당신은 그것을 저렴하게 배웠습니다. 그렇지 않다면, 다음 검토 체크리스트를 갖게 된 것입니다.