# 机器人全生命周期管理 - 测试场景矩阵

> **module**: robot-manager
> **doc_type**: TestScenarios
> **status**: Active
> **owner**: FFOA Team
> **upstream_docs**: 01-prd.md, 04-state-machine.md, 10-e2e-test-spec.md, 11-user-guide.md
> **last_verified**: 2026-04-18

本文档是 L1 集成测试 + L2 E2E 的场景总索引。每个场景在左列给出语义名，在右列标注对应测试文件位置或 E2E 编号，避免"同一场景两处跑、某场景没人跑"的盲区。

---

## 一、业务流矩阵（对照 11-user-guide.md 三部分）

### Part 1 · Setup（基础数据配置）

| 业务流 | L1 集成测试 | L2 E2E | 角色 |
|--------|------------|--------|------|
| 创建 Model / SKU / Supplier / Customer / Location CRUD | `describe('Admin Entities')` | E2E-RM-011 / E2E-RM-012 | Admin / RLE / SupplyChain / Sales |
| FieldDef Custom scope 增删改 | `describe('Admin Field Defs · Custom scope')` | E2E-RM-011 步骤 2-3 | Admin / RLE |
| FieldDef System scope（serviceType / locationType） | `describe('Admin Field Defs · System scope')` | E2E-RM-011 步骤 4 | Admin / RLE |
| FieldDef key / code 自动生成规则 | `describe('FieldDef · key auto-gen')` | E2E-RM-011 步骤 2 | Admin |
| 系统配置 ffsn_rule / status_default_location | `describe('Admin Config')` | — | Admin |
| 角色分配（Administrator / RLE / SupplyChain / Sales / Finance） | `describe('Permission · manage:* split')` | E2E-RM-009 / 012 / 014 | 所有 |

### Part 2 · Lifecycle（10 状态机）

| 状态转换 | L1 | L2 |
|---------|----|----|
| ORDERED → IN_TRANSIT | ✅ `Happy path` | E2E-RM-001 |
| IN_TRANSIT → BONDED → IN_STOCK | ✅ | E2E-RM-001 |
| IN_TRANSIT → IN_STOCK（直入库） | `describe('直入库分支')` | — |
| IN_STOCK → RESERVED（Guard: customerId） | ✅ `Guard: customerId` | E2E-RM-001 + 003 |
| RESERVED → SOLD（Guard: salesPrice） | ✅ `Guard: salesPrice` | E2E-RM-001 |
| SOLD → DELIVERED | ✅ `Happy path` | E2E-RM-001 |
| SOLD → CANCELLED | `describe('SOLD 分支: CANCELLED')` | — |
| RESERVED → IN_STOCK（取消预留，清 customer） | `describe('RESERVED → IN_STOCK 清客户')` | — |
| IN_STOCK → REPAIR（库存故障） | ✅ `副作用: → REPAIR ServiceRecord` | — |
| DELIVERED → REPAIR | ✅ | E2E-RM-002 步骤 2 |
| REPAIR → REPAIRED（ServiceRecord 补 completedDate） | `describe('REPAIR → REPAIRED')` | E2E-RM-002 步骤 4 |
| REPAIRED → DELIVERED（归还原客户） | `describe('REPAIRED → DELIVERED')` | E2E-RM-002 步骤 5 |
| REPAIRED → IN_STOCK（重新入库，清 customer） | `describe('REPAIRED → IN_STOCK')` | E2E-RM-002 步骤 6 |
| REPAIRED → CANCELLED（报废） | `describe('REPAIRED → CANCELLED')` | — |
| REPAIR → CANCELLED（无法修复） | `describe('REPAIR → CANCELLED')` | — |
| 任意合法源 → CANCELLED（Guard: remark） | ✅ `Guard: → CANCELLED 必填 remark` | E2E-RM-003 |
| 非法跳跃 ORDERED → DELIVERED | ✅ | E2E-RM-003 |
| 非法跳跃 RESERVED → DELIVERED（必须经 SOLD） | ✅ | — |
| 乐观锁 409 | ✅ `乐观锁` | E2E-RM-006 |

### Part 3 · Daily Ops（日常操作）

| 业务流 | L1 | L2 |
|--------|----|----|
| 列表 / 分页 / 按 modelId/status 过滤 / FFSN 搜索 | ✅ `CRUD 列表` | E2E-RM-005 |
| 详情 join（model/sku/supplier/customer/location） | ✅ | — |
| Section 级编辑（identity/sc/sales/finance/after-sales/compliance） | ✅ `Section update` | E2E-RM-001 步骤 7-8 |
| Section 白名单过滤 | ✅ `PUT sales 白名单过滤 cost` | — |
| 不可变字段（poNumber / salesOrderId） | ✅ | E2E-RM-006 |
| 无效字典 code 拒绝写入 | ✅ | — |
| Bulk status change（部分成功） | ✅ | — |
| 软删除 | ✅ | E2E-RM-007 |
| Excel 导出 | ✅ | E2E-RM-008 |
| Excel 导入 | ✅ | E2E-RM-008 |
| Excel 动态字段模板下载（默认全字段） | ✅ `Excel · template` | E2E-RM-008 步骤 3 |
| Excel 模板按 `?fields=` 过滤可选列 | ✅ `Excel · template fields` | E2E-RM-008 步骤 4 |
| 报表 · inventory | ✅ | E2E-RM-004 |
| 报表 · sales（含 customerBreakdown） | ✅ | E2E-RM-004 |
| 报表 · finance（revenue / cost / margin） | ✅ | E2E-RM-004 |
| Currency 字段持久化 | ✅ `Currency field persistence` | — |
| Currency 格式化（formatMoney + Intl.NumberFormat） | — | E2E-RM-001 步骤 7 |
| Attachment 上传/列表/下载/删除 | ✅ `Attachments` | — |
| 全局搜索（跨 units + archives） | ✅ `Global search` | — |
| Dashboard 概览 | — | E2E-RM-015 |
| 多设备对比 | — | E2E-RM-016 |
| Help 多角色导航 | — | E2E-RM-017 |
| Models/SKUs/Locations Admin CRUD | ✅ | E2E-RM-018 |
| 系统配置页 | ✅ `Admin Config` | E2E-RM-019 |

---

## 二、权限矩阵（5 角色 × 18 权限）

> L1 对每条权限断言 "授权 → 200 / 未授权 → 403"；L2 验证 UI 可见性 + 前端 403 友好提示。

### 5 个角色分别可见 / 可写的范围

| 权限 | Administrator | RLE | SupplyChain | Sales | Finance |
|------|---|---|---|---|---|
| `read` | ✓ | ✓ | ✓ | ✓ | ✓ |
| `create` | ✓ | ✓ | ✓ | ✗ | ✗ |
| `update` | ✓ | ✓ | ✗ | ✗ | ✗ |
| `delete` | ✓ | ✓ | ✗ | ✗ | ✗ |
| `change-status` | ✓ | ✓ | ✓ | ✓ | ✗ |
| `import` | ✓ | ✓ | ✓ | ✗ | ✗ |
| `export` | ✓ | ✓ | ✓ | ✓ | ✓ |
| `write:identity` | ✓ | ✓ | ✓ | ✗ | ✗ |
| `write:supply-chain` | ✓ | ✓ | ✓ | ✗ | ✗ |
| `write:sales` | ✓ | ✓ | ✗ | ✓ | ✗ |
| `write:finance` | ✓ | ✓ | ✗ | ✗ | ✓ |
| `write:after-sales` | ✓ | ✓ | ✗ | ✗ | ✗ |
| `write:compliance` | ✓ | ✓ | ✗ | ✗ | ✗ |
| `manage:fields` | ✓ | ✓ | ✗ | ✗ | ✗ |
| `manage:models` | ✓ | ✓ | ✗ | ✗ | ✗ |
| `manage:suppliers` | ✓ | ✓ | ✓ | ✗ | ✗ |
| `manage:customers` | ✓ | ✓ | ✗ | ✓ | ✗ |
| `manage:locations` | ✓ | ✓ | ✓ | ✓ | ✗ |

### L1 权限测试组织

新增 `describe('Permission · manage:* split')` 覆盖：

1. SupplyChain 账号：
   - POST `/admin/suppliers` → 201
   - POST `/admin/customers` → 403
   - POST `/admin/models` → 403
   - POST `/admin/locations` → 201
2. Sales 账号：
   - POST `/admin/customers` → 201
   - POST `/admin/suppliers` → 403
   - POST `/admin/locations` → 201
3. Finance 账号：
   - POST `/admin/suppliers` → 403
   - POST `/admin/customers` → 403
   - POST `/admin/locations` → 403
   - GET `/reports/finance` → 200
4. Administrator ≠ Admin 跳权限（盲区 #1）：
   - Admin 是平台根权限，非 RobotManager* 角色 → 必须分别测

---

## 三、错误码矩阵（RobotError 枚举）

| Error Code | 触发点 | L1 断言 | L2 断言 |
|------------|--------|---------|---------|
| `ROBOT_VERSION_CONFLICT` | 乐观锁失败 | ✅ `乐观锁` | — |
| `ROBOT_MUST_BIND_CUSTOMER_BEFORE_RESERVE` | IN_STOCK → RESERVED 无 customer | ✅ | E2E-RM-013 |
| `ROBOT_MUST_SET_SALES_PRICE_BEFORE_SOLD` | RESERVED → SOLD 无售价 | ✅ | — |
| `ROBOT_CUSTOMER_ID_REQUIRED_FOR_RETURN` | REPAIRED → DELIVERED 无 customer | ✅ | — |
| `ROBOT_SALES_PRICE_REQUIRED_FOR_RETURN` | REPAIRED → DELIVERED 无售价 | ✅ | — |
| `ROBOT_REMARK_REQUIRED_FOR_CANCEL` | 任意 → CANCELLED 无 remark | ✅ | — |
| `ROBOT_PO_NUMBER_IMMUTABLE` | poNumber 二次修改 | ✅ | E2E-RM-006 |
| `ROBOT_SALES_ORDER_ID_IMMUTABLE` | salesOrderId 二次修改 | ✅ | — |
| `ROBOT_EXCEL_MISSING_SHEET` | Excel 无 Sheet | 防御性代码（xlsx 库不允许构造 sheet-less workbook，真实场景不可触发） | — |
| `ROBOT_EXCEL_MISSING_ROWS` | Excel 空行 | ✅ | — |
| `ROBOT_FIELD_KEY_OR_LABEL_EN_REQUIRED` | FieldDef 无 key/labelEn | ✅ | — |
| `ROBOT_FIELD_KEY_GENERATION_FAILED` | labelEn 只含符号 | ✅ | — |
| `ROBOT_FIELD_KEY_MUST_START_WITH_LETTER` | key 以数字开头 | ✅ | — |
| `ROBOT_FIELD_GROUP_REQUIRED_FOR_UNIT_SCOPE` | scope=unit 无 group | ✅ | — |
| `ROBOT_FIELD_OPTIONS_REQUIRED_FOR_SELECT` | type=select 无 options | ✅ | — |
| `ROBOT_FIELD_OPTION_REQUIRES_CODE_AND_LABEL` | options 缺 code 或 label | ✅ | — |
| `ROBOT_FIELD_OPTIONS_ONLY_FOR_SELECT` | type≠select 提供 options | ✅ | — |
| `ROBOT_ATTACHMENT_NO_FILE` | 上传无文件 | ✅ | — |
| `ROBOT_ATTACHMENT_TOO_LARGE` | 上传超 50MB | 未断言（成本/时间） | — |

> 每个新增 L1 断言检查 `response.body.error.code` 精确等于对应枚举值。

---

## 四、数据质量矩阵（L1c）

| 检查项 | 方式 |
|--------|------|
| 18 条权限种子存在 | `SELECT resource, action FROM platform_iam.permissions WHERE resource='robot-manager'` |
| 5 个角色存在 + 无 legacy | 查 `LEGACY_ROLE_CODES` 是否已清 |
| 角色权限分配 = seed 声明（防盲区 #6） | 每角色枚举 `rolePermission` 与 `ROLE_DEFS.permissionKeys` 对比 |
| 32 条 unit FieldDef 种子存在 | `SELECT count(*) WHERE scope='unit'` |
| 2 条系统字典 (serviceType / locationType) | `SELECT * WHERE scope IN ('service_record','location')` |
| FK 约束无孤儿（RobotUnit → Model/Sku/Supplier/Customer/Location） | join 查 null |
| RobotStatus 枚举含 10 个值（含 SOLD / REPAIRED） | `pg_enum` |
| `@@unique([scope, key])` 已施加 | `pg_index` |
| `RobotOption` 表已移除（v5） | `information_schema.tables` |

---

## 五、契约矩阵（L0a/L0b）

| 接口 | 前端类型 | 后端 DTO | L0a | L0b |
|------|---------|----------|-----|-----|
| `GET /` 列表 | `Paginated<RobotUnit>` | `RobotUnitResponseDto` | ✅ | ✅ |
| `POST /` 创建 | `CreateRobotUnitInput` | `CreateRobotUnitDto` | ✅ | ✅ |
| `PUT /:id` / `PUT /:id/:section` | `UpdateRobotUnitInput` | `UpdateRobotUnitDto` + 6 section DTOs | ✅ | ✅ |
| `POST /:id/status` | `ChangeStatusInput` | `ChangeStatusDto` | ✅ | ✅ |
| `/admin/field-defs` | `RobotFieldDef` with scope | `RobotFieldDefDto` | ✅ | ✅ |
| `/admin/models` /skus/suppliers/customers/locations | 各 entity type | 各 DTO | ✅ | ✅ |
| `/excel/import` / `/excel/export` | `ImportResult` | `ImportResultDto` | ✅ | ✅ |
| `/reports/*` | `InventoryReport` / `SalesReport` / `FinanceReport` | `*ReportDto` | ✅ | ✅ |

---

## 六、兜底矩阵（与 docs/standards 对齐）

| test-main 盲区 | 对应场景 |
|----------------|---------|
| #1 只用 Admin 测 | E2E-RM-009 / 012 / 014 + L1 `Permission · manage:* split` |
| #2 DTO whitelist 丢字段 | L0a 契约校验 + L1 `Section 白名单过滤` |
| #3 写操作副作用未验证 | L1 `副作用: → REPAIR 创建 ServiceRecord` + 新增 `REPAIR → REPAIRED 补 completedDate` |
| #4 种子关系不完整 | L1c 数据质量矩阵 |
| #5 前端条件过严隐藏 | E2E-RM-014 逐角色 Tab 可见矩阵 |
| #6 seed upsert drift | L1c 每角色 vs `ROLE_DEFS.permissionKeys` |

---

## 七、执行入口

- L0a/L0b：`cd testing && npm run test:contract:module robot-manager`
- L0c：`cd testing && npm run test:snapshot:module robot-manager`（需测试后端 3011）
- L1：`bash testing/scripts/run-backend-integration.sh testing/backend/integration/robot-manager --runInBand`
- L1c：`cd testing && npm run test:data-quality:module robot-manager`
- L2：通过 `test-frontend` skill 调用，浏览器指向 `http://localhost:3010`
- 权限审计：`cd testing && npx ts-node --transpile-only scripts/robot-manager-permission-audit.ts`
