AI 에이전트에게 테스트 추가나 CI 실패 수정 작업을 맡기면, "로컬에서는 통과했습니다", "수정했습니다"라는 보고가 돌아옵니다. 하지만 에이전트가 시도한 환경은 자신의 동작 환경일 뿐, CI의 job(작업)이 실행되는 환경과는 다릅니다. 이 보고서만을 근거로 변경 사항을 채택하면, CI에서만 발생하는 실패를 놓칠 수 있습니다.
Cloudflare Workers와 Vitest로 만든 한 리포지토리에서는 AI 에이전트가 추가한 통합 테스트가 CircleCI에서 가끔 실패했습니다. 에이전트가 제시한 타임아웃 연장이나 메모리 기반의 수정 사항들은 모두 CI 로그나 Resources 탭의 기록과 비교했을 때 원인에 도달하지 못하고 있었습니다. 본 기사에서는 CircleCI의 기록을 통해 무엇을 알 수 있는지, 그리고 AI 보고서 중 어디를 확인해야 하는지 소개합니다.
Cloudflare R2(Cloudflare의 오브젝트 스토리지) 목록을 가져오는 작업은 페이지당 1000개 단위로 분할됩니다. AI 에이전트는 페이지 경계를 넘어서도 누락되는 부분이 없는지 확인하기 위해, 1003개의 항목을 R2에 순차적으로 쓰는 통합 테스트를 추가했습니다. 이 테스트는 개별적인 타임아웃 설정이 없기 때문에, Vitest의 테스트 타임아웃(testTimeout) 기본값인 5,000ms가 상한선입니다. CI에서는 이 상한선인 5초에서 중단됩니다.
AI 에이전트는 커밋 메시지에 "로컬에서는 약 1.5초"라고 적고 타임아웃을 30초로 연장했습니다. 측정된 머신이 다르기 때문에 직접적으로 비교할 수는 없지만, 같은 테스트를 로컬에서 5번 측정했을 때 2133ms~2664ms가 소요되었습니다.
| 환경 | 횟수 | 소요 시간 | 중앙값 |
|---|
| 로컬 (CPU 4개, 15 GB, 제한 없음) | 5회 | 2133ms~2664ms | 2431ms |
| ... | | | |
중앙값의 차이는 50ms 미만이며, 차이가 벌어지는 것은 분포의 꼬리 부분입니다. 로컬에서는 CPU를 1코어로 제한해도 이 꼬리가 나타나지 않기 때문에, 로컬 값을 기준으로 한 타임아웃은 CI에서 꼬리에 해당하는 경우에 중단됩니다. 타임아웃 값은 CI에서 성공한 경우의 최대값을 밑돌지 않는 값으로 설정해야 합니다.
Workers Vitest 통합 테스트는 기본적으로 테스트 파일을 병렬로 실행하며, 스토리지(storage)를 테스트 파일별로 분리합니다(Isolation and concurrency). 1 CPU의 small 환경에서는 이 작업의 15개 테스트 파일이 하나의 CPU를 공유하는 형태가 됩니다.
로컬에서는 같은 테스트를 여러 번 실행하고, Vitest의 JSON 출력에서 대상 테스트의 소요 시간을 jq로 추출했습니다. CPU를 1코어로 제한하여 측정할 때는 taskset -c 0(프로세스를 0번 CPU만 사용하도록 하는 Linux 명령어)을 추가했습니다.
CI=true npx vitest run --config vitest.integration.config.mts \
test/integration/medallion-store.spec.ts --reporter=json --outputFile=out.json
jq '[.testResults[].assertionResults[] | select(.title|test("1000 件")) | .duration] | first' out.json
...
설정 파일, 테스트 파일 경로, select의 조건(테스트 이름 일부)은 프로젝트에 맞게 변경해 주세요.
CI 측 값은 호스트형 CircleCI MCP 서버(hosted CircleCI MCP server)의 get_job_logs를 사용하여 과거 test job 로그를 여러 번 가져와 모았습니다. MCP를 사용하지 않아도, 작업 로그를 성공한 경우와 실패한 경우로 비교하는 것부터 동일한 검증을 시작할 수 있습니다.
타임아웃을 30초로 연장했음에도 불구하고, 같은 테스트는 네트워크 연결 끊김 오류(Network connection lost)로 2번 실패했습니다. 다른 실행에서는 통합 테스트가 같은 오류로 떨어진 후, 실패 지점부터 워크플로우를 재실행(Rerun workflow from failed)하는 것만으로 성공했습니다.
의존 라이브러리 업데이트 시에는 pnpm install
OOM으로 인해 다운되었습니다. AI 에이전트는 원인을 확인하지 않은 채 빈 커밋(empty commit)으로 2회 재실행했으며, 두 번 모두 동일한 OOM (exit 137)으로 실패했습니다. 게다가 AI 에이전트는 '메모리 상한을 초과하여 workerd가 다운되고 있다'는 가설을 세우고, 통합 테스트를 하나의 worker로 실행하는 설정(singleWorker: true)까지 추가했습니다.
이 가설은 Job 상세의 Resources 탭에 표시되는 CPU와 RAM 사용량(Resource class overview)으로 검증할 수 있습니다.
| 설정 | 결과 | test job의 RAM 피크 (2 GB 대비 비율) |
|---|
| 기본값 | 성공 (1회) | 93.4% |
| 기본값 | 실패 (3회) | 92.1~92.7% |
singleWorker: true | 성공 (1회) | 48.5% |
singleWorker: true | 실패 (1회) | 50.3% |
기본 설정에서는 성공한 경우와 실패한 경우의 RAM 피크에 차이가 없습니다. 메모리가 원인이라면, 실패한 경우에만 피크가 높아져야 합니다. 피크가 같다면, 메모리 상한을 높여도 이 실패는 설명할 수 없습니다. singleWorker: true로 설정하니 피크는 약 절반으로 낮아졌지만, 그 횟수에서도 3건의 테스트가 연결 끊김 오류(connection lost error)로 다운되었습니다. AI 에이전트는 MCP 서버에서 얻은 이 데이터를 보여주며 스스로 가설을 철회했습니다.
의존 라이브러리 업데이트 시 발생한 OOM 문제는 로컬 환경에서 2 GB로 제한한 Docker를 사용하여 메모리를 측정한 후, 피크를 낮추는 수정으로 해결되었습니다. 커밋 기록에서는 피크가 수정 전 1.371.38 GB였고, 수정 후 0.991.13 GB입니다.
Resources 탭의 데이터 포인트는 15초 간격으로 측정되기 때문에(How To Determine if Your Build Is Under-Provisioned), 짧은 작업에서는 종료 직전의 피크가 기록에 남지 않을 수 있습니다. OOM으로 종료된 27초짜리 lint job의 경우, 데이터 포인트가 0초와 15초 두 개밖에 없어 기록상의 RAM 피크는 56.5%였습니다.
로그에서는 이 작업의 pnpm install이 강제 종료(표시: 134 Killed)되어 exit 137로 끝났습니다. Resources 탭 값이 낮다는 것은 OOM이 발생하지 않았다는 근거가 될 수 없습니다.
Network connection lost로 다운된 경우는 R2에 쓰기 작업이 많은 테스트뿐이었습니다. 총 1003건의 테스트와, R2의 읽기/쓰기를 수천 번 수행하는 다른 통합 테스트(status-silver)에서 그 외의 테스트는 한 번도 다운되지 않았습니다. 무거운 테스트를 추가한 후 통합 테스트 job이 실행된 35회 중, 이 오류로 다운된 것은 4회(약 11%)입니다.
스택 트레이스는 테스트마다 스토리지 격리/복원 처리(isolatedStorage)의 마무리 단계에서 멈추고 있습니다.
Error: Network connection lost.
❯ WorkersTestRunner.updateStackedStorage .../@cloudflare/vitest-pool-workers/dist/worker/lib/cloudflare/test-runner.mjs:126:17
❯ WorkersTestRunner.ensurePoppedActiveTryStorage .../test-runner.mjs:183:7
...
쓰기 작업이 많은 테스트일수록 마무리 단계에서 처리해야 할 데이터가 많아지고, 그 과정 중에 연결이 끊긴 것으로 보이지만, 근본 원인은 특정할 수 없었습니다. 로컬 환경에서는 CPU를 1코어로 제한해도 한 번도 재현되지 않았습니다.
결론적으로, 1003건의 테스트는 모크(mock)를 사용하는 유닛 테스트로 옮겼고, 의존성 업데이트에 따라 테스트 도구도 새로운 것으로 변경했습니다. 그 이후 5회에서는 이 오류가 발생하지 않았지만, 실패율 11%를 고려할 때 5회 연속으로 발생하지 않을 확률은 약 55%이므로, 사라졌다고 말하기에는 아직 충분한 횟수가 아닙니다. 모크로 옮기면서 실제 R2가 페이지당 1000건으로 나누는 동작도 검증하지 못하게 되었습니다. R2의 읽기/쓰기를 수천 번 수행하는 status-silver 테스트는 현재까지 통합 테스트에 남아 있습니다.
타임아웃(Timeout)과 원인 가설 모두에 대해, AI 에이전트의 보고와 상충되는 사실들은 CircleCI 로그와 Resources 탭에 남아 있었습니다. CircleCI MCP 서버를 사용하면, AI 에이전트 스스로가 작업(Job) 로그와 CPU 및 RAM 사용량을 가져와 성공 횟수와 실패 횟수를 비교할 수 있습니다. 본 기사의 사례에서도, 해당 데이터를 읽은 AI 에이전트가 메모리 가설을 철회했습니다. AI 에이전트에게 수정을 요청할 때는 이 비교까지 함께 요청해 보세요.