Skip to content

model-registry.test.ts가 호스트 API 키 환경변수를 읽어 로컬에서만 실패 — 리뷰어가 유령 실패를 PR 탓으로 오인한다 #3945

Description

@yazzang-homelab

문제

packages/coding-agent/test/model-registry.test.ts가 호스트 셸의 프로바이더 API 키 환경변수를 읽는다. 그래서 개발자 환경에 따라 결과가 달라진다.

재현

현재 dev(2c55db492) 기준, 자격증명이 설정된 셸에서:

$ bun test packages/coding-agent/test/model-registry.test.ts
(fail) ModelRegistry > generic local OpenAI-compatible provider config > uses stored credentials for OpenAI-compatible providers without inline keys
(fail) ModelRegistry > active provider resolution > keeps credentialless discovery active with an irrelevant dangling credential
 222 pass, 3 fail

실패 내용이 원인을 그대로 보여준다 — 테스트가 기대하지 않은 실제 프로바이더가 결과에 섞인다:

- Expected  - 0
+ Received  + 16
+   { "connectionKind": "credential", "provider": "openai" },
+   { "connectionKind": "credential", "provider": "openrouter" },
+   { "connectionKind": "credential", "provider": "local" },

모든 *API_KEY* 변수를 제거하고 격리된 HOME으로 같은 커밋을 돌리면:

$ env -u OPENAI_API_KEY -u OPENROUTER_API_KEY -u GROQ_API_KEY ... HOME=/tmp/isohome \
    bun test packages/coding-agent/test/model-registry.test.ts
 225 pass, 0 fail

HOME만 격리하면 부족하다 — 2건은 계속 실패한다. 환경변수 제거가 결정적이다. 즉 HOME이 아니라 환경변수 경로가 누출 지점이다.

왜 문제인가

  1. 로컬 검증이 신뢰할 수 없다. 자격증명을 설정해 둔 기여자는 자기 변경과 무관한 빨간 불을 보고, 자기가 깨뜨렸다고 오인한다. 실제로 내가 #3741을 리뷰하면서 이 2건을 그 PR 탓으로 돌릴 뻔했다 — dev에서 대조하지 않았으면 잘못된 블로커를 걸었을 것이다.
  2. CI에서는 안 잡힌다. 러너에는 그 변수가 없어서 항상 초록이다. 그래서 아무도 고치지 않는다.
  3. 모델/프로바이더 영역은 기여가 잦다. 지금 열린 PR 중 fix(onboarding): load pasted custom-provider credentials #3741, fix(models): surface DeepSeek V4 proxy max effort and wire mapping #3892, feat(providers): add ClinePass and Command Code GOAT #3927, #3777이 전부 이 파일이나 인접 파일을 건드린다. 그 리뷰어들이 전부 같은 유령 실패를 만난다.
  4. 저장소에 이미 같은 계열의 수정 전례가 있다. 23983ef4a("isolate red-team test fixtures from CI changed-path leak")가 CI_DEV_CHANGED_PATHS 누출에 대해 정확히 같은 일을 했다 — 모듈 로드 시 캡처, beforeEach에서 클리어, afterAll에서 복원. 여기에도 그 패턴을 적용하면 된다.

제안

model-registry.test.ts(및 프로바이더 자격증명을 읽는 인접 스위트)를 그 전례대로 격리한다.

  1. 프로바이더 자격증명 환경변수 집합을 모듈 로드 시 캡처한다.
  2. beforeEach에서 제거한다.
  3. afterAll에서 복원한다.
  4. 자기 값을 설정하는 테스트는 자체 try/finally 안에서 유지한다.

변수 목록을 손으로 유지하는 대신, 레지스트리가 실제로 참조하는 apiKeyEnv 이름들을 번들 카탈로그에서 유도하면 새 프로바이더가 추가돼도 자동으로 덮인다.

부수적으로: 격리 상태에서 removes stale model bindings after config removal or partial replacement가 5,007ms에서 한 번 실패하고 재실행에서 통과했다. 별개의 타이밍 취약성으로 보이며 같이 볼 가치가 있다.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions