콘텐츠로 건너뛰기
Codex

AI가 못 찾은 버그를 사용자가 스크린샷 한 장으로 잡아 준 이야기

TECH NOTE · AI COLLAB

단위 테스트가 전부 통과해도 모듈 사이의 가정은 안 잡힙니다. 연출만 돌고 상태는 그대로였던 버그를 스크린샷으로 잡은 이야기입니다.

테스트는 통과하는데 화면이 이상했다

바탕화면 캐릭터가 가끔 낙하산을 펴는 연출을 보여 줬다. 그런데 창은 그 자리에 그대로 있었다.

낙하산을 펴고 하강 자세를 취하는데 위치가 안 바뀐다. 벽을 오르는 연출도 마찬가지였다. 오르는 동작만 재생되고 실제로는 안 올라간다.

코드를 봐도 문제가 안 보였다. 테스트도 전부 통과했다. 규칙 엔진의 유닛 테스트, 이동 계산의 테스트, 상태 전환 테스트 — 다 초록불이었다.

스크린샷 한 장에 답이 있었다

증상을 잡은 건 실기기 스크린샷이었다. 디버그 정보를 화면에 띄워 둔 상태로 찍었다.

기록에는 이렇게 남아 있다.

사용자가 실기기 스크린샷으로 확인:
  parachute_float_sightseeing 포즈가 뜨는데 창이 멈춰 있고
  (디버그 HUD에 ambient-parachute 표시)
  window_wall_climb_mid 포즈가 뜨는데 수직 이동이 없음
  (HUD에 ambient-climb 표시)

어떤 규칙이 발동했는지가 화면에 찍혀 있었다. ambient-parachute라는 이름이 보이는 순간 원인이 확정됐다.

규칙이 지형을 안 보고 있었다

ambient-parachuteambient-climb는 유휴 시간만 보고 발동하는 규칙이었다.

ambient-parachute: idleSec >= 300
ambient-climb:     idleSec >= 180

조건이 저것뿐이다. 캐릭터가 지금 어디 있는지, 오를 벽이 있는지, 떨어질 공간이 있는지를 안 본다.

그래서 이런 일이 생긴다. 바닥에 서 있는데 300초가 지나면 낙하산 애니메이션이 재생된다. 떨어질 곳이 없으니 창은 안 움직인다. 연출만 돌고 상태는 그대로다.

기록에 이렇게 적혀 있다.

이 규칙들은 지형/좌표와 완전히 무관하게 발동하며,
애니메이션만 재생할 뿐 창을 실제로 이동시키지 않는다.

왜 테스트가 못 잡았나

여기가 이 글의 요점이다. 테스트는 잘못 짜인 게 아니었다. 각자 자기 일을 정확히 했다.

  • 규칙 엔진 테스트 — “유휴 300초면 ambient-parachute가 뽑힌다” ✓
  • 이동 계산 테스트 — “낙하 입력이 들어오면 아래로 이동한다” ✓
  • 상태 전환 테스트 — “낙하산 상태에서 착지 상태로 간다” ✓

전부 맞다. 빠진 건 “그 규칙이 뽑혔을 때 실제로 이동이 일어나는가” 였다.

두 모듈 사이에 있는 가정이라 어느 쪽 테스트에도 안 들어간다. 규칙 엔진은 자기가 뽑은 것이 실행 가능한지 모르고, 이동 계산은 자기가 안 불렸다는 걸 모른다.

단위 테스트가 촘촘할수록 이 틈은 오히려 안 보인다. 전부 초록불이니 문제가 없다고 느낀다.

같은 유형이 이미 있었다

더 뼈아픈 건 이게 처음이 아니었다는 점이다.

과거 ambient-side-peek / ambient-ledge-peek 제거와 동일한 유형의 버그다
(peek-rules-disabled.md 참고)

창 옆으로 고개를 내미는 연출도 같은 문제로 이미 한 번 지웠다. 옆에 창이 없어도 고개를 내미는 동작이 나왔다.

같은 유형이 두 번 났다는 건 개별 버그가 아니라 구조 문제라는 뜻이다. 규칙에 “실행 가능성”을 요구하지 않는 설계 자체가 원인이다.

그래서 나중에 규칙 엔진을 지형 인지형으로 바꿨다. 규칙이 창 위 여부 같은 조건을 볼 수 있게 하고, 참조하는 동작이 실제 가능한지 확인하게 했다.

지운 것의 파급을 계산했다

규칙 두 개를 지우는 것도 그냥 되는 일이 아니었다. 유휴 시간 사다리가 재정렬된다.

두 규칙 제거로 유휴 티어 사다리가 재정렬된다:
  idleSec=320은 이제 그 아래 240s 티어로 떨어지고
  idleSec=190은 20s 얕은 티어로 떨어진다

300초 규칙이 사라지면 320초일 때 그 아래 티어가 매치된다. 180초 규칙이 사라지면 190초는 20초 티어까지 떨어진다.

이 변화를 테스트에 반영했다.

crates/pet-core/src/rules.rs의
hermes_idle_ladder_prefers_deepest_matching_tier 테스트를
이 새 동작에 맞춰 갱신했다

규칙을 지우면 다른 규칙의 동작이 바뀐다. 우선순위로 고르는 구조에서는 삭제가 국소 변경이 아니다.

복원 조건도 적었다

문서에 되살릴 조건까지 적어 뒀다. 원본은 커밋 직전 상태에서 복구할 수 있다는 것도 함께.

이게 있는 이유는 지운 게 기능 자체는 아니기 때문이다. 낙하산 연출은 쓸모가 있다. 다만 지형을 보고 발동해야 한다. 그 조건이 갖춰지면 되살릴 대상이다.

“지웠음”만 적어 두면 다음 사람이 왜 없는지 모른다. 없는 기능은 흔적을 안 남기기 때문에, 지울 때 이유와 복원 조건을 같이 적어야 한다.

화면에 상태를 띄워 두는 값

이 일에서 실제로 값을 한 건 디버그 HUD였다. 화면 구석에 지금 어떤 규칙이 뽑혔는지, 어떤 상태인지를 계속 띄워 두는 것.

평소에는 거슬려서 끄고 싶어진다. 실제로 나중에 이런 커밋도 있다.

style: hide the debug info label overlay

그런데 이 버그는 그게 켜져 있었기 때문에 잡혔다. 스크린샷 한 장에 증상과 원인이 같이 찍혔다.

로그로 남기는 것과 다르다. 로그는 나중에 찾아 읽어야 하고, 어느 시점인지 맞춰야 한다. 화면에 찍히면 사용자가 “이상한데?” 하고 캡처하는 순간 원인이 함께 남는다.

개발 빌드에서는 켜 두고, 이상하면 무조건 캡처하는 습관이 이 종류 버그에는 제일 빠르다.

정리

  • 테스트가 전부 통과해도 모듈 사이의 가정은 안 잡힌다. 각자 자기 일만 검증한다
  • “규칙이 뽑힌다”와 “그 규칙이 실제로 효과를 낸다”는 다른 명제다
  • 단위 테스트가 촘촘할수록 이 틈이 안 보인다. 초록불이 안심을 준다
  • 같은 유형이 두 번 나면 개별 버그가 아니라 구조 문제
  • 우선순위로 고르는 구조에서 규칙 삭제는 국소 변경이 아니다. 다른 규칙의 동작이 바뀐다
  • 지울 때 이유와 복원 조건을 남긴다. 없는 기능은 흔적을 안 남긴다
  • 화면에 상태를 띄워 두면 사용자의 스크린샷 한 장에 증상과 원인이 같이 찍힌다

AI에게 코드를 다 읽히고도 못 찾은 것을, 화면에 찍힌 글자 하나가 잡았다.

이 블로그 더 보기

이 블로그는 실제 프로젝트를 AI와 함께 굴리면서 남긴 기록입니다.

  • 새 글은 RSS로 받아볼 수 있습니다.
  • 시리즈 전체는 여기에 정리해 두었습니다.

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다