
AI가 '테스트 완료'라던 게임이 튜토리얼 첫 탭에서 죽었다: VIBRATE 권한과 신규 설치 몽키 테스트
진동 권한 한 줄이 빠진 크래시를 스크립트 테스트는 놓쳤고, 앱 데이터를 지운 뒤 돌린 adb 몽키는 15번째 이벤트에서 잡았습니다.
2026년 10월 1일 22시 58분, AI 코딩 에이전트가 출시 전이던 퍼즐 게임 화살표 매듭풀기를 Galaxy S24에 설치하고 "실행 시간 857ms, 크래시 없음"이라고 보고했습니다. 4분 뒤 운영자가 튜토리얼부터 에러가 난다고 알려 왔습니다. 첫 화살표를 누르는 순간 앱이 꺼지는 크래시였죠. 코드는 Vibrator.vibrate를 부르는데 매니페스트에 VIBRATE 권한이 없어서 SecurityException이 났습니다. 수정 커밋 65dcf41은 크래시 보고 5분 29초 뒤에 들어갔고, 15초 뒤 신규 설치 몽키 테스트 스크립트(f7beb3d)가 커밋됐습니다.
근거는 게임 저장소(sugar-game)의 git 기록, 그날 에이전트 세션 기록, 에이전트 메모리 파일입니다. 시각은 한국 시간이고, 기준일은 2026년 10월 19일입니다.
목차 (7)
설치 보고에서 크래시 보고까지 4분
그날 밤 기록을 시각순으로 옮겼습니다. 운영자의 말은 요지만 남겼습니다.
| 시각 | 일어난 일 |
|---|---|
| 22:47 | 튜토리얼을 넣으면서 진동을 VibrationEffect 합성 방식으로 바꾼 커밋 3eb21df |
| 22:58 | 에이전트가 S24 설치 뒤 "크래시 없음"으로 보고. 직후 S24 연결이 끊겨 화면은 못 봄. 운영자가 "테스트 완료된 거야?"라고 묻자 S24는 설치와 실행만 확인했고 S21은 "자동 테스트까지 완료"라고 답함 |
| 23:02 | 운영자가 튜토리얼부터 에러가 난다고 알림. S21에서 진행 기록을 지우고 몽키 시드 7을 돌리자 800회 중 15번째 이벤트에서 크래시 |
| 23:07 | 수정 커밋 65dcf41, 15초 뒤 tools/monkey_check.sh 커밋 f7beb3d |
에이전트가 틀린 곳은 "크래시 없음"의 근거였습니다. 근거는 am start -W가 돌려준 Status: ok와 실행 시간 857ms뿐이었습니다. 앱을 띄웠을 뿐, 화면을 한 번도 누르지 않았죠. (그때 본 logcat에도 FATAL EXCEPTION 한 줄이 있었는데, 9월 30일에 삼성 시스템 앱이 남긴 것이라 에이전트가 제대로 걸러 냈습니다.)
원인: VIBRATE 권한 없이 부른 Vibrator.vibrate
설치 10분 전 커밋 3eb21df가 진동을 View.performHapticFeedback에서 Vibrator.vibrate(VibrationEffect.startComposition()…)로 바꿨습니다. 코드 주석에 적힌 이유는 "삼성 기기에서는 CONTEXT_CLICK이 거의 느껴지지 않는다"였습니다.
두 API는 권한 조건이 다릅니다. View.performHapticFeedback은 권한 없이 동작하고, Vibrator.vibrate는 android.permission.VIBRATE를 요구합니다. 매니페스트에는 이 권한이 없었습니다. 화살표가 빠질 때마다 진동을 부르는 코드라서, 튜토리얼 첫 탭이 곧 크래시 지점이었습니다.
E AndroidRuntime: FATAL EXCEPTION: main
E AndroidRuntime: Process: (개발용 패키지명), PID: 12034
E AndroidRuntime: java.lang.SecurityException: vibrate: Neither user 10322 nor current process has android.permission.VIBRATE.
수정은 두 파일, 추가 6줄과 삭제 1줄입니다. 권한을 선언하고, 진동 호출이 어떤 이유로 실패해도 View.performHapticFeedback으로 넘어가도록 예외 처리를 넣었습니다.
<uses-permission android:name="android.permission.VIBRATE" />
runCatching { v.vibrate(VibrationEffect.startComposition().addPrimitive(primitive, scale).compose()) }
.onFailure { view.performHapticFeedback(fallback) }
VIBRATE는 일반(normal) 권한이라 사용자에게 묻는 창이 뜨지 않습니다. 선언하면 설치할 때 부여되고, 빠뜨려도 디버그 빌드는 문제없이 만들어집니다.
스크립트 테스트가 놓친 이유
그때 S21에서 돌던 기기 테스트는 두 개였습니다. tutorial_check.py는 튜토리얼 세 단계를 정해진 화살표 순서대로 누르고, device_check.py는 진행 기록 파일을 써 넣어 원하는 레벨로 건너뛴 뒤 판을 진행합니다. 둘 다 합격 기준이 따로 없었습니다. 튜토리얼 스크립트는 "다음" 버튼을 찾지 못할 때만 멈추고, 레벨 스크립트는 자기가 보낸 탭 수를 출력하고 끝납니다. logcat의 크래시와 프로세스 생존 여부는 어느 쪽도 보지 않았습니다.
에이전트는 "정해진 경로만 누르는 스크립트라서 못 잡았다"고 설명했습니다. 반만 맞는 설명입니다. 경로가 고정된 것보다 크래시를 보지 않는 판정이 더 큰 구멍이었습니다. (22시 45분쯤 튜토리얼 스크립트는 S21에서 끝까지 돌았습니다. 그 빌드에 진동 변경이 들어 있었는지는 그날 따지지 않았고, 남은 기록으로도 알 수 없습니다.)
device_check.py는 레벨로 건너뛸 때 tutorialDone=true도 함께 써 넣어서, 설치 직후 사용자가 처음 보는 화면을 거치지 않습니다. 처음 설치한 사람의 경로는 앱 데이터가 없는 상태에서만 재현됩니다.
신규 설치 몽키 테스트 스크립트
tools/monkey_check.sh는 시드 3개(7, 21, 99)마다 앱 데이터를 지우고, 앱을 띄우고, 무작위 이벤트 800회를 넣은 뒤 logcat에서 FATAL EXCEPTION을 셉니다. 한 시드라도 0이 아니면 실패로 끝납니다. 지금 버전에서 기기 지정, 출력 정리, 실패 종료 코드를 빼고 핵심만 옮겼습니다. 스크립트 기본값은 디버그 빌드 패키지이고, 아래는 Play에 올라간 릴리스 패키지로 적었습니다.
PKG=com.sugarmount.arrows
adb shell setprop debug.firebase.analytics.app $PKG
for seed in 7 21 99; do
adb shell am force-stop $PKG
adb shell pm clear $PKG
adb logcat -c
adb shell am start -W -n $PKG/sugar.arrows.MainActivity
sleep 2
adb shell monkey -p $PKG --pct-touch 90 --pct-motion 10 --pct-nav 0 --pct-majornav 0 \
--pct-syskeys 0 --pct-appswitch 0 --throttle 120 -s $seed 800
n=$(adb logcat -d | grep -c "FATAL EXCEPTION")
echo "seed=$seed fatal=$n"
done
-p: 이 패키지 안에서만 이벤트를 넣습니다.--pct-touch 90 --pct-motion 10: 탭 90%, 드래그 10%입니다. 내비게이션, 시스템 키, 앱 전환은 0으로 두어 게임 화면을 벗어나지 않게 했습니다.--throttle 120: 이벤트 사이에 120ms를 둡니다. 마지막 인자 800이 이벤트 수라서, 대기 시간만 96초입니다.-s: 시드를 고정하면 같은 이벤트 순서가 다시 나옵니다. 수정 전 시드 7은System appears to have crashed at event 15 of 800 using seed 7을 남겼고, 수정 뒤 같은 시드는 800회를 모두 넣었습니다.

여기서 "신규 설치"는 앱 데이터가 하나도 없는 상태를 말합니다. 첫 버전은 run-as로 진행 기록 파일 하나만 지웠습니다. run-as는 디버그 가능한 빌드에서만 되기 때문에, 릴리스 설정을 넣은 10월 3일 커밋 f421333에서 pm clear로 바꿨습니다. 주석은 "릴리스(디버그 불가) 빌드에서도 동작"입니다. 설치 과정 자체가 의심스러울 때는 adb uninstall 뒤 다시 설치하십시오. 스크립트는 시드마다 재설치하지 않고 데이터만 삭제합니다.
판정 방식도 바뀌었습니다. 첫 버전은 기기 전체 logcat의 FATAL EXCEPTION을 세었는데, 이러면 S24 로그의 삼성 시스템 앱처럼 남의 크래시까지 셉니다. 10월 4일 한판블록용 block_monkey.sh(커밋 575aaa1)는 Process: 패키지명, 줄만 세고, events 버퍼의 am_anr로 ANR도 셉니다. 주석에는 "uiautomator나 monkey 도구가 스스로 죽을 수 있다"고 적혀 있습니다. 10월 18일(dca02f2)에는 두 스크립트의 반복문 앞에 setprop debug.firebase.analytics.app을 넣었습니다. 이 설정은 테스트 실행을 Firebase DebugView에 보이게 할 뿐, GA4 보고서에서 빼 주지는 않습니다. 보고서에서 빼려면 GA4 속성에 개발자 트래픽 데이터 필터를 따로 켜야 합니다(2026-10-26 정정).
(수정 뒤 검증 중에 시드 21을 돌리다 S21 연결이 잠깐 끊겼습니다. 출력에 - waiting for device -가 찍힌 실행은 무효로 치고 다시 돌렸습니다.)
규칙: 신규 설치 몽키 없이 "완료"라고 하지 않는다
이 사고 뒤 에이전트 메모리에 규칙이 하나 생겼습니다. Android 빌드를 "테스트 완료"라고 보고하기 전에 정해진 경로를 누르는 스크립트에 더해 연결된 기기에서 monkey_check.sh를 돌리고, 어떤 기기에서 돌렸는지 정확히 적는다는 내용입니다. 매니페스트, 권한, 시스템 서비스 호출이 바뀌면 반드시 돌린다는 조건과 "설치만으로는 검증이 아니다"라는 문장도 들어 있습니다.
그 뒤로 커밋 메시지가 달라졌습니다. 2026년 10월 19일 기준, f7beb3d 이후 커밋 144개 중 19개의 메시지에 monkey가 들어 있고, 그중 16개가 결과를 적었습니다. "release (R8, test ads) monkey 3x1500 0 fatals", "Android 7 launch and monkey pass on API 24" 같은 줄입니다. 나머지 3개는 도구 목록이나 "아직 안 돌림" 같은 언급이었습니다. 결과에 적힌 이벤트 수는 600회부터 4,000회까지였습니다.

에이전트가 "완료"라고 하면 어느 기기에서 무엇을 돌렸는지부터 물으십시오. 신규 설치 상태의 몽키 결과가 없으면 완료로 받지 않습니다. 출시 전 검사 전체는 AI가 만든 Android 앱, 출시 전에 무엇을 검사하나에, 이런 규칙을 지침 파일에 남기는 방법은 AGENTS.md 작성법에 적었습니다.
다음 날(10월 2일) 만든 스트레스 스위트는 300레벨을 도는 첫 실행에서 다른 크래시를 28번 냈습니다. 멀리 막힌 화살표가 되돌아올 때 스프링이 넘치면서 points[-1]을 읽은 IndexOutOfBoundsException이었고, 커밋 69781a7에서 고쳤습니다. 신규 설치 몽키는 튜토리얼과 첫 레벨에서 출발하니, 깊은 레벨까지 가는 경로는 따로 돌려야 했습니다.
남은 일
화살표 매듭풀기의 monkey_check.sh는 지금도 기기 전체의 FATAL EXCEPTION을 세고 ANR은 보지 않습니다. 한판블록 스크립트처럼 자기 프로세스의 크래시와 ANR을 함께 세도록 두 스크립트를 맞춰야 합니다. 여러 세션이 같은 기기를 쓰는 환경이라, 다른 앱의 크래시로 실패가 나는 일을 먼저 막을 생각입니다.
자주 묻는 질문
VIBRATE 권한은 사용자에게 따로 요청해야 하나요?
요청하지 않아도 됩니다. VIBRATE는 일반 권한이라 AndroidManifest.xml에 선언하면 설치 때 부여됩니다. View.performHapticFeedback만 쓰면 선언이 필요 없지만, Vibrator.vibrate를 직접 부르면 선언이 없을 때 SecurityException으로 앱이 죽습니다.
adb 몽키 테스트는 몇 번, 몇 이벤트를 돌려야 하나요?
SugarMount는 시드 3개(7, 21, 99)에 시드당 800 이벤트를 기본으로 씁니다. 테스트 광고를 넣은 릴리스 빌드는 시드당 1,500 이벤트로 돌린 커밋이 5개입니다. 시드를 고정해야 실패한 순서를 그대로 다시 재현할 수 있습니다.
몽키 테스트를 실제 광고가 붙은 빌드에서 돌려도 되나요?
돌리지 않습니다. 무작위 탭이 실제 광고를 누르면 무효 클릭이 되어 AdMob 계정이 위험해집니다. 몽키와 자동 탭은 Google 테스트 광고 ID로 만든 빌드에서만 돌립니다.
단위 테스트가 통과하면 몽키는 생략해도 되나요?
생략하면 이번 같은 크래시를 놓칩니다. 그날 엔진 단위 테스트 31개는 모두 통과했지만, 권한 검사는 기기의 시스템 서비스가 하기 때문에 JVM 단위 테스트에는 나타나지 않습니다.
이 글은 SugarMount가 직접 측정한 값으로 쓰고, 초안 작성에 AI 코딩 에이전트를 썼습니다.
다른 AX 노트
- LLM 공유 캐시에 프롬프트 인젝션이 굳을 수 있었다: 문맥을 보내지 않기로 한 블라인드 A/B
- AI 코딩 에이전트와 비밀값: 채팅·로그·권한 세 겹, 복사된 마스킹 정규식이 놓친 예외 2곳
- 'E2E 100% 통과'의 정체: expect()가 0개인 러너 7개를 지운 날
- 편집 기능을 만들고도 꺼서 출시했다: HWP 저장 검증 게이트가 잡은 누름틀 결함
- Android assets를 미리 gzip한 실측: APK 이득은 0, .pack.gz는 .pack이 되어 FileNotFoundException
- SKT 판매 삼성 폰에서 market://을 열면 스토어 선택 창이 뜬다: Play 패키지부터 지정하기