Skip to content

테스트 자식의 console 을 stderr 로 돌려 러너 역직렬화 실패를 없앤다 - #57

Merged
lpaiu-cs merged 1 commit into
mainfrom
claude/test-runner-stdout-guard
Oct 3, 2026
Merged

lpaiu-cs merged 1 commit into
mainfrom
claude/test-runner-stdout-guard

Conversation

@lpaiu-cs

@lpaiu-cs lpaiu-cs commented Oct 3, 2026

Copy link
Copy Markdown
Owner

문제

npm test 가 간헐적으로 assertion 이 아니라 러너 자체 오류로 실패했다.

not ok 16 - tests\\review-boundaries.test.ts
  failureType: 'uncaughtException'
  error: 'Unable to deserialize cloned data due to invalid or unsupported version.'
  stack: #processRawBuffer (node:internal/test_runner/runner:353:20) / FileTest.parseMessage ...

그 파일의 남은 테스트는 보고조차 되지 않는다 (137개 → 126·130개). 같은 파일만 다시 돌리면 통과한다.

원인 — Node 22 러너의 stdout 프레임 파서

node --test 의 자식 프로세스는 stdout 으로 v8 직렬화 프레임([FF 0F][u32 길이][본문])을 보내고, 테스트 대상 코드의 console.log 도 같은 stdout 으로 섞여 나간다. 부모의 #processRawBuffer 는 프레임 하나를 읽은 뒤 같은 청크의 나머지가 다음 프레임 헤더라고 가정하고 3~6번째 바이트를 길이로 읽는다 (헤더 재탐색 없음).

  • 뒤에 붙은 게 한글이면 UTF-8 바이트(≥ 0x80)의 << 24 가 int32 음수가 되어 rawBufferSize < fullMessageSize 검사를 통과하고, 쓰레기를 역직렬화하다 readHeader() 에서 던진다.
  • ASCII 면 길이가 수백 MB 로 읽혀 기다리다가 다음 청크에서 헤더를 다시 찾아 복구된다 — 그래서 대부분의 프로젝트에선 안 보인다. 이 레포는 로그가 거의 다 한글이다.
  • 프레임과 로그가 한 청크로 합쳐질 때만(부모가 읽기 전에 두 write 가 파이프에 쌓일 때) 나므로 간헐적이다.

근거

  • 실패 로그 직전 줄은 매번 테스트 통과 프레임 바로 뒤의 대화 기록: https://chatgpt.com/c/fixture 였다. 3번째 바이트가 대 의 선두 바이트 0xEB 다.
  • 결정적 재현: 프레임 하나 + 한글을 한 번의 process.stdout.write 로 내보내는 테스트 파일은 매번 같은 오류로 실패하고, 같은 프레임 + ASCII 는 통과한다.
  • 누출량 측정: 각 테스트 파일을 자식 모드(NODE_TEST_CONTEXT=child-v8)로 돌려 stdout 에서 프레임을 걷어내면, 프레임 사이에 로그를 흘리는 파일이 7개 · 8,982B 였다 (review-boundaries 6,243B · chatgpt-send 852B · response-progress 743B · account-switch 614B · project-entry 365B · concurrency 105B · duplicate 60B). review-boundaries 만의 문제가 아니다.
  • upstream: 같은 증상이 Error: Unable to deserialize cloned data due to invalid or unsupported version nodejs/node#64061 로 보고돼 test_runner: convert to uint during deserialization nodejs/node#64706(>>> 0)으로 고쳐졌고 v24.x 에는 들어갔지만 v22.x 브랜치에는 없다 (main 은 test_runner: do not crash on stdout that mimics a v8 frame nodejs/node#66273 으로 헤더 재탐색까지 넣었다). engines 가 >=20 이라 Node 업그레이드로 풀 수 없다.

변경

테스트마다 t.mock.method(console, 'log', ...) 를 붙이지 않았다 — 위험한 파일이 7개였고, 한글 로그를 남기는 테스트가 새로 생길 때마다 재발한다. 모든 테스트 자식이 지나는 한 곳에서 막는다.

  • tests/stdout-guard.ts — 테스트 자식(NODE_TEST_CONTEXT === 'child-v8')에서만 전역 console 을 stderr 로 쓰는 Console 로 바꾼다. 러너는 stderr 를 readline 으로 줄 단위로만 읽고 파싱하지 않으므로 안전하다. stdout 은 프레임 전용이 된다. 부모 러너 프로세스는 건드리지 않는다.
  • package.json — npm test 가 이 파일을 --import 로 싣는다 (러너가 --test* 외의 execArgv 를 자식에 그대로 넘긴다).
  • tests/stdout-guard.test.ts — 자식의 console.log 가 stdout 에 닿지 않는지 확인한다. 가드 없이 tsx --test 를 직접 부르면 간헐적 실패 대신 이 테스트가 실행 방법을 알리며 결정적으로 실패한다.
  • AGENTS.md 빌드 명령 목록에 npm test 를 적었다.

로그는 없애지 않고 stderr 로 그대로 나온다. 기존 테스트·assertion 은 손대지 않았다.

검증

실행 역직렬화 실패
수정 전 npx tsx --test (전체) 33회 3회 (run 3 · 16 · 17, 모두 review-boundaries)
수정 후 npm test (전체, 연속) 30회 0회
  • 실패율이 ~9% 라 반복 실행만으로는 근거가 약하다 — 결정적 근거는 누출량 측정이다: 가드 후 18개 파일 전부 프레임 사이 누출 0B 로, 버그 경로에 들어갈 입력 자체가 없어졌다.
  • 가드 없이 stdout-guard.test.ts 실패 / 가드와 함께 통과를 둘 다 확인했다.
  • tsc --noEmit 통과 · npm run smoke 339 passed · npm test 138 passed · git diff --check 통과.

테스트 인프라만 바뀌므로 데몬 재시작은 필요 없다.

npm test 가 간헐적으로 assertion 이 아니라 러너 자체 오류로 실패했다
("Unable to deserialize cloned data due to invalid or unsupported version",
failureType=uncaughtException). 그 파일의 남은 테스트는 보고조차 되지 않는다.

원인은 Node 22 러너(internal/test_runner/runner.js #processRawBuffer)다. 자식의 stdout 은
직렬화된 이벤트 프레임과 console.log 출력이 섞여 흐르는데, 러너는 프레임 하나를 읽은 뒤 같은
청크의 나머지가 다음 프레임 헤더라고 가정하고 3~6번째 바이트를 길이로 읽는다. 로그가 한글이면
UTF-8 바이트(≥ 0x80)의 `<< 24` 가 음수가 되어 길이 검사를 통과하고 쓰레기를 역직렬화한다.
ASCII 로그는 길이가 수백 MB 로 읽혀 기다리다 다음 청크에서 복구되므로 안 터진다 — 이 레포는
로그가 거의 다 한글이라 runRound 를 도는 테스트에서 났다. 프레임과 로그가 한 청크로 합쳐질
때만(부모가 읽기 전에 두 write 가 쌓일 때) 나므로 간헐적이다.

- 실패 로그 직전 줄은 테스트 통과 프레임 바로 뒤의 "  대화 기록: ..." 이었다 (3번째 바이트
  0xEB). 프레임 + 한글을 한 번에 write 하는 테스트로 결정적으로 재현되고, 같은 프레임 +
  ASCII 는 통과한다.
- upstream 은 nodejs/node#64706(`>>> 0`, v24.x 반영)·#66273 으로 고쳤지만 v22.x 에는 없다.

테스트마다 console 을 mock 하지 않는다 — 자식 stdout 에서 프레임을 걷어내 보면 프레임 사이에
로그를 흘리는 파일이 7개(8,982B, review-boundaries 6,243B)였고, 한글 로그를 남기는 새 테스트가
생길 때마다 재발한다.
npm test 가 tests/stdout-guard.ts 를 --import 로 싣고, 테스트 자식(NODE_TEST_CONTEXT=child-v8)
에서 console 을 stderr 로 보내는 Console 로 바꾼다. stderr 는 러너가 줄 단위로만 읽고 파싱하지
않는다. 출력은 그대로 남고 assertion 은 손대지 않았다. 가드 없이 실행하면
stdout-guard.test.ts 가 실행 방법을 알리며 결정적으로 실패한다.

@lpaiu-cs lpaiu-cs left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

gpt-chat-pr-reviewer 1차 리뷰

이 리뷰는 자동화 봇에 의한 gpt 리뷰이며, 사용자 의견이나 판단을 담지 않습니다.

추론 모델: Latest · Pro (5/5)

판정: ✅ approve — 지적 사항 없음

지정된 head와 merge-base를 확인하고 고정 diff의 4개 파일 전체를 검토했습니다. 이번 변경으로 새로 발생하거나 악화되는 구체적인 버그·보안·성능 문제는 발견하지 못했습니다. Node.js 22.16.0에서 추가된 회귀 테스트를 --experimental-strip-types로 별도 실행하여 가드 적용 시 통과하고 미적용 시 의도한 assertion으로 실패함을 확인했습니다. 일반 프로세스에서는 기존 console이 유지되는 것도 확인했습니다. 실제 tsx 기반 npm test 전체와 다른 실행 환경은 검증하지 않았습니다.

@lpaiu-cs
lpaiu-cs merged commit e0a6ad3 into main Oct 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant