Files
homeclaw/tests
kimandClaude Opus 5 b2075c62d8 fix: buildDirectPriceAnswer가 무관한 달러 값을 "Answer:"로 합성해 붙이던 문제
이 함수가 만드는 줄은 검색 결과 stdout 맨 앞에 붙는다. 시스템 프롬프트의 ANTI-HALLUCINATION은
"도구가 돌려준 것을 정확히 보고하라, 절대 무시하지 말라"고 지시하므로, 여기서 틀리면 모델에게
틀린 값을 신뢰하라고 시키는 셈이다. 게다가 합성된 숫자는 도구 출력 텍스트의 일부가 되므로,
모델이 그대로 옮기면 NUMERIC-GROUNDING이 "소스에 있다"며 통과시킨다 — 우리가 만든 숫자가
'근거 있음' 도장을 받아 나간다.

실제 사고(프로덕션 로그):
  USER: 컨테이너선 급유비가 얼마나 될까?
  TOOL: web_search("average fuel cost for 20,000 TEU container ship per voyage")
  TOOL OK: Answer: The current price is approximately $38.00 USD.
$38은 스니펫에 있던 "신조선 7일 항해 시 TEU 1개당 추가 연료비"였고, 실제 답은 항해당 수백만
달러다. 그 턴은 모델이 결과 [1]을 직접 읽어 스스로 바로잡았지만 그건 운이다.

* isPriceQuery에 단어 경계가 없어 통화·가격 낱말이 다른 단어 **안에서** 걸렸다. "eur"가
  "Europe"·"Neuralink"에, "cost"가 "costume"에, "value"가 "valuable"에 들어맞는다. 실사용
  고유 검색어 195건 중 19건이 통과했고 그중 8건이 가격과 무관했다("Europe drought status
  August 2026", "Neuralink Blindsight resolution pixels" 등). 경계를 넣어 19 → 11건.

* generic 자산 경로를 제거했다. 금·은·비트코인은 타당성 범위(온스당 300~10,000 등)와 자산명
  대조가 성립해 판정이 의미가 있다. generic은 둘 다 없다 — 허용 범위가 $0.5~$5,000,000이라
  사실상 모든 달러 표기를 받고, 문구도 무엇의 가격인지 말하지 못한 채 "The current price"라고만
  쓴다. 이 로그 표본에서 generic 경로가 실제로 답을 낸 유일한 사례가 위 컨테이너선 건이다.

* 자산명 대조를 가점(+2)에서 **필수 조건**으로 올렸다. 가점일 때는 자산을 한 번도 언급하지
  않은 스니펫의 숫자가 score 0으로 통과했다 — "금 시세" 질문에 다른 상품 가격이 답으로 나갈
  수 있다는 뜻이다. 티커(XAU/XAG/BTC)도 함께 본다.

순효과(실사용 고유 검색어 195건): 발동 19 → 11건, 그중 실제로 "Answer:" 줄이 생길 수 있는
것 0건. 이 표본의 가격 질문은 전부 GPU·옵션·펌프 같은 generic 대상이라, 원래도 이 기능이
맞는 답을 낸 적이 없다.

테스트 544개 통과(+8).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rfme1WVEPkpNwnf5oVNXXc
2026-09-04 18:06:31 +09:00
..

tests/

npm test          # 전체 실행 (약 0.6초)
npm run test:watch  # 파일 저장할 때마다 재실행
npx tsx --test tests/prompt-gates.test.ts   # 한 파일만

Node 22 내장 러너(node:test) + tsx. 테스트 프레임워크 의존성 없음 — jest/vitest 설치 불필요.

배경

2026-07-29 기준 이 디렉터리는 비어 있었고, package.json의 test 스크립트는 존재하지 않는 tests/test-v2.ts를, gateway 스크립트는 v4.3.5에서 삭제된 src/gateway/server-v2.ts를 가리킨 채 방치돼 있었다. 즉 npm test가 실행 자체가 안 되는 상태로 몇 달을 보냈다.

그 대가는 명확했다. 같은 날 프롬프트 게이트 정규식을 다섯 번 고치면서, 매번 node -e "..."로 임시 검증 스크립트를 손으로 짜고 버렸다. 회귀 방지는 하나도 남지 않았다.

무엇을 테스트하는가

순수 함수 우선. 이 저장소에서 가장 값어치 있는 테스트 대상은 I/O가 없는 판정 함수들이다:

파일 대상 왜 중요한가
prompt-gates.test.ts src/gateway/guards/prompt-gates.ts 모델 동작을 강제/억제하는 정규식 게이트. 각 함수는 전부 실제 프로덕션 사고를 겪고 생겼다
usage-log.test.ts src/providers/usage-log.ts Ollama 외 모든 provider의 토큰 집계 단일 지점

이 함수들의 회귀는 크래시도 스택트레이스도 없이 조용히 구멍을 다시 연다. 예를 들어 looksLikeUnverifiedSpecClaim이 한 패턴을 놓치면, 모델이 지어낸 수치가 검증 없이 그대로 사용자에게 간다 — 로그에는 아무 이상도 남지 않는다.

케이스 작성 규칙

프로덕션 원문을 그대로 쓸 것. 줄이거나 다듬지 말 것.

[2026-07-29] 태그가 붙은 케이스들은 실제 대화 로그에서 가져온 문장이다. 어색한 표현이 바로 핵심이다 — 정규식이 놓치는 건 언제나 "예상하지 못한 말투"이지 교과서적인 문장이 아니다.

이 규칙은 실제로 값을 했다. 위 테스트를 처음 쓸 때 원문 "...GPU 간 통신 병목으로 인해 토큰 처리 속도 손실이 약 15%~25%..."를 "...토큰 처리 속도 손실이 약 15%~25%..."로 줄여 썼더니 테스트가 실패했고, 그게 진짜 버그였다. 원문에 우연히 들어있던 GPU 덕에 걸리던 것이지, 같은 주장을 한 단어 다르게 쓰면 그대로 통과하고 있었다. (→ SPEC_CONTEXT_KEYWORD에 토큰/대역폭/추론/처리속도 추가)

다음에 추가하면 좋을 것

현재 커버리지는 순수 함수에 한정된다. 아래는 값어치는 크지만 먼저 리팩터링이 필요하다:

  • skillToolFilter (handle-chat.ts) — 도구 노출을 결정하는 키워드 게이트. handleChat() 내부 클로저라 지금은 테스트 불가. 순수 함수로 추출하면 바로 테스트 가능해진다.
  • 시스템 프롬프트 조립 — 도구 유무에 따른 조건부 블록(imageEditRuleBlock 등). 역시 handleChat() 내부에 있다.
  • executeWebSearch provider 폴백 체인 — I/O가 있어 mock 서버가 필요하다.

handleChat()은 단일 함수가 2,959줄이라 위 둘 다 막혀 있다. 그 분해가 테스트 커버리지를 넓히는 가장 큰 지렛대다.