버그 재현 스크립트는 종종 탐정 수첩처럼 시작한다.
첫 장에는 증상이 적혀 있다.
명령은 실행됐다. 화면에도 결과가 보인다. 그런데 감사 로그에는 없다.
두 번째 장에는 용의자가 적힌다.
- shell이 이상한가?
- 프롬프트가 이상한가?
- 로그 저장이 늦나?
- Web Terminal과 실제 SSH 사이에 뭔가 다르나?
그리고 세 번째 장쯤 오면 갑자기 수첩이 지저분해진다.
프롬프트를 바꾸고, shell을 열고, 명령을 실행하고, output을 보고, 감사 로그를 기다리고, shell을 닫고, JSON으로 저장한다. 한 번은 괜찮다. 두 번도 괜찮다. 그런데 비슷한 사건이 또 오면 슬슬 이런 생각이 든다.
“이거 매번 새로 쓰는 게 맞나?”
범인은 작았지만, 절차는 반복됐다
이번에 다룬 문제의 핵심은 아주 작았다.
zsh 프롬프트 끝에 붙은 %, 그리고 그 뒤의 공백 하나.
터미널 화면에서 프롬프트는 장식처럼 보인다. 하지만 중계 시스템 입장에서는 “명령이 끝났다”는 신호등이 될 수 있다. 명령 출력 뒤에 다음 프롬프트가 다시 보이면, 시스템은 대략 이렇게 판단한다.
출력이 끝났다.
프롬프트가 돌아왔다.
이전 명령은 완료됐다.
감사 로그를 남길 수 있다.
그런데 프롬프트가 고객 환경마다 다르면 이 신호등도 조금씩 달라진다.
PROMPT='[%n@%m]%~%# '
이 zsh 템플릿은 실제 화면에서 대략 이런 모양이 된다.
[qpuser@host]~%
눈으로 보면 별것 아니다. 하지만 마지막 공백은 사람 눈보다 바이트가 더 정직하게 안다.
... 5d 7e 25 20
끝의 25 20은 % 다. 퍼센트와 공백. 이 작은 꼬리가 명령 경계를 찾는 단서가 된다.
그래서 도구함을 만들었다
처음에는 특정 재현 스크립트 안에 모든 절차가 들어 있었다.
- SAC role 선택
- shell 시작
- 명령 실행
- shell output 확인
- Command Audit 조회
- shell 종료
- 결과 JSON 저장
문제는 이 흐름이 특정 사건 전용이 아니라는 점이다.
앞으로도 비슷한 문제는 계속 올 수 있다.
- bash 프롬프트는 되는데 zsh 프롬프트는 안 되는 경우
- ANSI 색상 코드가 섞인 프롬프트
- multiline prompt
- root 계정의
#프롬프트 - Web Terminal에서는 안 보이는데 직접 SSH에서는 보이는 경우
- 출력 버퍼가 커져서 명령 경계가 흐려지는 경우
그때마다 같은 절차를 복사하면, 재현 스크립트는 점점 “사건마다 다른 수첩”이 된다. 읽는 사람은 매번 새 탐정의 필체를 해독해야 한다.
그래서 이번에는 거창한 프레임워크를 만들지 않았다. 딱 반복되는 부분만 공용 도구로 뺐다.
새 도구함의 핵심은 세 가지다.
1. 명령 감사 케이스를 한 번에 실행하는 함수
첫 번째 도구는 run_command_audit_case다.
이 함수는 하나의 명령을 실행하고, 그 명령이 감사 로그에 남았는지까지 확인한다.
개념적으로는 이런 일을 한다.
role 선택
shell 열기
명령 실행
output 확인
Command Audit 기다리기
shell 닫기
표준 결과 반환
좋은 점은 단순하다.
이제 재현 시나리오를 쓰는 사람은 “shell을 닫는 걸 깜빡했나?”, “audit 조회 retry는 몇 번이지?”, “결과 key 이름은 뭐였지?” 같은 잡음을 덜 신경 써도 된다.
시나리오는 더 중요한 질문에 집중할 수 있다.
어떤 프롬프트 조건에서 감사 로그가 남고, 어떤 조건에서 사라지는가?
2. 프롬프트 증거를 같은 모양으로 남기기
두 번째 도구는 PromptProbe.to_result()다.
프롬프트 문제는 말로 설명하면 자주 헷갈린다.
“끝에 공백이 있어요.”
이 말은 충분하지 않다. 어디에? 실제 화면에? 설정 문자열에? raw byte에? zsh가 렌더링한 뒤에?
그래서 프롬프트 증거는 같은 모양으로 남기기로 했다.
container
account
shell
templateVar
template
rendered
renderedHex
trailingSpace
containsEsc
여기서 중요한 건 rendered와 renderedHex를 같이 보는 것이다.
rendered는 사람이 읽는 화면이다.renderedHex는 거짓말을 못 하는 바이트 기록이다.
공백은 부끄러움이 많아서 화면에서는 잘 안 보인다. 하지만 hex에서는 숨지 못한다.
3. 결과 JSON을 표준 모양으로 저장하기
세 번째 도구는 write_command_audit_result다.
재현 결과는 결국 사람이 읽어야 한다. 개발자도 읽고, QA도 읽고, 지원팀도 읽고, 미래의 나도 읽는다.
그래서 결과 JSON의 뼈대를 맞췄다.
ticket 또는 사건 식별자
scenario
generatedAtUtc
target
querypie
targetPromptProbe
cases
verdict
이렇게 해두면 좋은 점이 있다.
하나의 케이스만 볼 때도 읽기 쉽고, 여러 조건을 matrix로 비교할 때도 쉽다.
예를 들면 이런 식이다.
조건 A: separator 없음 -> 명령 output은 있음, audit은 없음
조건 B: separator 정확히 설정 -> 명령 output 있음, audit 있음
조건 C: 퍼센트만 설정 -> 기대와 다름
이제 재현 결과가 “스크립트가 뭔가 출력했다”가 아니라, “비교 가능한 실험 기록”이 된다.
오버엔지니어링은 피했다
이런 작업을 하다 보면 유혹이 온다.
“아예 YAML DSL을 만들까?”
shell: zsh
prompt: "[%n@%m]%~%# "
cases:
- separator: null
- separator: "% "
멋있어 보인다. 하지만 지금은 하지 않았다.
이유는 간단하다.
아직 사건의 모양이 충분히 쌓이지 않았다. 지금 DSL을 만들면, 다음 사건에서 바로 예외가 생길 가능성이 높다. 그러면 버그를 재현해야 하는데 DSL부터 디버깅하는 이상한 상황이 된다.
이번 개선은 일부러 작게 했다.
- 반복되는 shell/audit 절차만 공통화
- 프롬프트 증거 shape만 고정
- 결과 JSON writer만 정리
- 기존 시나리오는 여전히 Python으로 명시적으로 유지
즉, 새 고속도로를 깐 게 아니라, 자주 쓰는 공구에 라벨을 붙인 정도다.
그 정도가 딱 좋았다.
좋은 재현 도구는 제품 코드만큼 친절해야 한다
재현 스크립트는 종종 “일회용” 취급을 받는다.
하지만 실제로는 그렇지 않다.
좋은 재현 도구는 팀의 기억이 된다. 왜 이 문제가 생겼는지, 어떤 조건에서 재현되는지, 무엇을 확인해야 하는지, 다음 사람이 어디서 시작하면 되는지를 알려준다.
특히 터미널/감사 로그처럼 눈에 보이는 화면과 내부 판정이 어긋나는 문제에서는 더 그렇다.
사람은 화면을 본다.
시스템은 바이트를 본다.
감사 로그는 경계를 본다.
좋은 ReproKit은 이 셋을 한 장의 사건 기록으로 묶어준다.
이번에 얻은 작은 원칙
이번 정리에서 남은 원칙은 네 가지다.
첫째, 반복되는 절차는 함수로 빼되, 사건의 맥락은 숨기지 않는다.
둘째, 프롬프트처럼 눈에 안 보이는 차이는 문자열과 hex를 같이 남긴다.
셋째, 결과 파일은 사람이 비교할 수 있는 schema로 저장한다.
넷째, 너무 일찍 DSL을 만들지 않는다. 먼저 좋은 Python helper를 만든다.
이 정도면 다음 탐정이 사건 현장에 도착했을 때 훨씬 덜 헤맨다.
수첩은 여전히 필요하다.
하지만 이제 적어도 돋보기, 장갑, 증거 봉투는 같은 위치에 있다.