---
date: 2026-05-09
type: error
tags: [ci, jest, integration-test, oom, v8, runner-band]
---

# jest --runInBand 跑全量集成测试 → V8 默认 ~2 GB heap OOM

## 现场

`backend-integration` job (run #1329 / job 5 / 内部 id 5786) 在 PR `docs/audit-system-all-modules` sync develop 后 OOM：

```
FATAL ERROR: Ineffective mark-compacts near heap limit
Allocation failed - JavaScript heap out of memory
965693 Aborted (core dumped) node ../backend/node_modules/.bin/jest ... --runInBand
exit status 134
```

PID 始终是 `965693` —— 整个 12 分钟跑了 32 个 suite **全在同一个 Node 进程**，heap 累积到 1879 MB 触 V8 默认 ~2 GB old-space 上限。

## 根因

`testing/scripts/run-backend-integration.sh` 调用：

```bash
node ../backend/node_modules/.bin/jest --config ... --forceExit "$@"
```

CI 通过 `$@` 传 `--runInBand`，把全部 36 个 integration suite **串行塞在同一个 jest 进程**。每个 suite 启动 NestJS app + Prisma client + Temporal client，`afterAll` 销毁不彻底 → 闭包 / event listener / Prisma connection pool 等持续累积。跑到第 32 个 suite 时 heap 撞 V8 上限 → OOM。

加剧因子：`TemporalService.onModuleInit` 在测试环境也尝试连 Temporal，每个 suite 都失败一次（35+ 次错误日志），每次失败都新建 error 对象 / Promise reject 上下文，加速 heap 增长。

## 不是机器内存问题

UAT runner 物理内存够（free -h 远超 2 GB），机器加内存救不了 V8 进程级 heap 上限。必须在 Node 启动时显式调高 `--max-old-space-size`。

## 修复（治标，本 commit）

`testing/scripts/run-backend-integration.sh` 在调用 jest 前 export：

```bash
export NODE_OPTIONS="${NODE_OPTIONS:-} --max-old-space-size=4096"
node ../backend/node_modules/.bin/jest --config config/jest-backend-integration.json --forceExit "$@"
```

V8 上限提到 4 GB。**仅止血，trend 不解决** —— 下次新增 ~10 个 suite 又会撞 4 GB。

为什么放脚本里而不是 runner 机器配置：
- 跟着脚本走，谁调谁受益（CI + 本地 + 任何新 runner）
- 改动可在 git review，可回滚，注释指向 issue #260
- 范围仅这一个 jest 调用，不污染 runner 上其他 node 进程
- 治根方案上线时一行删除即可，不会有"runner 上还有个隐式 export"漏拆

## 治根（issue #260，2026-05-11 已落地方案 3）

选定**方案 3（脚本层 batch by module）**：testing/scripts/run-backend-integration.sh
按 12 个 module 目录依次起 jest，每个模块独立 Node 进程，进程退出 heap 由内核
自动回收。模块内仍 --runInBand（共享 test DB 的 cleanup race 规避）；不再依赖
`NODE_OPTIONS=--max-old-space-size`。

方案 1（每 worker 独立 schema）被认为工作量与风险不成比例——issue 列的
三个阻塞点（共享容器 / cleanup LIKE 't_%' race / DROP DATABASE 互踩）都要
解决才能真并行；方案 3 通过"进程级隔离 by shell" 跨过这三个阻塞点，零数据
隔离改动。

详见：.learnings/2026-05-11-jest-batch-by-module.md。

## 顺带修

`backend/src/core/workflow/temporal/temporal.service.ts` 加测试环境跳过逻辑（与 `engines/approval/temporal/temporal.service.ts` 已有的判断对齐），消除 35+ 次 `Failed to connect to Temporal` 错误日志噪声。两个同名 service 之前**行为不一致**，是个小坑：项目里有同名 service 时，改一个记得查另一个。

## 适用范围

- 任何用 `jest --runInBand` 跑大量 integration / e2e suite 的 Node 项目
- suite 数量增长到 30+ 后必现，CI 报 OOM 时第一反应不该是"加 runner 内存"，而是查 jest 是否串行单进程
- 长期方案永远是 worker 隔离，不是无限提 heap 上限

## 关联

- 失败 run: http://43.130.59.228/FFAIWorkspace/workspace/actions/runs/1329/jobs/5
- 治根工单: issue #260
- Temporal noisy retry 同款发现: 本 commit 第二个改动
