## [ERR-20260427-013] Prisma strict-relation + 测试 cleanup 关 FK 留孤儿数据 → API 500

**日期**: 2026-04-27
**类别**: Prisma / 测试隔离 / 数据完整性
**严重度**: 高（list endpoint 直接 500，CI 持续失败）

### 问题描述
两个 ai-tools API（`GET /grants` 和 `GET /tool-subjects/:tool`）在 CI backend-integration 测试中持续返回 500：
```
Inconsistent query result: Field role is required to return data, got `null` instead.
Invalid `this.prisma.aIToolGrant.findMany()` invocation
```

### 根因（双重）
**A. 测试 cleanup 制造孤儿数据**
- `testing/backend/helpers/cleanup.helper.ts` 用 `SET session_replication_role='replica'` 关闭 FK 约束（避免逐表清理 50+ FK 引用）
- 删 `roles` 时 FK 关闭，**`ai_tool_grants` 的 ON DELETE CASCADE 不触发**
- cleanup 没显式列 `ai_tool_grants` → 留下指向已删 role 的孤儿 grant
- 开 FK 时不会回滚（关 FK 期间允许违反约束）

**B. Prisma strict-relation 模式放大问题**
- Schema 中 `role Role @relation(...)` 是必填关系（不带 `?`）
- `findMany` + `include: { role }` 时 Prisma 要求 role 必须存在
- 拉到孤儿 grant → role 关联返回 null → 直接抛 PrismaClientUnknownRequestError → 整个 endpoint 500

为什么 PR #145 修不彻底：作者只改了 `listRoleGrantsAggregated`（从 Role 起查回避 include），漏了 `listRoleGrants` 和 `getToolSubjects`。

### 修复（3 层防御）

**第 1 层：cleanup 显式删 ai_tool_grants**
```sql
DELETE FROM platform_iam.ai_tool_grants
  WHERE role_id IN (SELECT id FROM platform_iam.roles WHERE is_built_in = false);
DELETE FROM platform_iam.ai_tool_grants_user
  WHERE user_id IN (SELECT id FROM platform_iam.users WHERE username NOT IN ('itadmin') ...);
```
**关键点**：放在删 roles/users **之前**。FK 关闭时不能依赖 cascade。

**第 2 层：service 层避开 strict-relation**
```ts
// listRoleGrants：不 include 必填关系，让前端按 roleId 单独查 role 详情
// getToolSubjects：从 Role 起查（不是从 Grant 起 include role）
const rolesWithGrant = await this.prisma.role.findMany({
  where: { aiToolGrants: { some: { toolName } } },
  select: { id, name, aiToolGrants: { where: { toolName } } },
});
```
即使未来再出现孤儿（迁移、CDC、并发删除），endpoint 不会 500。

**第 3 层（不需要）**：迁移 SQL 清生产 DB 孤儿
- 生产 DB 上 FK 一直是开的，不可能有孤儿
- 只有测试环境（cleanup 关 FK）会留孤儿
- 跑一次 cleanup helper 修复后会自动清干净，不需要 migration

### 启示
- **任何 cleanup helper 关 FK 后，必须显式 DELETE 所有引用即将删除主表的子表数据**。不能依赖 ON DELETE CASCADE
- **新加 model 后立刻审计 cleanup helper**：项目里 ai-tools (PR #138 加的) 至今没人补 cleanup，所以这个雷潜伏了 2 周才被触发
- **Prisma strict-relation + include 是脆弱组合**：任何"必填关系"被 include 都假设 100% 数据完整性。生产可能 OK，测试环境一旦数据不一致就 500
  - 防御性写法：要么用 `select` + 单独查关联，要么从父表起查（如 Aggregated 模式）
- **修类似 bug 时要 grep 整个文件**：PR #145 只修了一处 `findMany + include role` 用法，但同文件还有 3 处。统一修才能根除

---
