본문으로 건너뛰기

resumate 07. 테스트 도입과 AI 환각 차단

·12 min read·7 / 8

수천 줄을 만들어놓고 테스트가 없었다. vitest로 스위트를 깔면서 테스트를 붙일 수 없는 코드 두 군데를 뜯어냈고, AI가 자료에 없는 내용을 전제로 말하는 문제를 프롬프트에서 잡았다.

테스트가 하나도 없었다

테스트라고는 bun test 스크립트 하나와 컨텍스트 빌드 회귀 테스트 몇 개가 전부였다.

프론트엔드를 테스트하려니 bun test로는 부족했다. jsdom도 필요하고 컴포넌트 렌더링도 해야 해서 vitest로 옮겼다. jsdom과 @testing-library/react를 얹고, 커버리지는 @vitest/coverage-v8로 봤다. 여기에 msw를 붙인 게 핵심이다. 테스트할 때마다 실제 사이드카 프로세스를 띄우고 claude CLI를 부를 수는 없으니, HTTP 레벨에서 가로챈다.

세 런타임을 한 번에 돌리는 스크립트도 만들었다.

pnpm test        # vitest (프론트엔드)
pnpm test:sidecar # bun test (sidecar)
pnpm test:rust   # cargo test (Tauri)
pnpm test:all    # 셋 다

사이드카는 bun, Tauri는 cargo다. 한 저장소에 런타임이 셋이라 어쩔 수 없다.

빈 env에 두 시간

테스트를 붙이다가 진짜 이상한 걸 만났다. 사이드카를 띄워서 로그인 상태를 확인하는데 계속 이렇게 나왔다.

{"loggedIn": false, "authMethod": "none"}

분명 claude CLI에 로그인돼 있는데. 사이드카의 경로 탐색이 잘못됐나 싶어 한참 들여다봤다.

원인은 코드가 아니었다. 이 개발 환경에서 bun을 실행하면 process.env가 완전히 빈 상태로 시작한다. Object.keys(process.env).length가 0이고 HOMEundefined다. 그러니 자식으로 spawn되는 claude CLI가 설정 디렉토리를 못 찾고 로그아웃 상태로 판단한다.

env HOME=... bun ...도, 절대경로 실행도, {...process.env} 스프레드도 우회가 안 됐다. 실제 앱에서는 Tauri가 정상 env를 물려주니까 아무 문제가 없다. 코드 버그가 아니라 테스트 환경 아티팩트였다. 테스트가 실패했다고 코드가 틀린 게 아니라, 실패의 원인이 측정 장비 쪽일 수 있다.

다행히 claudeEnv()HOME/USER/SHELL/PATHos 모듈로 보강하고 있어서, 이 경로를 타는 코드는 빈 env에서도 정상 동작한다. 그게 오히려 좋은 검증 신호가 됐다.

테스트를 붙일 수 없다는 게 설계 문제였다

두 군데를 뜯어냈다.

SSRF 판정부를 먼저 뜯어냈다. Paws가 채용공고 URL을 크롤링하는데, 사용자가 http://192.168.0.1/이나 http://localhost:8080/을 넣으면 내부망을 긁는 도구가 된다. isPrivateAddress()로 막고 있었다. 이게 보안의 핵심인데 index.ts 한가운데 박혀 있어서 단위 테스트를 붙일 수가 없었다. security.ts로 빼내고 IPv4, IPv6, IPv4 매핑 주소 케이스를 테스트로 고정했다.

사이드카 경로 계산도 같은 이유로 뺐다. Rust에서 개발 모드는 <프로젝트>/binaries/sidecar를 보고 릴리스 모드는 <번들 리소스>/sidecar를 본다. cfg!(debug_assertions) 분기가 setup() 안에 인라인으로 있어서 검증할 방법이 없었다. 릴리스 빌드에서만 깨지는 종류의 버그인데, 그건 앱을 패키징해봐야 안다. resolve_sidecar_path(debug, manifest_dir, resource_dir) 순수 함수로 빼내고 테스트 두 개를 붙였다. 이제 cargo test로 잡힌다.

두 경우 다 테스트를 붙이려고 리팩터링한 게 아니라, 테스트를 붙일 수 없다는 사실이 설계 문제를 알려준 것에 가깝다.

CI도 붙이고(.github/workflows/test.yml), 테스트 입문 가이드 문서도 하나 썼다.

Echo가 없는 경험을 지어내고 있었다

Echo가 뽑은 예상 질문을 읽다가 이런 걸 발견했다.

"여러 프로젝트를 동시에 진행하면서 우선순위를 어떻게 정하셨나요?"

이력서에는 프로젝트가 세 개 적혀 있었다. 동시에 했다는 말은 어디에도 없었다. 목록에 여러 개가 있다는 이유만으로 병행했다고 단정한 것이다.

면접 질문에서 이건 꽤 나쁘다. 지원자가 "아, 동시에 한 건 아닌데" 하고 전제부터 정정해야 한다. 실제 면접에서 그렇게 시작하면 답변이 꼬인다.

사이드카의 Echo 시스템 프롬프트와 질문 생성 프롬프트 양쪽에 규칙을 박았다.

- 자료에서 확인되지 않은 경험을 사실처럼 전제하지 마라
- 여러 프로젝트가 기재돼 있다는 이유만으로 동시 진행을 추론하지 마라
- 경험 여부가 확인되지 않으면 먼저 경험 유무를 물어라
- 한 질문에서 한 역량만 확인해라

마지막 규칙 덕에 질문이 짧아졌다. 이전 질문들은 한 문장에 두세 개를 욱여넣어서 읽고 나면 뭘 답해야 할지 헷갈렸다.

바뀐 형태는 이렇다.

"여러 업무의 우선순위를 정해야 했던 경험이 있나요? 있다면 어떤 기준으로 순서를 정했는지 말씀해 주세요."

전제 대신 확인부터 한다. 설계 원칙의 근거 우선이 프롬프트에 들어갔다.

곁다리로 한글 줄바꿈도 고쳤다. break-words를 쓰면 한글 단어가 중간에서 잘린다. word-break: keep-all로 바꿨다.

Nova는 개발자만 쓰는 게 아니다

같은 문제의 다른 버전이다.

Nova는 프로젝트 자료에서 경험을 뽑아내는 에이전트인데, 프롬프트가 이렇게 시작했다.

"다음 프로젝트를 분석해줘. 코드와 문서에서 확인되는 사실만 사용하고..."

코드. 개발 직군 자료를 암묵적으로 전제하고 있었다. 마케팅 기획서나 디자인 포트폴리오를 넣으면 결과가 이상하게 나오는 게 당연했다. techStack 필드도 마찬가지로 기술 스택만 담게 돼 있었다.

프롬프트를 다시 썼다. "코드와 문서에서 확인되는 사실만"을 "개발 자료라고 가정하지 말고, 자료에서 확인되는 사실만"으로 바꾸고, techStack도 기술 스택이 아니라 도구와 업무 역량, 방법론까지 담게 넓혔다.

UI 문구도 "경험 발굴"에서 "프로젝트 정리"로 바꿨다. 숨은 경험을 발굴한다는 표현이 은근히 없는 것도 찾아낸다처럼 읽혀서 마음에 안 들었다.

파일 지원도 넓혔다. 비개발 직군 자료는 대부분 PPTX다. 사이드카에 /parse-presentation을 추가해 슬라이드 텍스트를 뽑는다. 구형 .ppt는 415로 거절하고 PPTX로 저장하라고 안내한다. 조용히 빈 결과를 주는 것보다 낫다.

파일 처리 경로도 하나로 합쳤다. 폴더 드롭이든 파일트리 드롭이든 readPathAsFile 하나를 거치게 해서, PDF와 PPTX가 텍스트 파일과 같은 길로 들어온다. 폴더 안의 파일 하나를 못 읽어도 나머지로 계속 진행한다.

끝난 것과 실패한 것을 구분할 수 없었다

에이전트가 작업 중인지 보여주는 게 loading 불리언 하나였다. 끝난 것도 실패한 것도 둘 다 그냥 false가 된다.

사용자가 그 화면을 열면 completed와 error를 확인 처리해서 idle로 돌린다. 읽음 처리와 같다.

캐릭터 독은 별도 WebView라서 Zustand 상태를 공유하지 않는다. Tauri 이벤트로 넘기고, 독이 새로 뜰 때는 현재 상태를 되묻게 했다. 이벤트만 쓰면 뜨는 타이밍에 따라 신호를 놓친다.

창 닫기도 같이 정리했다. Cmd+W와 OS 종료 요청을 가로채서 창을 파괴하지 않고 숨긴다. WebView가 살아 있어야 진행 중인 작업이 유지된다.

모델은 빈칸을 그럴듯한 걸로 채운다

오늘 고친 AI 문제 두 개가 같은 모양이었다. 프롬프트에 적어둔 게 아니라 적지 않은 것 때문에 생긴 문제다.

여러 프로젝트가 있으면 동시에 한 건 아닐 수도 있다는 걸 안 적었더니 동시에 했다고 단정했고, 개발 자료가 아닐 수도 있다를 안 적었더니 코드를 가정했다. 모델은 빈칸을 그럴듯한 걸로 채운다. 그럴듯한 게 사실은 아닌데.

테스트 쪽에서는 두 번 다 같은 패턴이었다. 테스트를 붙이려는데 못 붙이겠으면 대체로 그 코드가 너무 얽혀 있는 거였다. 테스트가 설계 리뷰 역할을 했다.

아직 새로고침하면 화면 작업 상태가 다 날아가고, 첨삭 결과가 기본 이력서를 덮어쓴다. Nova가 정리한 경험 카드도 이력서 반영에 안 쓰이고 있다.