버그 재현 스크립트는 종종 “아는 사람만 운전할 수 있는 차”처럼 생겼다.
시동 거는 법은 README 어딘가에 있고, 브레이크가 어디 있는지는 만든 사람만 알고, 어떤 버튼이 실제 DB를 바꾸는지는 실행해봐야 안다. 운전자는 늘 긴장한다.
“이거 실행하면 뭐가 바뀌지?”
“verify만 하는 줄 알았는데 Slack 메시지가 나가면 어떡하지?”
“cleanup이 정말 cleanup만 할까?”
이번에 QueryPie ReproKit을 정리하면서 목표를 하나로 잡았다.
재현 스크립트에 운전면허를 발급하자.
누가 타도 최소한 계기판은 같은 위치에 있고, 출발 전 점검 방법도 같고, 위험한 페달에는 빨간 스티커가 붙어 있게 만들자는 뜻이다.
여섯 단어짜리 운전 규칙
ReproKit의 핵심은 의외로 작다.
모든 재현 시나리오를 여섯 단계로 맞춘다.
doctor -> setup -> reproduce -> verify -> report -> cleanup
처음 보면 너무 평범하다. 그런데 이 평범함이 일을 한다.
doctor는 출발 전 점검이다. API가 살아 있는지, Docker가 있는지, token file이 있는지, target port가 열려 있는지 본다.
setup은 재현 재료를 만든다. 사용자, 그룹, DB connection, Docker fixture 같은 것들이다. 그래서 보통 안전하지 않다. 실행하면 로컬 환경이 바뀐다.
reproduce는 문제 조건을 실제로 일으킨다. proxy query를 날리거나, workflow를 트리거하거나, intentionally failing DB에 붙는다.
verify는 결과를 확인한다. 여기서 중요한 건 “스크립트가 성공했다”가 아니라 “제품 경로에서 기대한 현상이 보인다”는 것이다.
report는 사람이 읽을 수 있게 요약한다.
cleanup은 만든 것을 지운다. 단, 좋은 cleanup은 자신이 만든 것만 지워야 한다.
이제 어떤 시나리오든 이렇게 시작할 수 있다.
cd /Users/elon/elon/ai/projects/querypie
python3 dev-scripts/run-repro.py list
python3 dev-scripts/run-repro.py matrix
python3 dev-scripts/run-repro.py qcp-5445-data-policy-group-exclusion doctor
이런 작은 통일이 팀의 대화 방식을 바꾼다.
예전에는 “그 스크립트 어떻게 돌려요?”라고 물었다면, 이제는 이렇게 묻는다.
“doctor는 통과했나요?”
“setup이 mutation을 하나요?”
“verify는 실제 proxy 경로까지 본 건가요?”
질문이 좋아지면 디버깅도 좋아진다.
안전한 명령과 위험한 명령을 분리하기
재현 자동화에서 가장 무서운 건 애매함이다.
예를 들어 verify라는 이름의 명령이 내부에서 token을 새로 만들거나, DB fixture를 고치거나, Slack DM을 보내면 안 된다. 사용자는 “확인만 하겠지”라고 생각하고 실행하기 때문이다.
그래서 ReproKit runner를 정리할 때 중요한 기준을 세웠다.
doctor/report는 가능하면 항상 안전해야 한다.
verify는 가능하면 read-only여야 한다.
setup/reproduce는 mutation 가능성을 명시해야 한다.
real send, destructive cleanup, credential refresh는 explicit command로 분리한다.
예를 들어 Slack DM 관련 시나리오에서는 토큰이 없을 때 실패로 터뜨리지 않는다. 대신 이렇게 말한다.
토큰이 없으므로 실제 Slack send 검증은 건너뜀.
하지만 helper 구조와 safe command는 정상임.
권한 만료 알림 시나리오도 마찬가지다. render/dry-run과 real Slack send를 분리한다. 실제 발송은 confirm이 있는 legacy command에서만 한다.
이건 겁이 많아서가 아니다.
좋은 자동화는 조심스럽다. 특히 고객 지원, QA, 개발자가 같은 도구를 쓸 때는 더 그렇다. 버튼 하나가 실제 메시지 발송이 될 수도 있고, DB row 수정이 될 수도 있기 때문이다.
“없으면 실패”가 아니라 “없으면 설명”하기
이번 정리에서 마음에 들었던 패턴이 하나 있다.
fixture나 secret이 없을 때 무조건 실패하지 않고, 안전하게 설명하는 runner를 만들었다.
DML Snapshot 시나리오를 보자.
MYSQL_ADMIN_PASSWORD가 없으면 querypie_log와 querypie_snapshot을 읽을 수 없다. 예전이라면 그냥 실패했을 수 있다.
하지만 ReproKit에서는 passwordless verify가 이렇게 동작한다.
MYSQL_ADMIN_PASSWORD가 없으므로 metadb/snapshot readback은 실행하지 않음.
현재 helper, state, guide, API health는 확인됨.
MySQL initial ERR packet 시나리오도 비슷하다. Docker target MySQL fixture가 꺼져 있으면 raw probe를 할 수 없다. 그래도 reproduce나 verify가 실패로 끝나는 대신 safe skip을 한다.
target MySQL fixture가 열려 있지 않음.
setup/start-target 후 raw probe 가능.
이 차이가 크다.
실패는 “망했다”라는 느낌을 주지만, 설명은 “다음에 뭘 해야 하는지”를 알려준다.
재현 도구가 똑똑해지는 순간은 모든 걸 자동으로 해줄 때가 아니라, 지금 어디까지 안전하게 확인했는지 말해줄 때다.
Matrix는 ReproKit의 건강검진표다
ReproKit에는 matrix라는 명령이 있다.
python3 dev-scripts/run-repro.py matrix
이 명령은 모든 시나리오의 표준화 상태를 한 번에 보여준다.
api-token-allowed-zone-repro: level=standard-native
dml-snapshot-repro: level=standard-native
mysql-initial-err-repro: level=standard-native
qcp-5440-dac-due-permissions: level=standard-native
qcp-5445-data-policy-group-exclusion: level=standard-native
redis-client-setinfo-repro: level=standard-native
## sac-repro: level=standard-native
slack-dm-workflow-repro: level=standard-native
여기서 standard-native는 “이 시나리오는 ReproKit의 표준 운전대를 달았다”는 뜻이다.
즉, 최소한 다음을 기대할 수 있다.
- doctor/setup/reproduce/verify/report/cleanup 명령이 있다.
- repro.yaml에 명령 매핑이 있다.
- native runner가 ReproContext와 dispatch를 사용한다.
- safe command와 mutating command의 경계가 정리되어 있다.
이건 단순한 메타데이터가 아니다.
팀원이 “이 재현 하네스 믿고 돌려도 되나?”라고 물을 때 첫 번째로 보는 건강검진표다.
결과 패키지는 보고서가 아니라 검증된 묶음이다
버그 재현은 실행만으로 끝나지 않는다.
누군가는 나중에 결과를 봐야 한다. QA가 볼 수도 있고, 개발자가 PR에서 볼 수도 있고, 고객 지원 담당자가 고객용 설명을 만들 때 볼 수도 있다.
그래서 ReproKit은 Result Package라는 개념을 둔다.
qcp/knowledge-lab/test-results/<ticket-or-scenario>/
result-manifest.json
evidence-report.md
step-json/
screenshots/
transcripts/
proof.html
중요한 건 Markdown report 하나가 아니라, 그 report가 가리키는 이미지와 JSON이 실제로 존재하는지까지 확인하는 것이다.
예를 들어 이런 식으로 검증한다.
python3 dev-scripts/reprokit/bin/verify-result-package.py \
qcp/knowledge-lab/test-results/QCP-5445-policy-delivery-ui-evidence \
--report QCP-5445-querypie-ui-evidence-report.md \
--ticket QCP-5445 \
--scenario policy-delivery-ui-evidence \
--expect-pass \
--qcp5445-checks \
--print
검증은 이런 질문을 한다.
report가 참조한 screenshot이 실제로 있나?
step JSON 링크가 깨지지 않았나?
stale FAIL 문구가 남아 있지 않나?
token/password/Bearer 값이 새지 않았나?
masking verdict가 실제 proxy query와 맞나?
좋은 증거는 예쁘기만 한 화면이 아니다.
나중에 다시 열어도 “이때 뭘 검증했고, 어떤 근거로 PASS라고 했는지” 설명할 수 있는 묶음이다.
새 시나리오를 만들 때의 최소 골격
이제 새 재현 시나리오를 만들 때는 대충 이런 구조로 시작하면 된다.
dev-scripts/<scenario>/
README.md
repro.yaml
run-<scenario>.py
repro.yaml에는 최소한 이런 정보가 들어간다.
scenario: example-repro
ticket: QCP-0000
title: Example repro
standardization:
level: standard-native
primarySurface: ReproKit native runner + External API
notes: doctor/report are safe; setup/reproduce may mutate local fixtures.
commands:
doctor:
script: run-example-repro.py
args: [doctor]
setup:
script: run-example-repro.py
args: [setup]
mutates: true
reproduce:
script: run-example-repro.py
args: [reproduce]
mutates: true
verify:
script: run-example-repro.py
args: [verify]
cleanup:
script: run-example-repro.py
args: [cleanup]
report:
script: run-example-repro.py
args: [report]
핵심은 코드 양이 아니다.
핵심은 “이 명령이 무엇을 바꾸는지” 숨기지 않는 것이다.
ReproKit이 준 가장 큰 이득
이번 정리를 끝내고 나니, ReproKit의 진짜 가치는 자동화 자체보다 언어의 통일에 있다는 생각이 들었다.
이제 팀은 이렇게 말할 수 있다.
doctor는 통과했어.
setup은 local fixture만 건드려.
reproduce는 실제 proxy path를 타.
verify는 read-only야.
report에 evidence 경로가 있어.
cleanup은 prefix-scoped야.
이 문장들은 작지만 강하다.
버그 재현은 늘 복잡하다. API, DB, proxy, Slack, Docker, UI, log가 한꺼번에 나온다. 복잡함 자체를 없앨 수는 없다.
대신 복잡함에 손잡이를 달 수는 있다.
ReproKit은 그 손잡이다.
그리고 손잡이가 생기면, 팀은 더 빨리, 더 안전하게, 더 자신 있게 버그를 다룰 수 있다.