# L1 集成测试 form-management 的可复用模式

**日期**: 2026-05-01
**触发**: PR-2 Bucket B 搭 form-management L1 测试基线时踩了 3 个坑
**适用范围**: 后续给任何含跨表 FK 链的模块写 L1 时

## 1. cleanupByPrefix 在 FK-heavy schemas 下不可靠

### 现象

`cleanupByPrefix(prisma)` 用 `SET session_replication_role = 'replica'` 暂时关 FK 检查再 DELETE，看似能过 form_definitions ← form_instances 这种父子链。但实测：

```
cleanupByPrefix: platform_form.form_definitions.slug DELETE 失败:
  update or delete on table "form_definitions" violates foreign key constraint
  "form_instances_form_definition_id_fkey" on table "form_instances"
```

### 根因

Prisma 的连接池（默认 17 个连接）让 `SET session_replication_role` 这种 session-scoped 设置不可靠：

- `prisma.$executeRawUnsafe('SET session_replication_role = replica')` 在某个 pool connection 上设置
- 紧接着的 `prisma.$executeRawUnsafe('DELETE ...')` 可能拿到**不同的** connection，FK 检查仍然开着

cleanupByPrefix 把 DELETE 失败 catch 住只 `console.warn`，不抛错。表面"过了"实际上 leftover 数据污染下个 test。

### 解决方案

**显式按依赖顺序删，不依赖 session 级 FK 关闭。** 用 prisma 高级 API（自动复用 transaction connection）：

```ts
afterEach(async () => {
  const targetDefs = await prisma.formDefinition.findMany({
    where: { slug: { startsWith: 't_' } },
    select: { id: true },
  });
  const defIds = targetDefs.map((d) => d.id);
  await prisma.$transaction(async (tx) => {
    await tx.formInstance.deleteMany({ where: { businessKey: { startsWith: 't_' } } });
    if (defIds.length > 0) {
      await tx.releaseSnapshot.deleteMany({ where: { formDefinitionId: { in: defIds } } });
      await tx.formVersion.deleteMany({ where: { definitionId: { in: defIds } } });
      await tx.formDefinition.deleteMany({ where: { id: { in: defIds } } });
    }
    await tx.approvalInstance.deleteMany({ where: { workflowId: { startsWith: 't_wf_' } } });
  });
  await cleanupByPrefix(prisma); // 兜底清 users / orgs 等
});
```

`$transaction` 内的所有 op 共用同一连接，FK 顺序删自然成立，**根本不需要 session_replication_role**。

### 进一步建议

可以把 `cleanupByPrefix` 升级为依赖图感知版（先扫 information_schema 拿外键依赖图，按拓扑序删），但工作量大；当前模式（每个测试套件自己写依赖序 cleanup）已经足够。

## 2. createAdminUser 在多次调用下 FK 风险

每次 beforeEach 调 `createAdminUser` 会 upsert TEST_ADMIN_ORG。如果：
- 之前的测试写过 t_ 前缀数据触发 cleanupByPrefix 失败（见上）
- 或 cleanupByPrefix 把 corp_hr 里某些 t_ 前缀行删掉影响 org 关联

会出现 `user_role_rel_organization_id_fkey violated`。

### 缓解

force-reset DB schema 重置一次（`bash scripts/run-backend-integration.sh --force-reset`），或确保 afterEach 的依赖序 cleanup 完整覆盖了所有自己造的数据。

## 3. 不依赖真实 ApprovalInstance 的 form-management 状态机测试

ApprovalInstance schema 字段名容易踩（versionId 而非 processDefinitionKey；workflowRunId 必填等），且需要先创建 ApprovalVersion / ApprovalDefinition。直接造 ApprovalInstance 成本高。

### 模式

测试 form-management 状态机时，**FormInstance.approvalInstanceId 用 fake UUID 占位**，关键的 `approvalService.withdraw` 用 `jest.spyOn` 屏蔽副作用：

```ts
const fakeApprovalId = `00000000-0000-4000-8000-${Date.now().toString().padStart(12, '0').slice(0, 12)}`;
const inst = await createTestFormInstance(prisma, {
  ...,
  approvalInstanceId: fakeApprovalId,
  approvalStatus: 'RUNNING',
});

const { ApprovalService } = require('@/engines/approval/approval.service');
const approvalService = app.get(ApprovalService);
jest.spyOn(approvalService, 'withdraw').mockResolvedValue({
  success: true,
  instanceId: fakeApprovalId,
  status: 'WITHDRAWN',
});
```

这样 form-management 这一侧的状态机改动完全可测，不依赖 Temporal 或 approval engine 数据完整性。

handleApprovalCompleted 回调测试更简单——直接 `app.get(FormApprovalIntegrationService).handleApprovalCompleted(dto)` 调用，连 approvalService spy 都不需要。

## 4. TransformInterceptor 在测试 app 也启用

- `createTestApp()` 同时 enable `TransformInterceptor`（与 main.ts 一致）
- 所以 supertest 的 `res.body` 形如 `{ success: true, data: ... }`
- 测试断言路径 = `res.body.data.<actual response>`
- form-management 的 create 服务返回 `{ formInstance: {...}, processInstance: {...} }`，所以测断言是 `res.body.data.formInstance.status`

容易踩坑：审批服务返回简单 `{ id, status, ... }`，而 form-management 包了一层 formInstance/processInstance；不同模块响应结构不统一。

## 5. POST 端点的 status code 不固定

NestJS POST 默认 201；但有些方法 `@HttpCode(200)` 显式标 200（如 form-management withdraw）。测试时**断言不要硬绑 200 或 201**：

```ts
expect([200, 201]).toContain(res.status);
```

或者去看 controller 装饰器再写精确断言。

## Metadata

- Reproducible: yes（任何含 FK 链 + cleanupByPrefix 的测试套件）
- Related Files:
  - `testing/backend/integration/form-management/_helpers.ts`
  - `testing/backend/integration/form-management/instance-state-machine.api.test.ts`
  - `testing/backend/helpers/cleanup.helper.ts`
- Pull Request: feature/approval-form-followup-batch1
