---
name: test-main
description: >
  当用户要求"跑一遍完整测试"、"全面验证模块"、"上线前测试"时使用。
  按层级顺序编排测试（L0→L0a/L0b→L0c→L1→L1c→L2→L3），
  前一层不过则阻断后一层，最终输出汇总报告。
  触发短语：完整测试、全面测试、全量验证、跑所有测试、上线前验证、
  full test、run all tests、quality gate、验证模块。
hooks:
  PreToolUse:
    - matcher: "mcp__plugin_playwright.*"
      hooks:
        - type: command
          command: "echo '⛔ test-main 是编排器，禁止直接操作 Playwright MCP。请通过 Agent 工具调用 test-frontend skill 执行 E2E 测试。' >&2 && exit 2"
---

# 测试总调度技能（test-main）

## 定位

三层测试的总编排器。负责：
1. 确认每层驱动文档是否就绪
2. **确认测试环境就绪（数据库 + 种子 + 测试栈）**
3. 按序执行每层，前一层不过则阻断后续
4. 需要子 skill 时调用 `test-backend`（L1）或 `test-frontend`（L2）
5. 汇总所有层的结果输出综合报告

**不做的事**：不自己写测试代码、不自己操作 MCP——这些由子 skill 负责。

## 测试环境架构

全流程测试使用**独立测试环境**，与开发环境隔离：

| 服务 | 开发环境 | 测试环境 |
|------|---------|---------|
| 前端 | localhost:3000 | localhost:3010 |
| 后端 | localhost:3001 | localhost:3011 |
| 数据库 | ffoa-dev-postgres:3002 | ffoa-test-postgres:35432 |
| 数据库名 | ffws_dev | ffws_integration_test |

**测试账号**（种子自动创建）：

| 账号 | 密码 | 角色 | 用途 |
|------|------|------|------|
| `itadmin` | `Admin@2024` | Administrator | 管理操作、周期配置 |
| `test.dev1` | `Test1234!` | Employee | KPI 填写、自评 |
| `test.lead1` | `Test1234!` | Leader | 团队审批、经理评分 |
| `test.mgr1` | `Test1234!` | DepartmentManager | 部门管理 |
| `test.robot1` | `Test1234!` | Employee（其他组织） | 隔离验证 |

## 测试脚本清单

| 脚本 | 运行方式 | 用途 |
|------|---------|------|
| `scripts/contract-check.ts` | `cd testing && npm run test:contract:module {module}` | L0a/L0b 前后端契约校验 |
| `scripts/response-snapshot-check.ts` | `cd testing && npm run test:snapshot:module {module}` | L0c 响应快照对比 |
| `scripts/data-quality-check.ts` | `cd testing && npm run test:data-quality:module {module}` | L1c 数据质量校验 |
| `scripts/run-backend-integration.sh` | `bash testing/scripts/run-backend-integration.sh [test-path] [--runInBand]` | L1 集成测试执行器 |
| `scripts/start-e2e-stack.sh` | `bash testing/scripts/start-e2e-stack.sh --daemon` | 启动测试栈（后端 3011 + 前端 3010） |
| `scripts/stop-e2e-stack.sh` | `bash testing/scripts/stop-e2e-stack.sh` | 停止测试栈 |

> 所有脚本支持 `--module` 参数，不带时默认 `performance`。

## 工作流

### Step 0: 确定范围与驱动文档就绪检查

读取模块文档目录 `docs/modules/{module}/`，逐层检查驱动文档是否存在：

| 层 | 驱动文档 | 缺失时的处理 |
|----|----------|-------------|
| L0 | 前端 `page.tsx` + `navigation.ts` + `services/api/*.ts` | 从代码扫描，不阻断 |
| L0a/L0b | 前端 API 类型 + 后端 DTO + `07-api.md` | **阻断** |
| L0c | 前端 API 类型 | 可跳过，标记为未执行 |
| L1a | `07-api.md` | **阻断** |
| L1b | `01-prd.md` + `04-state-machine.md` | 无状态机时范围缩小但不阻断 |
| L1c | `06-data-model.md` + `prisma/schema/` + `prisma/seeds/` | **不可跳过** |
| L2 | `10-e2e-test-spec.md` + `09-test-scenarios.md` | 缺 10 则调用 `docs-main` skill 创建 |
| L3 | `05-ui-interaction-spec.md` | 输出验收清单供人工使用 |

**同时检查**：
- 前端 API 文件位置（`services/api/{module}.ts` 或 `_lib/api/index.ts`）
- 后端集成测试是否存在（`testing/backend/integration/{module}/`）
- 契约/快照/数据质量脚本是否支持当前模块

输出：**驱动文档就绪矩阵**（每层 ✅/❌/⚠️）

### Step 1: 测试环境就绪检查 ⛔ 阻断级

**目标**：确保测试数据库、种子数据、测试栈全部就绪后再开始测试。环境不就绪 → 后面全白跑。

#### 1.1 测试数据库启动 + Schema 迁移

```bash
# lib-test-db.sh 会自动处理：启动容器 → 等待就绪 → 迁移 schema → 执行种子
source testing/scripts/lib-test-db.sh
ensure_test_db
reset_test_db_schema
```

**验证**：
```bash
docker exec ffoa-test-postgres pg_isready -U ffws_test -d ffws_integration_test
```

**失败处理**：Docker 未运行或端口被占 → **阻断**，提示用户修复。

#### 1.2 基础种子数据检查

标准种子（`prisma/seed.ts`）会创建权限、角色、组织、测试用户。验证关键数据：

```sql
-- 1. 组织存在
SELECT count(*) FROM corp_hr.organizations;  -- 必须 > 0

-- 2. 测试用户存在
SELECT username FROM platform_iam.users
WHERE username IN ('itadmin', 'test.dev1', 'test.lead1', 'test.mgr1', 'test.robot1');

-- 3. 模块权限存在（注意：permissions 表无 code 列，是 resource + action 两列）
SELECT count(*) FROM platform_iam.permissions WHERE resource = '{module-prefix}';
```

任一为 0 → 重新执行种子：`cd backend && npx ts-node prisma/seed.ts`

#### 1.3 模块专属种子数据检查

不同模块需要不同的业务数据才能完整测试。**核心实体为空 = L0c 只能验壳层、L2 跑不起来**。

**检查方式**：用 curl 或 SQL 查询模块核心列表端点，确认 `total > 0`。

| 模块 | 核心实体检查 | 种子文件 |
|------|------------|---------|
| performance | 等级配置、绩效周期（至少有 GOAL_SETTING + COMPLETED 状态各一个） | `performance-demo-seed.sql` |
| asset-management | 分类、状态标签、资产 | `asset-mgmt-seed.ts`（如有） |
| organization | 组织、部门、用户 | `org-seed.ts`（标准种子已覆盖） |

**核心实体为空时**：
1. 检查是否存在种子文件：`backend/prisma/seeds/{module}*`
2. **存在但未执行** → 执行种子文件
   ```bash
   # SQL 种子
   docker exec -i ffoa-test-postgres psql -U ffws_test -d ffws_integration_test < backend/prisma/seeds/{file}.sql
   # TS 种子
   cd backend && DATABASE_URL="${TEST_DATABASE_URL}" npx ts-node prisma/seeds/{file}.ts
   ```
3. **不存在** → **调用 `test-backend` skill 创建模块种子文件**（包含基础配置数据 + 至少 3 条核心业务实体 + FK 引用完整），创建后执行
4. 再次验证 total > 0

#### 1.4 测试后端启动（L0c/L1c 使用）

```bash
# 启动测试后端（端口 3011，连接测试数据库）
cd backend && DATABASE_URL="${TEST_DATABASE_URL}" BACKEND_PORT=3011 NODE_ENV=test \
  npx ts-node -r tsconfig-paths/register src/main.ts &
```

**验证**：`curl -sf http://localhost:3011/api/v1/health` 返回 200。

**冲突处理**：端口 3011 已被占用 → 先杀占用进程或提示用户。

#### 1.5 测试前端启动（仅 L2 需要时）

```bash
bash testing/scripts/start-e2e-stack.sh --daemon
```

如果因锁文件或端口冲突失败 → 仅标记 L2 为"环境未就绪"，不阻断 L0~L1c。

#### 环境就绪矩阵

输出格式：

| 检查项 | 状态 | 详情 |
|--------|------|------|
| 测试数据库 | ✅/❌ | ffoa-test-postgres:35432 |
| Schema 迁移 | ✅/❌ | prisma db push |
| 基础种子（权限/角色/组织/用户） | ✅/❌ | N 个组织, M 个用户 |
| 模块种子（业务数据） | ✅/⚠️/❌ | 核心实体 total = X |
| 测试后端（3011） | ✅/❌ | HTTP 200 |
| 测试前端（3010） | ✅/⚠️ | 仅 L2 需要 |

**阻断规则**：数据库/Schema/基础种子 任一 ❌ → **阻断全流程**。模块种子 ⚠️ → 继续但标注覆盖受限。

### Step 2: L0 — 页面清点与测试范围

扫描前端代码，确定待测模块的可达页面和 API 端点：

```
1. 扫描 frontend/src/app/(modules)/{module}/ 下所有 page.tsx
2. 提取前端 API 文件中的所有 API 调用（路径 + 方法 + 参数类型）
3. 对照 navigation.ts 确认可达入口
4. 输出：页面清单 + API 端点清单（后续各层的测试范围基准）
```

### Step 3: L0a/L0b — 契约校验

**优先使用脚本**：
```bash
cd testing && npm run test:contract:module {module}
```

**脚本报错或需补充时**：AI 手动执行等效检查：
1. 读取所有后端 DTO 文件（`backend/src/modules/{module}/dto/*.dto.ts`）
2. 读取前端 API 类型定义（TypeScript interface + API 调用参数）
3. 读取 `07-api.md` 中的字段定义
4. 逐个对比

**L0a（请求契约）**：Create DTO 必填字段 vs 前端调用参数
**L0b（响应契约）**：后端返回字段 vs 前端 TypeScript interface

**阻断规则**：结构性不匹配 → 阻断。可选字段缺失 → 不阻断。

### Step 4: L0c — 响应快照校验

**关键变化**：L0c **打测试后端（3011）**，不打开发后端（3001）。

```bash
# 指定测试后端地址
cd testing && API_BASE_URL=http://localhost:3011/api/v1 npm run test:snapshot:module {module}
```

如果脚本不支持 `API_BASE_URL`，手动 curl 测试后端：
```bash
TOKEN=$(curl -s http://localhost:3011/api/v1/auth/login \
  -H 'Content-Type: application/json' \
  -d '{"username":"itadmin","password":"Admin@2024"}' | jq -r '.data.accessToken')
curl -s http://localhost:3011/api/v1/{module}/{endpoint} -H "Authorization: Bearer $TOKEN"
```

**验证重点**：
- 分页壳层字段完整性
- items 中的字段名与前端 interface 一致（需要模块种子数据支撑）
- 如果 Step 1.3 标记模块种子为 ⚠️，在报告中注明「仅壳层验证」

**阻断规则**：不阻断，但记录差异。

### Step 5: L1 — 后端集成测试

#### 5.1 覆盖率差距分析

1. 从 Step 2 获取**全量端点清单**（扫描 `backend/src/modules/{module}/controllers/*.ts` 中的 `@Get/@Post/@Put/@Delete` 路由装饰器）
2. 扫描现有集成测试提取已覆盖端点（grep `request(app).get\|post\|put\|delete` 或 `request(app.getHttpServer())` 中的路径）
3. 逐个对比，计算覆盖率 = 已覆盖端点 / 全量端点
4. 输出**未覆盖端点清单**（传给 test-backend skill 作为补充依据）

#### 5.2 路径选择

| 情况 | 处理 |
|------|------|
| 无集成测试 | 调用 `test-backend` skill 创建 |
| 覆盖率 < 90% | 调用 `test-backend` skill 补充 |
| 覆盖率 ≥ 90% | 直接执行 |

#### 5.3 执行

```bash
# 复用 Step 1 已启动的测试数据库
bash testing/scripts/run-backend-integration.sh testing/backend/integration/{module} --runInBand
```

**阻断规则**：覆盖率 < 90% → 阻断。失败率 > 20% → 阻断 L2。

#### 5.4 验证模块种子数据完整

L1 执行后验证模块种子数据是否仍然存在（`cleanupAllData` 已改为只清理测试数据，保护种子）：

```sql
SELECT count(*) FROM platform_performance.performance_cycle;  -- 应与 Step 1.3 一致
```

如果种子被意外清空 → 重新加载：
```bash
docker exec -i ffoa-test-postgres psql -U ffws_test -d ffws_integration_test \
  < backend/prisma/seeds/{module}-demo-seed.sql
```

### Step 6: L1c — 数据质量校验

**优先使用脚本**：
```bash
cd testing && npm run test:data-quality:module {module}
```

**脚本报错或需补充时**：AI 通过 SQL 查询测试数据库执行等效检查：

```sql
-- 6.1 权限种子检查（⛔ 阻断级）
SELECT resource || ':' || action AS permission_code
FROM platform_iam.permissions WHERE resource = '{module-prefix}'
ORDER BY action;

-- 6.2 FK 约束覆盖
SELECT tc.table_name, kcu.column_name, ccu.table_name AS fk_table
FROM information_schema.table_constraints tc
JOIN information_schema.key_column_usage kcu ON tc.constraint_name = kcu.constraint_name
JOIN information_schema.constraint_column_usage ccu ON ccu.constraint_name = tc.constraint_name
WHERE tc.constraint_type = 'FOREIGN KEY' AND tc.table_schema = '{schema_name}';

-- 6.3 枚举一致性（与前端类型对比）
SELECT t.typname, e.enumlabel
FROM pg_type t JOIN pg_enum e ON t.oid = e.enumtypid
WHERE t.typname LIKE '{Module}%'
ORDER BY t.typname, e.enumsortorder;

-- 6.4 孤儿记录检查（对所有关联表验证 FK 引用有效）
```

**阻断规则**：权限码缺失 → **阻断**。核心配置表空 → **阻断**。孤儿/枚举 → 不阻断。

### Step 7: L2 — 前后端连通性（MCP E2E）

#### ⛔ MANDATORY-CHECKPOINT（L2 执行前必须完成）

在调用 test-frontend skill 前，必须输出以下清单并逐项确认，任一项为空则不得继续：

```
L2 门控检查：
- [ ] 全量流程清单：___（从 10-e2e-test-spec.md 列出所有流程编号、名称和优先级）
- [ ] 本次开发范围清单：___（从 git diff 提取本次新增/修改的页面、组件、按钮、操作、API 端点；不依赖 10-e2e-test-spec.md 是否列全，规格未覆盖的新按钮必须在 L2 补测）
- [ ] 本次执行范围：全量测试 → P0 + P1 + P2 全部执行；快捷模式"跑 E2E" → 仅 P0
- [ ] 将使用的角色：___（必须 ≥ 3 个，含 itadmin + test.dev1 + test.lead1）
- [ ] 组织隔离验证角色：___（必须含 test.robot1，验证跨组织不可见）
- [ ] i18n 验证 locale：___（至少 zh-CN + en-US 两套，切换后对比关键文案）
- [ ] 遇到阻断时策略：修复 bug → 从失败层重跑（禁止用 API 绕过前端 bug）
- [ ] E2E 执行方式：通过 Agent 工具调用 test-frontend skill（禁止自己操作 MCP）
```

**前置条件**：
- L1 通过率 ≥ 80%
- L1c 无阻断级问题
- Step 1.5 测试前端已启动（3010）

**如果测试前端未启动**：尝试重新启动。Docker 方式：
```bash
docker rm -f ffoa-test-frontend 2>/dev/null
docker run -d --name ffoa-test-frontend \
  --add-host=host.docker.internal:host-gateway \
  -p 3010:3000 \
  -v "$(pwd)/frontend:/app" \
  -v "$(pwd)/frontend/node_modules:/app/node_modules" \
  -v ffoa-test-frontend-next:/app/.next \
  -w /app \
  -e "NEXT_PUBLIC_API_URL=http://localhost:3011/api/v1" \
  -e "NEXT_PUBLIC_API_BASE_URL=http://host.docker.internal:3011" \
  node:20 sh -c "npx next dev -p 3000"
```
仍然失败 → 标记 L2 为"环境未就绪"，输出报告。

#### 7.1 执行方式：必须调用 `test-frontend` skill

⛔ **禁止在 test-main 中自己写 MCP 代码做 E2E**。test-main 是编排器，不操作浏览器。

- 有 `10-e2e-test-spec.md` → **调用 `test-frontend` skill**，传入测试前端地址 `http://localhost:3010`
- 无规格文档 → 调用 `docs-main` skill 创建后再调用 `test-frontend`

自己打开浏览器检查"页面能不能加载"**不算 L2 通过**。L2 的通过标准是 `10-e2e-test-spec.md` 中定义的流程全部执行并断言。

#### 7.2 通过标准

| 维度 | 全量测试要求 | 快捷"跑 E2E"要求 |
|------|------------|-----------------|
| P0 流程 | 全部执行，每步有断言 | 全部执行 |
| P1 流程 | **全部执行**，每步有断言 | 可跳过，标记为未执行 |
| P2 流程 | **全部执行**，每步有断言 | 可跳过，标记为未执行 |
| **本次开发范围** | **本次 diff 涉及的所有页面/按钮/操作逐项过**（含正常流 + 边界 + 空数据 + 无权限态），不依赖规格文档是否列全 | 同左（新增/修改的必测） |
| 多角色 | 至少 itadmin + test.dev1 + test.lead1 三个角色 |
| 状态流转 | 至少验证一个完整周期生命周期（DRAFT → COMPLETED） |
| 组织隔离 | test.robot1 看不到其他组织数据 |
| **i18n 国际化** | **切 zh-CN ↔ en-US 两套 locale**，验证：(1) 关键文案无硬编码中文/英文（切换后应全部切换），(2) 控制台无 `missing key` warning，(3) 日期/数字按 locale 格式化（中文 2026年4月22日 / 英文 Apr 22, 2026） | 同左 |
| 0 console errors | 全流程无 JS 报错（含 i18n missing key warning） |

#### 7.3 MCP 浏览器指向

`http://localhost:3010`（测试前端 Docker 容器，不是开发前端 3000）

**阻断规则**：P0 流程有阻断性失败 → 标记为未通过。

### Step 8: L3 — 人工验收清单

不自动执行。输出验收清单供用户手动检查：

```
- [ ] 页面布局与设计稿一致
- [ ] 空数据状态显示正确
- [ ] 多组织切换后数据刷新
- [ ] 极端数据展示正常
- [ ] 管理菜单对非管理员不可见
- [ ] 本次开发涉及的每个按钮/操作手工点一遍，无异常
- [ ] 切换语言（zh-CN ↔ en-US）后页面文案完整一致，无硬编码、无残留、无截断
- [ ] 日期/数字/货币按 locale 格式化（避免直接显示 ISO string 或 Decimal）
```

### Step 9: 输出综合报告 + 清理

报告路径：`testing/reports/{module}-{YYYY-MM-DD}-full-test-report.md`

```markdown
# {模块名} 全量测试报告

## 概要
- 日期 / 分支 / 环境
- 测试范围：{页面数}个页面、{端点数}个 API 端点
- 测试环境：测试后端 3011 / 测试前端 3010 / 测试数据库 35432

## 环境就绪矩阵
| 检查项 | 状态 |
|--------|------|
| ... | ... |

## 三层执行结果
| 层 | 状态 | 详情 |
|----|------|------|
| L0 | ✅ | ... |
| L0a/L0b | ✅/❌ | ... |
| L0c | ✅/⚠️ | ... |
| L1 | ✅/❌ | ... |
| L1c | ✅/❌ | ... |
| L2 | ✅/❌ | ... |
| L3 | 待执行 | ... |

## 阻断问题
## 非阻断问题
## 子报告链接
```

**清理**（可选）：
```bash
bash testing/scripts/stop-e2e-stack.sh  # 停止测试栈
# 测试数据库保留（下次复用，避免重复迁移）
```

## 快捷模式

| 指令 | 执行范围 | 环境需求 |
|------|---------|---------|
| "跑完整测试" / "全量测试" | Step 0 → Step 9 全部 | 测试数据库 + 测试栈 |
| "跑后端测试" | L0a/L0b + L1 + L1c | 仅测试数据库 |
| "跑契约校验" | L0a/L0b 只 | 无（静态分析） |
| "跑 E2E" | L2 只（前提：L1 已通过） | 测试数据库 + 测试栈 |
| "出验收清单" | L3 只 | 无 |

## 核心规则

1. **环境先行** — 测试数据库、种子数据、测试栈必须在执行任何需要数据的测试之前就绪。环境不就绪 → 后面全白跑
2. **按序执行，逐层阻断** — 不跳层、不并行
3. **驱动文档优先** — 缺文档的层先创建文档再执行，不跳过、不瞎测
4. **脚本优先，AI 兜底** — L0a/L0b、L0c、L1c 优先使用自动化脚本。脚本报错时 AI 手动补充
5. **L1 先评估覆盖率再执行** — 无测试则创建；覆盖不足则先补全。绝不带着缺口直接跑
6. **种子数据先行** — L0c 和 L2 依赖真实数据。核心实体为空时先执行种子，不在空库上跑然后标「无数据跳过」
7. **L0c 打测试后端** — 使用 3011 端口的测试后端（有种子数据），不打 3001 开发后端
8. **L2 无规格则创建** — 缺 `10-e2e-test-spec.md` 时调用 `docs-main` skill 创建
9. **修复后重跑** — 从失败层重新开始，不需要从头跑
10. **报告必输出** — 每次执行必须产出综合报告，即使中途阻断
11. **L1 不重建 schema** — 集成测试自己管理数据生命周期（beforeEach 创建、afterEach 清理），不依赖也不破坏种子数据。`run-backend-integration.sh` 仅在 schema 不存在时初始化，不每次重建
12. ⛔ **禁止为了通过测试而修改断言** — 详见 `test-backend` skill
13. **本次开发范围必须全覆盖** — L2 不能只跑 10-e2e-test-spec.md 列出的流程；必须从 git diff 提取本次新增/修改的页面/按钮/操作，规格未覆盖的新按钮在 L2 补测。规格漏列不是跳过的借口
14. **i18n 必须双语验证** — L2 每次必须切 zh-CN ↔ en-US 两套 locale，对比关键文案、console missing key、日期/数字格式化。单语通过 ≠ i18n 通过

## 通用测试盲区防护（适用于所有模块）

以下规则源自实际项目中 13 个 Bug 的复盘，均为跨模块通用问题。

### 盲区 1：只用 Admin 测试 → 权限 Bug 全漏

**规则**：L1 集成测试和 L2 E2E 必须覆盖至少 3 个角色（Admin + 普通用户 + 主管/经理）。

**检查点**：
- 普通用户能访问自己的数据（查看自己的 KPI/审批/结果等）
- 普通用户不能访问他人的数据（返回 403）
- 管理菜单/管理功能对普通用户不可见

**反模式**：全部测试用 itadmin → `@RequirePermissions` 装饰器的 Bug 永远发现不了（Admin 跳过所有权限检查）。

### 盲区 2：DTO whitelist 静默丢弃

**规则**：当测试"写操作后数据未变化"时，优先排查 DTO 是否声明了该字段。

NestJS `ValidationPipe({ whitelist: true })` 会**静默丢弃** DTO 中未声明的字段。API 返回 200 但数据没变 — 不报错、不告警、不记录。

**检查方式**：L0a 契约校验时，不仅比较前端传的字段名和 DTO 字段名是否一致，还要检查 **Update/Create DTO 是否包含了前端会传的所有字段**（特别是关联字段如 `dependentUserId`、`assigneeId` 等）。

### 盲区 3：写操作副作用未验证

**规则**：每个状态推进/写操作的测试，除了验证主表状态变化，**必须验证关联表的副作用**。

常见遗漏：
- 推进到某阶段 → 应自动生成关联记录（如 result、assessment）
- 推进到发布阶段 → 关联记录应自动标记为 isPublished
- 设置关联（依赖人/评估人）→ 关联表应有记录
- 删除主记录 → 关联记录应级联软删除

### 盲区 4：种子关系不完整

**规则**：L1c 数据质量检查必须验证测试用户之间的**业务关系链路**，不仅仅是"用户存在"。

需要验证的关系：
- manager 链路（test.dev1 → test.lead1 → itadmin）
- 组织归属（测试用户在正确的组织和部门）
- 角色分配（Employee/Leader/Manager 角色正确）

关系缺失 → 团队审批、经理评分、下属列表等功能空白，但 API 返回 200（空数组），不报错。

### 盲区 5：前端条件过严导致功能隐藏

**规则**：L2 E2E 测试必须验证"完成操作 A 后，功能 B 是否仍然可见"。

常见遗漏：
- 自评完成后 360 评估区域消失（条件绑定了 `!allSelfEvalDone`）
- 提交后编辑按钮消失是正确的，但不相关的功能也消失是 Bug
- 日期/数字字段未格式化（直接显示 ISO string 或 Decimal 对象）

### 盲区 6：i18n 硬编码/缺失 key 单语测试抓不到

**规则**：L2 每次必须切两套 locale（至少 zh-CN + en-US）验证。只跑一套语言 → 硬编码中文/英文 + 缺失 key 全漏。

**检查点**：
- 切换 locale 后，页面关键文案全部切换（无残留中文/英文 = 硬编码）
- 浏览器控制台无 `i18next::translator: missingKey` 或类似 warning
- 日期/数字/货币按 locale 格式化（`2026年4月22日` / `Apr 22, 2026`，不是 `2026-04-22T00:00:00.000Z`）
- 错误提示、表单校验信息、按钮 tooltip、表格列头都要切换

**反模式**：
- 只用默认 locale（通常 zh-CN）跑完就算通过 → 英文用户见到一堆中文
- 文案走 `t('key')` 但 key 没在 locales 文件里声明 → 运行时显示 key 字符串本身
- 日期直接 `{row.createdAt}` 显示 ISO string，没走 formatter

**切换方式**（MCP 操作）：调用用户菜单切语言，或直接改 localStorage / cookie 的 locale 值后刷新。

### 盲区 7：seed upsert drift 导致实际权限 ≠ 文档声明

**规则**：检查角色 × 权限、用户 × 角色等**多对多关联**时，ground truth 是**运行时数据库查询结果**，不是 seed 源码。

背景：seed 用 `rolePermission.upsert` 只保证关系"存在"，不保证"只存在这些"。老版本给 SalesRole 加过 `robot-manager:update`，后来移除时删了 seed 文件但没删 DB 记录，结果 Sales 在数据库里有 5 条权限，seed 文件和 PRD 都说是 4 条。

**实测方法**：
```bash
# 调 /users/me 枚举某角色的真实权限
T=$(curl -s .../auth/login -d '{"username":"sales1",...}' | jq -r .data.accessToken)
curl -s .../users/me -H "Authorization: Bearer $T" | \
  jq '.data.roles[].role.permissions[].permission | select(.resource=="robot-manager")'
```

若和 seed 文件 `permissionKeys` 不一致 → seed 有 drift bug。修复方法：seed 在 upsert 前先 `deleteMany` 本模块该角色的旧关系，再 create 想要的。详见 `.learnings/2026-04-14-seed-upsert-drift.md`。

**这条盲区与盲区 1 配套**：L2 必须覆盖多角色，且每个角色的权限集合必须用 ground truth 而非源码对比。

---

## 📐 Meta-rules（三条核心判断原则）

1. **Ground truth 是运行时，不是源码**
   检查权限/关联关系/API shape 等动态内容时，以**实际 DB 查询 + curl 响应**为准，不以 seed 文件或 interface 源码为准。

2. **静态检查和动态检查必须都跑**
   - 只跑 L0a/L0b 静态契约校验 → 抓不到"DB 与 seed 漂移"和"API 真实响应 shape 错误"
   - 只跑 L1 集成测试 → 抓不到"前端类型与后端响应字段名不一致"
   - 两者正交。砍角度时保留动态层（L1c + L2）> 保留静态层（L0）。

3. **换角度 > 加轮次**
   同一层测试跑两次发现不了新问题。如果时间紧张，宁可每层跑一次，不要某一层跑两次。

---

## ⛔ 铁律（近因锚定 — 必须遵守）

1. **test-main 是编排器，不是执行器** — L1 调用 test-backend skill，L2 调用 test-frontend skill。禁止自己写测试代码或操作 Playwright MCP（已通过 frontmatter hook 硬约束）。
2. **遇到 Bug 修复后重跑，禁止绕过** — 前端 bug 导致 E2E 失败时，先修复 bug，再从失败流程重新执行。禁止用 API/curl 绕过前端操作。
3. **L2 多角色 ≥ 3 + 组织隔离用 test.robot1** — 每次 L2 必须通过 MANDATORY-CHECKPOINT 确认角色覆盖。
