Diff-Scoped Mutation Probing: 에이전트 패치를 위한 단언 강도 원장(Assertion-Strength Ledger)
요약
본 글은 에이전트가 생성한 코드 패치에서 발생하는 '단언 강도' 저하 문제를 해결하는 워크플로우를 제시합니다. 핵심 아티팩트는 단언 강도 원장(assertion-strength ledger)으로, 테스트 파일의 두 버전을 비교하여 단언 변화량과 거친 강도 변화량을 측정합니다. 이 도구는 패치가 단순히 코드를 변경한 것을 넘어, 실제로 검증 로직을 약화시키지 않았는지 정밀하게 분석하는 데 도움을 줍니다.
핵심 포인트
- 단언 강도 원장(assertion-strength ledger)을 생성하여 코드 패치의 검증력 변화를 측정합니다.
- 테스트 파일의 두 버전 비교를 통해 단언 카운트 및 거친 강도 변화량을 보고합니다.
- 패치가 테스트 코드를 유지하되, 핵심적인 검증 로직만 제거하는 경우를 탐지할 수 있습니다.
- 뮤테이션 프로빙을 수행하기 전, 기준선(baseline) 테스트의 안정성을 확보하는 것이 중요합니다.
Diff-Scoped Mutation Probing: 에이전트 패치를 위한 단언 강도 원장
패치는 모든 테스트 파일을 유지하고, 모든 테스트 함수 이름도 유지하면서도, 그 테스트를 실행할 가치가 있게 만들었던 검증(check)을 삭제할 수 있습니다. 저는 에이전트가 생성한 패치에서 이런 형태를 계속 목격합니다: 테스트 스위트는 두 개의 테스트로 증가하고, diff는 40줄로 증가하지만, assert x == 104.50 하나가 assert x is not None으로 바뀝니다. 스위트 크기 게이트(Suite-size gates)는 앞의 두 숫자는 계산합니다. 하지만 세 번째 것은 아무것도 계산하지 않습니다.
이 글은 그것을 계산하는 워크플로우입니다. 이 워크플로우는 하나의 아티팩트, 즉 단언 강도 원장(assertion-strength ledger)과 각 단언 뮤턴트(per-assertion mutants)가 죽었는지 또는 살아남았는지에 대한 증거 클래스를 생성합니다. 만약 뮤턴트가 살아남는다면, 그것이 겨냥했던 단언은 장식적인 것이 됩니다.
커버리지와 제약 사이의 간극
커버리지는
이 원장(ledger)은 동일한 테스트 파일의 두 개 버전을 비교하고 세 가지 숫자를 보고합니다: 단언(assertion) 카운트 변화량(delta), 거친 강도 변화량(coarse strength delta), 그리고 새로 도입된 탈출구(escape hatches) 여부입니다. 이것은 증명이 아니라 분류 점수이며, 스크래치 작업 공간에서 실행할 수 있도록 의도적으로 stdlib 전용으로 만들어졌습니다.
#!/usr/bin/env python3
"""assertion_ledger.py - 두 개 버전 간의 단언 강도 비교.
...
main이 아닌 부모 트리(parent tree)에 대해 실행해야 합니다. 패치의 부모만이 에이전트가 수정하기 전 테스트가 무엇을 단언했는지 아는 유일한 버전입니다.
git worktree add ../parent-tree "$(git rev-parse HEAD~1)"
python assertion_ledger.py ../parent-tree/tests/test_billing.py tests/test_billing.py
strength_delta가 음수이고 assert_count_delta가 0인 경우는 테스트를 유지하고 제약 조건만 제거한 패치의 정확한 시그니처입니다. git worktree의 동작 및 정리(cleanup)는 git-scm.com에서 문서화되어 있습니다.
2단계: 지루할 정도로 평범한 기준선 설정하기
뮤테이션 프로빙(Mutation probing)은 두 가지 결과를 비교합니다: 변형된 실행은 실패해야 합니다. 만약 기준선 테스트가 불안정하다면(flaky), 성공하는 변형된 실행은 아무것도 증명하지 못합니다. 왜냐하면 그 불안정성 자체가 두 결과 모두 가능하게 만들기 때문입니다. 먼저 격리하고, 나중에 프로빙을 수행하세요.
# 모든 프로브 증거가 계산되기 전에 N/N 녹색(green) 상태를 요구합니다.
for i in $(seq 1 10); do python -m pytest -q tests/test_billing.py::test_refund_partial; done
결과를 보기 전에 임계값(threshold)을 정하고 PR에 기록하세요. 변경된 테스트 ID에서 연속으로 열 번의 녹색 상태는 빠른 단위 테스트 스위트(unit suite)에 합리적인 하한선입니다. 비용이 많이 드는 통합 테스트(integration tests)는 완전히 다른 계약(contract)이 필요하며, 이는 아래 제한 사항에서 다루어집니다.
프로브는 테스트 대상 코드에 대한 구문 편집(syntactic edit)이며, 그 뒤에 한 번의 테스트 실행이 이어집니다. 만약 테스트가 여전히 통과한다면, 해당 뮤턴트(mutant)는 생존했으며 어설션(assertion)이 동작을 제약하지 못했다는 의미입니다. 아래 참조 하네스(reference harness)는 git checkout으로 상태를 복원하므로 클린 트리(clean tree)가 필요합니다.
# probe.sh - 참조 하네스, 라이브러리가 아닙니다. 뮤턴트 하나, 테스트 하나, 판결 하나.
SRC="src/billing.py"
...
실제 환경에서 어설션이 약화되는 네 가지 방식에 대응하기 위해 다음 순서로 네 가지 뮤테이션 연산자(mutation operators)를 사용합니다.
- 값 이동 (Value shift) — 예상 상수 값을 한 단위 변경합니다. 범위 체크나 진리성 검사(truthiness check)로 완화된 어설션을 포착합니다.
- 연산자 플립 (Operator flip) —
==를>=로, 또는>를>=로 바꿉니다. 경계 조건(boundary assertion)을 잃은 경우를 포착합니다. - 반환 스왑 (Return swap) — 패치 후 값이 아닌 패치 전 값을 반환합니다. 구성상 통과하도록 만들어진 어설션을 포착합니다.
- 분기 삭제 (Branch deletion) — 새로 추가된 분기를 삭제합니다. 수정 사항이 행복한 경로(happy path)만을 유일한 경로로 만들었을 때, 그 행복한 경로에만 의존하는 테스트를 포착합니다.
4단계: 레저와 살상률을 함께 읽기
이 두 숫자는 유용한 방식으로 불일치할 수 있습니다. 낮은 살상률과 높은 양의 strength_delta는 패치가 정밀해 보이지만 아무도 확인하지 않는 상태를 목표로 하는 어설션을 추가했음을 의미합니다. 델타가 0이고 살상률이 높다는 것은 어설션이 변경되지 않았으며 여전히 부하를 지탱하고 있다는 뜻입니다. 이는 가장 저렴하게 달성 가능한 좋은 결과입니다.
| assert_count_delta | strength_delta | mutants killed | verdict |
|---|---|---|---|
| 0 | ">= 0" | ">= 80%" | 테스트 증거를 현 상태로 수용 |
| ... | |||
이 마지막 행은 판단적 결정(judgment call)이 아닙니다. 패치 내부에 새로 도입된 skip, xfail, 또는 빈 except 블록은 해당 패치가 수정한다고 주장하는 조건 하에서 프로브가 실행되지 않았음을 의미합니다. |
무료 모델과 무료 서버가 여기에 어떻게 들어맞는가
3단계와 4단계는 기계적인 과정이며, 변경된 단언(assertions)의 수에 따라 확장되므로, 퇴근 시간인 오후 6시에 사람이 손으로 작성하고 싶지 않은 바로 그 작업입니다. MonkeyCode의 무료 모델 접근 및 무료 서버 옵션은 제가 이 단계에서 의존하는 두 가지 요소입니다. 모델이 변경된 각 단언별로 후보 돌연변이(candidate mutants)를 제안하고, 프로브 매트릭스는 제 노트북과 분리된 워크스페이스에서 실행되므로 기준선(baseline)이 재현 가능합니다. 공개 고지: 이 글은 MonkeyCode의 제품 홍보 활동의 일환으로 작성되었습니다.
명확히 밝혀야 할 두 가지 한계점이 있습니다. 이 설정은 프로브를 생성하고 실행할 뿐, 판결을 내리지는 않습니다. 4단계의 표가 원시적인 제거(raw kill counts) 카운트를 기반으로 합니다. 그리고 무료 등급 제한은 변경될 수 있으므로, 이를 중심으로 배치 작업을 설계하기 전에 현재 약관을 확인하십시오. 그 이유로 저는 여기서 할당량(quotas)을 인용하지 않습니다.
한계점 및 사용해서는 안 되는 경우
돌연변이 프로빙(Mutation probing)은 diff-scoped(변경 범위 기반)이기 때문에, 아무도 건드리지 않은 동작에 대해서는 아무것도 말해주지 못합니다. 이는 패치 검토에는 장점이지만 아키텍처 검토에는 사각지대입니다.
구문적 돌연변이(Syntactic mutants)가 의미론적 돌연변이(Semantic mutants)를 대체하지는 않습니다. 상수를 변경하는 것은 느슨해진 단언을 포착할 수는 있지만, 적절한 정밀도에서 단순히 잘못된 속성을 단언하는 단언은 포착하지 못합니다.
다음 중 하나라도 해당된다면 이 워크플로우를 사용하지 마십시오.
- 변경된 테스트가 안정적인 녹색 기준선(green baseline)을 가지고 있지 않으며, 먼저 격리할 수 없는 경우. 프로브 결과는 노이즈일 것입니다.
- 패치가 수정된 동작보다는 주로 새 파일로 구성되어 있는 경우. diff 할 부모 리비전(parent revision)이 없으므로, 레저가 점수를 매길 것이 없습니다.
- 단언이 이를 실행하는 테스트와 멀리 떨어져 공유되는 피처(shared fixtures)나 테이블 기반 데이터에 존재하는 경우. 레저는 단언이 아닌 피처에 점수를 매길 것입니다.
- 테스트 스위트가 통합(integration)-중심이며 실행당 비용이 분 단위인 경우. 변경된 각 단언별 돌연변이는 그 가격대에서 감당할 수 없습니다. 대신 변경된 동작별로 하나의 돌연변이를 사용하십시오.
이 워크플로우는 패치가 작고, 단언(assertions)들이 국소적이며, 리뷰어의 실제 질문이 '이 테스트 스위트가 수정되기 전에 실패했을까?'일 때 가치가 있습니다. 그 질문에는 답이 있습니다. 다만 그것이 커버리지 리포트가 제공하는 답은 아닙니다.
다음 에이전트 패치에 이 원장(ledger)을 실행한 후, 세 가지 숫자—카운트 델타(count delta), 강도 델타(strength delta), 그리고 킬 레이트(kill rate)—를 게시해 주세요. 그중 가운데 숫자가 아무도 공개하지 않는 수치입니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기