깨끗한 러너가 동일한 클래스를 재현할 때만 플레이크를 고정해야 하는 이유
요약
이 문서는 테스트의 신뢰성 확보와 '플레이크(flaky)'한 실패 처리 기준을 제시합니다. 단순히 로컬에서 빨간색 막대가 뜨는 것만으로는 충분하지 않으며, 깨끗한 환경에서 동일한 조건으로 재현된 예외여야 진정한 버그로 간주해야 합니다. 특히, 테스트의 결정론적 특성을 보장하기 위해 러너 식별자(Runner Identifier)를 이미지 다이제스트와 작업 공간 논스 등으로 엄격하게 관리할 것을 강조합니다.
핵심 포인트
- 플레이크 고정은 통과 기록이 아닌, 재현된 예외여야 합니다.
- 단일 로그 라인이나 로컬 실패만으로는 신뢰할 수 없습니다.
- 테스트의 결정론적 특성을 위해 러너 식별자는 이미지 다이제스트와 논스 기반이어야 합니다.
플레이크(flake) 고정은 통과 기록이 아닌, 기록된 예외입니다. 에이전트 패치는 깨끗한 작업 공간에서 시작된 두 번째 러너가 동일한 실패 클래스(failure class)를 동일한 피처 해시(fixture hash)와 최소화된 입력(minimized input)으로 재현했을 때만 해당 예외를 받을 수 있습니다. 단지 로컬의 빨간색 막대 하나로는 충분하지 않습니다. 합의가 증거이며, 불일치는 고정 경로를 닫습니다.
에이전트 차이점(Agent diffs)은 단일 로그 라인으로는 무너지는 방식으로 실패합니다. 해당 속성(property)은 거짓일 수 있습니다. 피처는 드리프트(drift)했을 수 있습니다. 테스트가 플레키(flaky)할 수도 있고, 패치를 작성한 머신 자체가 오염되었을 수도 있습니다. 이러한 사례들은 각기 다른 처리 방식을 필요로 합니다. 모든 빨간색 실패를 고정 후보로 간주하는 병합 게이트는 결정론적 버그를 숨길 것이며, 반대로 모든 빨간색 실패를 하드 블로커(hard blocker)로 취급하는 게이트는 패치가 전혀 건드리지 않은 알려진 플레이크에서 멈출 것입니다.
의사결정 매트릭스 (Decision matrix)
아래 매트릭스는 측정된 벤치마크가 아닌 정책입니다. 행은 패킷이 기록하는 필드를 포괄합니다. 여기에 타이밍 데이터나 통과율 주장은 없습니다.
| 로컬 클래스 | 클린 러너 클래스 | 속성 코호트 (Property cohort) | 고정 경로 (Freeze lane) |
|---|---|---|---|
| flake-shaped | same class, same fixture hash | pass | eligible |
| ... | |||
| 읽은 마지막 두 행을 하드 스톱(hard stops)으로 간주하십시오. 누락된 클린 실행은 면제 사유가 아닙니다. 실패한 속성 코호트는 양쪽 러너 모두 유사한 스택을 출력하더라도 플레이크가 아닙니다. |
러너 식별자가 필드인 이유
작성 작업 공간(authoring workspace)은 편향된 샘플입니다. 여기에는 에디터 교체 파일, 따뜻한 종속성 캐시(warm dependency cache), 부분적인 환경 파일, 그리고 에이전트가 반복하는 동안 작성한 모든 것이 포함됩니다. 해당 머신에서 재실행하면 종종 그 편향을 반복합니다. 두 번째 로컬 재실행 확인은 하나의 환경을 이중으로 계산하는 것입니다.
러너 식별자는 감사(audit)하기에 충분히 거칠하고, 재사용을 감지하기에는 충분히 세밀해야 합니다. 유용한 ID는 단순히 호스트 이름만 사용하는 것이 아니라, 이미지 다이제스트(image digest)와 작업 시작 시 생성된 작업 공간 논스(workspace nonce)여야 합니다. 호스트 이름은 자동 확장되는 풀에서 충돌합니다. 작업 간에 지속되는 논스 역시 이 필드의 목적을 놓칩니다.
만약 두 ID가 일치한다면, 투표는 거부(refuse)입니다. 깨끗한 러너(clean runner) 역시 실제로 로드했던 피처 해시(fixture hash)를 에코(echo)해야 합니다. 이러한 에코 없이 재실행되었다고 기록하는 로그는 불완전한 실행이며, 불완전한 실행은 사용 불가능한 행(row)을 가져갑니다.
논문이 아닌 전제 조건
이전에 설정된 게이트들은 이미 적격성을 제한합니다: 실패한 입력(input)부터 축소하고(shrink), 피처 해시를 고정하며(pin the fixture hash), 더 느슨한 경계는 거부하고, 속성 확인(property check)이 녹색으로 표시되었다고 해서 새로운 동결 토큰(freeze token)을 발행해서는 안 됩니다. 이 문서는 그러한 규칙들을 다시 여는 것이 아닙니다. 이는 그들이 표현하지 못하는 제약을 추가합니다. 로컬에서 완벽하게 작동하는 패킷이라도 작성 환경(authoring machine)의 산물일 수 있습니다.
이전 확인 사항들을 이 투표에 대한 입력으로 취급하십시오. 만약 축소(shrink)가 실행되지 않았다면, local_class를 flake_shaped로 설정하지 마십시오. 클래스를 비워두고 검사기(checker)가 거부하도록 하십시오.
번호가 매겨진 워크플로우
-
재실행하기 전에 ID들을 고정하십시오.
patch_hash,test_id,fixture_hash,minimized_input_hash, 그리고oracle_version을 기록합니다. 어떤 필드라도 비어 있다면 중단하십시오. 이러한 키들 없이 동결하는 것은 다음 패치에서 감사(audit)될 수 없습니다. -
로컬 실패를 닫힌 집합으로 분류하십시오:
flake_shaped,deterministic, 또는environment. 단, 별도의 포기 기록(waiver record)이 이미 존재하지 않는 한, 어설션-텍스트 편집과 경계 편집을deterministic에 매핑하십시오. 녹색 투표를 강제하기 위해 러너 스크립트 내부에 네 번째 클래스를 추가하지 마십시오. -
의심되는 플레이크(flake)가 소유하지 않은 입력들로 속성 코호트(property cohort)를 실행하십시오. 이 코호트는 후보 패치에서 통과해야 합니다. 여기서 통과했다고 해서 동결을 승인하는 것은 아닙니다. 단지 선택한 관련 없는 불변 집합(unrelated invariant set)을 깨뜨리지 않았다는 것만을 보여줄 뿐입니다.
-
패치를 작성하지 않았고, 로컬 작업 공간(local workspace), 종속성 캐시(dependency cache), 또는 에디터 스크래치 파일(editor scratch files)을 재사용하지 않은 깨끗한 러너를 시작하십시오. 축소된 입력만을 고정된 피처 해시에 대해 재실행하십시오. 원격 클래스, 원격 피처 해시, 원격 입력 해시, 그리고 원격 러너 ID를 저장하십시오.
-
비교하고, 그 다음 영구적으로 저장(persist)하십시오.
적격성(Eligibility)은 동일한 실패 클래스(failure class), 동일한 고정값 해시(fixture hash), 동일한 최소화된 입력 해시(minimized-input hash), 통과하는 속성 코호트(passing property cohort), 완료된 클린 실행(completed clean run), 그리고 두 개의 서로 다른 러너 ID를 요구합니다. 먼저 packet.json을 작성하세요. 두 번째 명령은 이 패킷을 소비하여 eligible 또는 거부 이유를 출력합니다. 그 결정을 YAML 조건문 안에 포함시키지 마세요. 아무도 검토하지 않을 것입니다.
oracle_version또는patch_hash가 변경될 때 패킷을 만료시키세요. 새로운 패치는 어제 유효했던 동결(freeze)과 테스트 이름이 일치하더라도 새로운 투표입니다.
속성 코호트와 클린 재실행은 병렬로 실행될 수 있습니다. 투표는 둘 다를 기다립니다. 빠른 속성 통과가 누락된 클린 실행을 단축시키지 않도록 해야 합니다.
러너 ID는 작업(job)에서 생성하고, 수동으로 하지 마세요:
nonce=$(openssl rand -hex 8)
image=$(docker image inspect "$CI_IMAGE" --format '{{.Id}}' 2>/dev/null || echo "local-uncontainerized")
printf 'img:%s+nonce:%s\n' "$image" "$nonce" | tee runner.id
...
local-uncontainerized 폴백은 통과가 아니라 정직한(honest) 상태입니다. 두 작업 모두 이 폴백을 출력하더라도 여전히 서로 다른 nonce를 필요로 합니다. 이미지를 이름 지을 수 없다면, 패킷에 그렇게 명시하고 거부 경로를 쉽게 만들도록 하세요.
참조 검사기 (Reference checker)
아래 모듈은 제안입니다. 이 초안은 프로덕션 스위트(production suite)에 대한 실행 보고서를 포함하지 않습니다. 이 함수는 투표를 인코딩합니다. 플레이크(flake)를 발견하거나, 입력을 축소시키거나, 네트워크와 통신하지 않습니다.
from dataclasses import dataclass
ELIGIBLE = "eligible"
...
만약 해당 모듈이 작성된 대로 가져와진다면 예상 결과는 주장되는 CI 이력이 아니라 단언(assertions)입니다:
def test_vote_matrix():
base = dict(
test_id="prop_balance_holds",
...
합성 패킷은 디스크 상의 형태를 보여줍니다. 해시는 사고 보고서가 아니라 플레이스홀더입니다.
{
"test_id": "prop_balance_holds",
"patch_hash": "c0ffee",
...
해시 파일들이 존재하는 후에만 로드하세요:
python - <<'PY'
import json, pathlib
from freeze_vote import FreezeVote, decide
...
제안된 프로세스 종료 코드는 decide를 CLI로 래핑할 때 다음과 같습니다: 결과가 eligible일 때는 0, 필수 필드가 비어있을 때는 2, 클래스나 해시가 불일치할 때는 3, 클린 실행이 완료되지 않았을 때는 4입니다. 이 매핑은 하나의 모듈에 유지해야 합니다. CI는 테이블을 재구현하는 것이 아니라 해당 모듈을 호출해야 합니다.
초안(draft)과 클린 작업 공간의 역할
속성 명세(Property statements)는 여전히 작성되어야 하며, 모델 초안은 최종 판결이 아닙니다. 고지: 이 문서는 MonkeyCode의 제품 홍보의 일환으로 준비되었습니다. MonkeyCode의 무료 모델 액세스는 초안 작성 보조 도구로 관련성이 높습니다: 패치 diff에서 후보 불변량(candidate invariants)을 요청한 다음, 실행 가능하지 않거나 테스트 대상 주장을 재진술하는 모든 초안은 폐기하십시오. decide는 절대 신뢰도 점수(confidence score)를 읽지 않습니다. 이는 해시, 클래스, 그리고 코호트 러너가 계산한 부울 값만을 읽습니다.
무료 서버 옵션은 clean_runner_id를 얻는 한 가지 방법으로 관련성이 높습니다. 이를 최소화된 케이스와, 정책이 허용한다면 속성 코호트(property cohort)를 위한 격리된 리플레이 작업 공간으로 사용하십시오. 원격 피처(fixture) 해시를 영구 저장하거나 투표를 거부하십시오. 해당 해시 없이 성공이라는 원격 상태는 여전히 사용할 수 없는 행입니다. 여기에는 할당량, 하드웨어 사양, 시간 제한 또는 옵션이 무료로 유지된다는 약속은 아무것도 명시되어 있지 않습니다.
packet.json을 병합(merge)을 소유한 저장소에 보관하십시오. 원격 로그는 투표의 입력일 뿐, 기록 그 자체는 아닙니다.
만약 네트워크 내부에 이미 두 번째 격리된 작업 공간이 존재한다면, clean_runner_id를 해당 작업 공간으로 지정하고 다른 곳에는 피처를 보내지 마십시오. 투표는 누가 두 번째 기계를 운영하는지에 달려 있는 것이 아니라, 고유한 러너 식별자와 일치하는 해시에 달려 있습니다.
한계점(Limitations)
두 개의 러너가 버그를 공유할 수 있습니다. 동일한 기본 이미지, 동일한 클럭 스큐(clock-skew) 창, 또는 동일한 외부 스텁은 잘못된 플레이크(false flake)를 두 번 재현할 것입니다. 이중 합의는 기준을 높입니다. 이는 실패가 프로덕션에서 비결정적(nondeterministic)임을 증명하지 않으며, 플레이크율(flake rate)을 추정하지도 않습니다.
재현되지 않는 경우(Non-reproduction)는 모호합니다. 깨끗한 실행(clean pass)이란 증명되지 않은 것이지, 로컬 실패가 상상에 불과했다는 의미는 아닙니다. clean runner가 실패할 때까지 재시도하는 것은 적격성(eligibility)을 만들어냅니다. clean 시도는 최소화된 입력(minimized input)의 한 번 재생으로 제한하십시오. 두 번째 시도를 하려면 새로운 패킷과, 현재 decide가 의도적으로 무시하는 작성 이유 필드(written reason field)가 필요하므로, 사람이 스키마를 인지적으로 확장해야 합니다.
시드된 테스트만 사용하십시오. 이 정책은 최소화된 입력과 고정 값 해시(fixture hash)가 케이스를 완전히 결정한다고 가정합니다. UI 타이밍 테스트, 실시간 네트워크 호출, 순서가 정해지지 않은 스레드 덤프는 그 가정을 충족하지 못합니다. 테이블을 완성하기 위해 이들을 flake_shaped로 강제 변환하지 마십시오.
참조 검사기(reference checker)는 pytest, JUnit 또는 공급업체 CI 로그를 파싱하지 않습니다. 어댑터 코드는 별도의 작업입니다. 만약 누군가 나중에 해시 검사를 삭제할 경우에만 갭을 숨기는 방식으로 clean_fixture_hash를 제거하고 여전히 clean_run_completed를 true로 표시하는 어댑터는 위험합니다. 파이프라인을 녹색으로 만들기 위해 이 필드를 삭제하지 마십시오.
공유 이미지 다이제스트(Shared image digests)는 잔여 위험일 뿐, 논스(nonce)를 건너뛸 이유가 아닙니다. 둘 다 기록하십시오. 만약 CI와 동일한 락파일(lockfile)로부터 clean 작업 공간을 재구축할 수 없다면, 해당 실행은 깨끗하지 않으므로 ID가 다르더라도 투표는 거부되어야 합니다.
사용해서는 안 되는 경우
케이스가 사설 네트워크를 벗어날 수 없고 내부 clean runner가 존재하지 않는 경우에는 이 접근 방식을 건너뛰십시오. 공유 또는 공개 작업 공간은 비밀 정보, 고객 고정 값(customer fixtures), 또는 취약점 관련 입력값에 적합한 장소가 아닙니다. 로그를 임시로 수정하고 클래스 문자열이 살아남기를 바라기보다는 실패 시 닫히는 방식(Fail closed)을 사용하십시오.
최소화된 입력이 없는 경우에도 건너뛰십시오. 두 번째 기계에서 긴 전체 고정 값 작업을 재생하는 것은 비용을 복사하고 실패한 바이트를 숨깁니다. 기존 규칙 내에서 축소시키거나, 아예 동결(freeze)하지 마십시오.
동결이 이 빨간색 상태가 영원히 받아들여질 것이라는 의미로 예상되는 경우에도 건너뛰십시오. 패킷은 패치 해시와 오라클 버전과 함께 만료됩니다. 상시 면제(standing waiver)가 필요한 팀은 기록상 인간 소유자(human owner)와 검토 날짜가 있는 다른 프로세스가 필요합니다. 이 검사기는 그러한 소유자 필드를 대신 만들어주지 않습니다.
병합할 내용
속성 코호트(property cohort)가 통과했고 프리즈 레인(freeze lane)이 사용되지 않았거나 eligible한 경우에만 병합해야 합니다. 클린 러너(clean runner)가 확인하지 않은 로컬 빨간색(local red)에서는 병합하지 마십시오. 시간을 절약하기 위해 클린 러너가 건너뛴 경우에는 병합하지 마십시오.
패킷은 웨이버(waiver) 기간 동안 테스트 옆에 보관하십시오. 다음 에이전트 패치(agent patch)는 이전 프리즈와 이름이 일치하더라도 자체 증명(attestation)을 생성할 때까지 이 투표를 실패시켜야 합니다. 이것이 바로 예외를 테스트 제목만 아니라 patch_hash에 바인딩하는 목적입니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기