---
date: 2026-05-11
type: pattern
tags: [ci, jest, integration-test, oom, batching, runner-band]
---

# jest 集成测试 OOM 治根方案 3：脚本层 batch by module

## 起因

issue #260：`jest --runInBand` 把 36 个 integration suite 串行塞 1 个 Node 进程，
heap 累积到 ~1879 MB 撞 V8 默认 ~2 GB 上限 OOM（详见 ERR-20260509-001）。
PR #261 临时 `NODE_OPTIONS=--max-old-space-size=4096` 止血，trend 不解决。

## 三方案权衡

| 方案 | 改动面 | 治根度 | 取舍 |
|---|---|---|---|
| 1. schema/DB 按 worker 隔离 | 大（lib-test-db.sh + cleanup.helper.ts + 12 模块 setup + jest globalSetup） | 治根 | 真并行；启动开销 ×worker；sessionId 假隔离需要真随机化 |
| 2. `--maxWorkers=2` + `--workerIdleMemoryLimit` | 小 | 部分 | 仍踩 cleanup race / `LIKE 't_%'` 全库前缀冲突 |
| 3. 脚本层 batch by module | 中（仅 shell + CI） | 治根（heap 维度） | 12 次 jest 启动 ~30-60s 额外开销；零数据隔离改动 |

最终选 3：方案 1 的工作量与风险比方案 3 高一个数量级，cleanup race 是 issue 的"阻塞点
2/3/4"——不解决就并行不了；方案 3 把"36 suite 1 进程"改成"12 个进程，每进程 ≤9 suite"，
heap 在每个 jest 进程退出时由内核自动回收，根本不会累积撞 2 GB。

## 实现要点

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

```bash
# 无参数 → batch by module：扫 testing/backend/integration/*/ 子目录，依次起 jest
# 传具体路径 → 单次 jest 透传（向后兼容 / CI 模块路径变更走这条）
if [[ $# -gt 0 ]]; then
  run_jest_once "$@"
  exit $?
fi

mapfile -t MODULES < <(find "${INTEGRATION_ROOT}" -mindepth 1 -maxdepth 1 -type d -printf '%f\n' | sort)

for module in "${MODULES[@]}"; do
  if ! run_jest_once "${INTEGRATION_ROOT}/${module}"; then
    FAILED_MODULES+=("${module}")  # 不 fail-fast，跑完再统一报失败
  fi
done
```

### 关键设计

1. **模块内仍 `--runInBand`**：单个 module 的 suite 共享 test DB，cleanup 用
   `LIKE 't_%'` 全库前缀扫——并行 worker 写入会被对方 cleanup 误删（issue #260
   阻塞点 2）。batch 模式让 jest 进程之间通过 shell 串行，进程内仍单 worker。
   两层串行 = heap 隔离 + 数据隔离都保住。

2. **不 fail-fast**：一次 batch run 12 个模块，任一失败收集到列表，全跑完再 exit 1。
   CI 上 reviewer 一次看到全部红项，而不是只看到第一个失败。

3. **--force-reset 只触发一次**：脚本顶部按 schema 是否存在判定，batch 模式下
   只在 batch 开始前 reset 一次；12 次 jest 调用共享 schema。CI 也是 `--force-reset`
   传一次。

4. **向后兼容**：传具体路径（CI 的 `TEST_PATHS` 路径）仍单次 jest。两条路径互斥。

5. **拿掉 NODE_OPTIONS=--max-old-space-size**：单模块最大 9 suite（organization），
   线性外推 heap 峰值 ~470 MB << V8 默认 ~2 GB，止血依赖可移除。

## CI 改动

`.gitea/workflows/quality-gates.yml::backend-integration`：

- 公共代码变更：`bash scripts/run-backend-integration.sh --force-reset`（无路径 → batch）
- 模块路径变更：`bash scripts/run-backend-integration.sh --force-reset $TEST_PATHS`（透传）
- 两条路径都删了 `--runInBand`（脚本内部已加，CI 透传冗余 + 误导）

## 副作用

- **耗时 +30-60s**：12 次 jest 启动 × ~3-5s ts-jest 预热。全量 ~5-10min 跑量下可接受。
- **报告每次覆盖**：jest 默认 `testing/reports/jest-results.json` 每次覆盖，但
  grep 全仓没下游消费者，OK。
- **本地用法不变**：`npm run test:backend:org:integration` 仍单模块路径透传。

## 仍未解决（留给方案 1）

- cleanup 用 `LIKE 't_%'` 全库前缀扫——是隐患不是 bug，因为 batch 模式下
  跨模块 cleanup 顺序串行，不会 race。
- `generateTestSessionId` 硬编码 `test_1234567890000_test`——所谓"会话隔离"
  是摆设，代码注释直说依赖 maxWorkers=1。方案 3 不动它。
- DB schema/Redis DB 仍全局共享单实例——方案 1 才需要按 worker 隔离。

## 验证

- 单模块路径透传：`bash testing/scripts/run-backend-integration.sh backend/integration/system` → 1 suite 5 tests 通过，23s。
- 全量 batch（force-reset，unset NODE_OPTIONS）：详见提交描述 / CI 跑量。

## 适用范围

任何 jest 集成测试因"suite 数量 × 单 suite heap"撞 V8 上限 OOM 的项目，且
"数据并发隔离"工作量过大不值得当前做。**进程级隔离 by shell** 是不需要改任何
测试代码的低风险治根路径。

## 关联

- issue: #260
- 临时缓解 PR：#261
- 同主题 learning：ERR-20260509-001
- 沿用容器互斥锁：2026-05-09-ci-flock-shared-test-containers.md
