# Import L1 集成测试：per-user in-flight 限制必须靠 `beforeEach` 隔离

**日期**：2026-05-18
**场景**：robot-manager Excel 导入工具 M1 第一次跑 L1 集成测试，11 个用例 7 个失败，
错误五花八门——sad 4 报 `IMPORT_CONCURRENT_BATCH`、sad 5 / sad 6 / sad 7 / happy 2
全是 `res.body.data` undefined 看似不同的 bug。

## 根因

backend 有一条**业务保护**：同一 user 同一 org 同时只允许有 1 个 in-flight ImportBatch
（status ∈ {PENDING, VALIDATING, VALIDATED, IMPORTING}）。controller `preview` 在创建
新 batch 前调 `ImportBatchService.ensureNoOtherInFlight(userId, orgId)`，若已存在则抛
`IMPORT_CONCURRENT_BATCH`。

**sad 用例的副作用**：
- `sad 3 / sad 4 / sad 5` 用 `errorRows > 0` 的 buffer 测 preview 校验 → backend 仍然
  **创建 VALIDATED batch**（带错误明细），让用户后续看错误报告 / 修后重传
- 测试代码**没**主动清这个 batch（因为本测试不 confirm 它）
- 下一个用例进来 `ensureNoOtherInFlight` 拦截 → 全报 `IMPORT_CONCURRENT_BATCH`

测试代码以为响应是 `{success:true, data:{batchId,...}}`，但拿到的是
`{success:false, error:{code:"IMPORT_CONCURRENT_BATCH"}}`，所以 `res.body.data.summary`
统一 undefined——**症状跟根因隔了一层壳**。

## 解决

```typescript
// 每个测试前清掉 user 的所有 ImportBatch
beforeEach(async () => {
  await prisma.importBatch.deleteMany({ where: { createdById: userId } });
});
```

这里**不能**只在 `afterAll` 清——sad 用例之间的 batch 累积才是问题。

## 为什么不直接禁掉 per-user in-flight 限制

这条限制是**产品行为**（防用户同时上传多个 Excel 互相冲突），不应该为测试改 backend。
测试要适配产品行为，不是反过来。

## 通用模式：业务"per-user 限 1 个"资源的 L1 测试

| 限制类型 | 测试做法 |
|---|---|
| per-user 限 N 个 in-flight（导入/异步任务/上传） | `beforeEach` 清掉该 user 的同类记录 |
| per-org 限 N 个 | 每个测试用独立 org id（`Date.now()` + 随机） |
| 全局唯一约束（code/name unique） | 每个测试用随机标识 |

CLAUDE.md 已有「测试数据隔离 — 用随机标识，禁止硬编码」，但**漏说**「per-user 异步资源
要 beforeEach 清」。这条建议加进 `.agents/skills/test-backend/references/testing-standards.md`。

## supertest 处理 binary 响应

附加发现：本测试还有一个用例下载 Excel 模板，`expect(res.body.length).toBeGreaterThan(1000)`
失败——supertest 默认把响应当 string 处理。binary endpoint 必须：

```typescript
const res = await request(app.getHttpServer())
  .get('/api/v1/.../template')
  .set('Authorization', `Bearer ${token}`)
  .buffer(true)
  .parse((response, cb) => {
    const chunks: Buffer[] = [];
    response.on('data', (c: Buffer) => chunks.push(c));
    response.on('end', () => cb(null, Buffer.concat(chunks)));
  });
expect((res.body as Buffer).length).toBeGreaterThan(1000);
```

`Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet`
（xlsx）/ `application/pdf` / `image/*` 等 binary 响应一律这样处理。

## 何时遇到

- 新写一组 L1 集成测试，业务带「per-user 限 1 个 in-flight」语义（异步任务 / 导入 /
  导出 / 长时间运行操作的占位记录）
- 测试下载 Excel / PDF / 图片等 binary endpoint
- L0 contract check 不报但 L1 集成跑下来 7/11 失败，错误码看似离散其实同源

## 防御措施

1. 写 L1 测试前先 grep `ensureNoOther` / `findInFlight` / `unique constraint` 等"业务
   单例"信号，预判 beforeEach 清理范围
2. binary 响应统一封装一个 `downloadBinary(path)` helper（避免每个测试重抄 `.parse(...)`）
3. 集成测试报告里如果出现 `error.code` 看似随机的失败，**先怀疑测试间相互污染**，再怀
   疑 backend
