TECH NOTE · AI COLLAB
단위 테스트가 전부 통과해도 모듈 사이의 가정은 안 잡힙니다. 연출만 돌고 상태는 그대로였던 버그를 스크린샷으로 잡은 이야기입니다.
테스트는 통과하는데 화면이 이상했다
바탕화면 캐릭터가 가끔 낙하산을 펴는 연출을 보여 줬다. 그런데 창은 그 자리에 그대로 있었다.
낙하산을 펴고 하강 자세를 취하는데 위치가 안 바뀐다. 벽을 오르는 연출도 마찬가지였다. 오르는 동작만 재생되고 실제로는 안 올라간다.
코드를 봐도 문제가 안 보였다. 테스트도 전부 통과했다. 규칙 엔진의 유닛 테스트, 이동 계산의 테스트, 상태 전환 테스트 — 다 초록불이었다.
스크린샷 한 장에 답이 있었다
증상을 잡은 건 실기기 스크린샷이었다. 디버그 정보를 화면에 띄워 둔 상태로 찍었다.
기록에는 이렇게 남아 있다.
사용자가 실기기 스크린샷으로 확인:
parachute_float_sightseeing 포즈가 뜨는데 창이 멈춰 있고
(디버그 HUD에 ambient-parachute 표시)
window_wall_climb_mid 포즈가 뜨는데 수직 이동이 없음
(HUD에 ambient-climb 표시)
어떤 규칙이 발동했는지가 화면에 찍혀 있었다. ambient-parachute라는 이름이 보이는 순간 원인이 확정됐다.
규칙이 지형을 안 보고 있었다
ambient-parachute와 ambient-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에게 코드를 다 읽히고도 못 찾은 것을, 화면에 찍힌 글자 하나가 잡았다.