# 机器人全生命周期管理 - E2E 测试规格

> **module**: robot-manager
> **doc_type**: E2ETestSpec
> **status**: ⚠️ **v2 sections DEPRECATED · v3 主路径见 §0**
> **owner**: FFOA Team
> **upstream_docs**: 01-prd.md, 04-state-machine.md, 07-api.md, business-analysis/*
> **last_verified**: 2026-05-18（v3 主路径 happy path 实测 ✅）

---

## ⚠️ v2 → v3 状态机重构通告（2026-05）

**本文档大量内容描述 v2 6 状态机（ORDERED / IN_TRANSIT / IN_STOCK / RESERVED / SOLD / DELIVERED），跟当前 schema 不符**。

当前 schema = **v3 28 stage**（`RobotLifecycleStage` enum，详见 `backend/prisma/schema/robot_manager.prisma:61-83`）。
v2 sections (E2E-RM-001 / 002 / 003) 仅作历史参考，**不要按它们跑测试**。

完整重写排期：见 §0「v3 主路径 spec」+ 配套 issue（待立）。

---

## §0 v3 主路径 E2E spec（2026-05-18 实测通过）

### [E2E-RM-V3-001] 完整 lifecycle happy path

**优先级**：P0
**角色**：itadmin（Administrator role；或后续按 RBAC 拆 RobotManagerRLE / SupplyChain / Sales / Finance 分段执行）
**预计耗时**：8-12 分钟（含 fixture setup）
**覆盖**：v3 占位 SN 机制 + 28 stage 状态机 + 13 lifecycle guard + 业务实体串联

#### 前置数据（fixture）

| 实体 | 创建方式 | 用途 |
|---|---|---|
| RobotModel | `POST /robot-manager/admin/models` | 型号档案 |
| RobotSku | `POST /robot-manager/admin/skus` (modelId 关联) | SKU 档案 |
| Customer | `POST /platform-master/customers` | 销售/交付目标 |
| Supplier | `POST /platform-master/suppliers` | 采购对象 |
| Location | `POST /platform-master/locations` | 仓库位置（可选）|

#### 主路径 18 stage 步骤表

| # | Stage | 操作 / Fixture | 验证 |
|---|---|---|---|
| 1 | SUPPLY_PO_CREATED | UI: `/robot-manager/purchase-orders` → 新建 PO（含 supplier + line × N quantity）| PO 创建后自动生成 N 个占位 RobotUnit (`{poNo}-LINE-{NNN}`)，全部在 SUPPLY_PO_CREATED stage |
| 2 | SUPPLY_IN_PRODUCTION | UI: my-work drawer 点「完成并推进 → 工厂在制」/ API `POST /:id/change-stage` `{toStage:SUPPLY_IN_PRODUCTION, version}` | snapshot.currentStage 更新 |
| 3 | SUPPLY_READY_TO_SHIP | 同上推进 | |
| 4 | LOGISTICS_IN_TRANSIT | 同上 | |
| 5 | LOGISTICS_BONDED | 同上 | |
| 6 | LOGISTICS_CUSTOMS_CLEARED | 同上 | |
| ⚠️ | **activate-sn** | `POST /:id/activate-sn` `{supplierSn:'XXX'}` | ffsn `{poNo}-LINE-{NNN}` → `FF-YYYYMM-NNNNN`；placeholderSnOrig 保留原值 |
| 7 | WAREHOUSE_RECEIVED | 推进 | Guard `PLACEHOLDER_NOT_ACTIVATED` 检查通过；未先激活 SN 时正确拦截 |
| 8 | WAREHOUSE_AT_W1_PDI | 推进 | |
| ⚠️ | **inspection fixture** | `POST /:id/inspections` `{inspectionNo:1, resolvedAt:now}` | 满足 checkFunctionTest guard |
| 9 | WAREHOUSE_MODIFICATION | 推进 | Guard `功能测试未完成 / 未通过` 通过 |
| ⚠️ | **labels + readiness fixture** | × 7 `PUT /:id/quality-labels` `{labelTypeCode, status:VERIFIED}` + `PUT /:id/readiness` `{hasRobot:true, ... all 10 boolean: true}` | 满足 checkConversionValidated guard（7 verified + readiness.completedAt 由 service 自动 set）|
| 10 | WAREHOUSE_AT_W2 | 推进 | |
| 11 | WAREHOUSE_BRANDED_READY | 推进（**注：AT_W2 → BRANDED_READY 直接转**，跳过 AT_W2_RLE 分支）| Guard `质量标签未齐 / 配件清点未完成` 通过 |
| ⚠️ | **SO + SalesOrderLine fixture** | `POST /robot-manager/sales-orders` `{customerId, lines:[{robotUnitId, lineType:HARDWARE, unitPrice, netAmount}]}` | 满足 checkReserveRequiresBranded（line 已绑 + fromStage 合法）|
| 12 | SALES_RESERVED | 推进 | snapshot.currentSalesOrderId + currentCustomerId 同步刷新（bug #3 修复后）|
| ⚠️ | **Payment PAID fixture** | `POST /robot-manager/payments` `{relatedType:SALES_ORDER, relatedId:so.id, robotUnitId, direction:INBOUND, paymentStatus:PAID, amount, paidAt}` | 满足 G5 checkG5Payment guard |
| 13 | SALES_PAYMENT_VALIDATED | 推进 | |
| 14 | DELIVERY_APPROVAL | 推进 | |
| ⚠️ | **DR + DF fixture** | `POST /robot-manager/delivery-requests` + `POST /robot-manager/delivery-fulfillments` `{signedFormStatus:SIGNED, acceptanceFormStatus:SIGNED, ...}` | 满足 checkDeliveryValidation + checkPGIReady（two forms signed + paid）|
| 15 | DELIVERY_READY | 推进（**注：DELIVERY_APPROVAL → DELIVERY_READY 直接转**，跳过 P6.1 DELIVERY_PAYMENT_COLLECTED 分支）| |
| 16 | DELIVERY_DELIVERED | 推进 | snapshot warrantyStatus=ACTIVE + physicalProductStatus=DELIVERED 同步 |
| ⚠️ | **disposal fixture** | `PUT /robot-manager/:id` `{retiredAt:now, disposalType:SCRAPPED, disposalNotes}` | 满足 checkClosedNeedsDisposal |
| 17 | CLOSED | 推进（终态）| snapshot physicalProductStatus=RETIRED |

#### 断言

- 18 stage 全部推进成功，snapshot.version 从 0 → 19
- 占位 SN 流程：ffsn 从 `{poNo}-LINE-001` 变成 `FF-YYYYMM-NNNNN`，placeholderSnOrig 保留原值供追溯
- 至少 18 条 RobotLifecycleEvent 写入（每次 change-stage + activate-sn + 各 fixture 触发的事件）
- 乐观锁正常：传错 version 应返回 409

### 13 lifecycle guard 清单（service 层硬编码，`lifecycle-guards.service.ts`）

| Guard | 触发转换 | 通过条件 |
|---|---|---|
| checkPlaceholderActivated | LOGISTICS_CUSTOMS_CLEARED → WAREHOUSE_RECEIVED | 必须先 `POST /:id/activate-sn` |
| checkFunctionTest | WAREHOUSE_AT_W1_PDI → WAREHOUSE_MODIFICATION | ≥1 条 InspectionRecord with resolvedAt |
| checkConversionValidated | WAREHOUSE_MODIFICATION → WAREHOUSE_BRANDED_READY | 7 QualityLabel VERIFIED + Readiness.completedAt |
| checkReserveRequiresBranded | → SALES_RESERVED | fromStage ∈ {WAREHOUSE_BRANDED_READY, WAREHOUSE_AT_W2_RLE} + SalesOrderLine 已绑 |
| checkG5Payment | SALES_RESERVED → SALES_PAYMENT_VALIDATED | PaymentRecord PAID 存在 |
| checkDeliveryValidation | DELIVERY_READY → DELIVERY_DELIVERED | DeliveryFulfillment.signedFormStatus = SIGNED |
| checkPGIReady | 同上 | DeliveryFulfillment.acceptanceFormStatus = SIGNED + Payment PAID |
| checkRMAEligible | DELIVERY_DELIVERED → AFTERSALES_RETURN_INITIATED | open ServiceTicket 存在 |
| checkQuoteApproved | AFTERSALES_UNDER_REPAIR → AFTERSALES_REPAIRED | placeholder（in-warranty 直接通过；OOW 路径待业务 review）|
| checkPOHasSupplier | PO 创建 | supplierId 必填 + supplier 存在 |
| checkSONeedsCustomer | SO 创建 | customerId 必填 + customer 存在 |
| checkClosedNeedsDisposal | → CLOSED | disposalType + retiredAt 必填 |
| checkRentalNeedsContract | → RENTAL_ACTIVE | RentalAgreement 已创建 |
| checkD2EligibleWindow | DELIVERY_DELIVERED → RETURNED | （TBD 业务规则）|

### 分支路径（待补 spec）

- **AFTERSALES**: DELIVERY_DELIVERED → AFTERSALES_TICKET → AFTERSALES_RETURN_INITIATED → ... → AFTERSALES_REPAIRED → DELIVERED（in-warranty）
- **OOW QUOTE**: AFTERSALES_UNDER_REPAIR → AFTERSALES_QUOTE_APPROVAL → AFTERSALES_REPAIRED / CLOSED
- **RENTAL**: WAREHOUSE_BRANDED_READY → SALES_RESERVED → → DELIVERY_DELIVERED → RENTAL_ACTIVE
- **CANCELLED**: any stage → CANCELLED（unit 释放回库）
- **RETURNED**: DELIVERY_DELIVERED → RETURNED（D2 退货窗口期内）

### 历史参考（v2，不再使用）

⚠️ 以下 §概述 / §角色矩阵 / §通用前置 / §P0-P2 sections 是 v2 6 状态机的 spec，**与当前 schema 不符**。
仅在迁移/对比时参考；新测试一律按 §0 跑。

---

## 概述

本文档定义机器人全生命周期管理模块的 E2E 测试流程，按优先级分为 P0（关键路径）、P1（重要功能）、P2（补充覆盖）。

E2E 测试通过 AI + Playwright MCP 执行，不编写测试代码。使用 accessibility tree 定位元素（role/name），不依赖 data-testid。

---

## 角色矩阵（v6 同步）

模块定义 5 个角色，对应 18 条权限（不再使用 `admin`，拆分为 5 个 `manage:*`）：

| 角色 code | 说明 | 关键权限（摘） |
|-----------|------|---------------|
| `Administrator` | 平台管理员 | 所有 robot-manager 权限 |
| `RobotManagerRLE` | 整机业务负责人 | 全 CRUD + 全 write + 5 个 manage:* |
| `RobotManagerSupplyChain` | 供应链 | read / create / change-status / write:identity / write:supply-chain / manage:suppliers / manage:locations / import / export |
| `RobotManagerSales` | 销售 | read / change-status / write:sales / manage:customers / manage:locations / export |
| `RobotManagerFinance` | 财务 | read / write:finance / export |

> L2 执行时**至少覆盖 3 个角色**，并用 `test.robot1`（其他组织 Employee）验证组织隔离。

---

## 通用前置条件

- 系统已部署并可访问
- 所有角色的测试账号已创建并分配到测试组织
- 数据库中已有必要的种子数据：
  - `RobotFieldDef`（scope=unit 32 条 + scope=service_record/location 系统字典）
  - `RobotSystemConfig` 的 `ffsn_rule`、`status_default_location`、`repair_auto_service_type`
  - `RobotModel` / `RobotSku` / `RobotSupplier` / `RobotCustomer` / `RobotLocation` 各至少 3 条

---

## P0：关键路径

### [E2E-RM-001] 设备全生命周期（含 SOLD 阶段）

| 属性 | 值 |
|------|-----|
| **优先级** | P0 |
| **角色** | RobotManagerRLE |
| **前置条件** | 以 RobotManagerRLE 角色账号登录系统 |
| **预计耗时** | 5-8 分钟 |
| **覆盖功能** | F1.1 新建、F1.2 编辑、F1.3 查看详情、F3.1 状态变更（含 SOLD 新阶段）、F3.2 合法性校验、F3.3 Location 联动、F4.1 客户绑定、F9.1 Currency 显示 |

**步骤：**

1. **登录系统**
   - 使用 RLE 角色的管理员账号登录
   - 预期：登录成功，进入主页

2. **导航到设备管理**
   - 导航到 `/robot-manager`
   - 预期：设备列表页加载完成，显示设备列表或空状态

3. **新建设备**
   - 点击"新建设备"按钮
   - 选择 modelId（型号下拉，从 `/admin/models` 加载）和 skuId（SKU 下拉，按 model 联动过滤）
   - 点击提交
   - 预期：
     - 创建成功，返回列表页
     - 列表中出现新设备
     - 新设备状态为 ORDERED
     - FFSN 已自动生成且显示
     - locationId 为 null（默认配置 `status_default_location[ORDERED] = null`，前端按 status 推断显示"未发货"）

4. **进入设备详情**
   - 点击新建的设备行，进入详情页
   - 预期：
     - 详情页显示 6 大数据模块
     - currentStatus 显示为 Ordered
     - FFSN 字段不可编辑

5. **状态变更：ORDERED → IN_TRANSIT**
   - 执行状态变更操作，选择 IN_TRANSIT
   - 预期：
     - 状态更新为 In Transit
     - locationId 仍为 null（`status_default_location[IN_TRANSIT] = null`，前端推断"运输中"）

6. **状态变更：IN_TRANSIT → IN_STOCK**
   - 执行状态变更操作，选择 IN_STOCK
   - 预期：
     - 状态更新为 In Stock
     - locationId 自动设为 `HQ_FZ`（福州总部仓库，按 `status_default_location[IN_STOCK]` 默认配置）

7. **编辑：身份/供应链信息 + 选择币种**
   - 切到"身份" Tab，选择 currency（货币下拉，选 USD 或 CNY），填写其他 identity 字段
   - 切到"供应链" Tab，选择 supplier、填写 PO Number、purchasePrice
   - 预期：
     - currency 下拉展示 USD/CNY/EUR/JPY/HKD/SGD/KRW 七种
     - purchasePrice 输入框下方实时显示格式化金额（如 "$50,000.00"，随 currency 切换）
     - 保存成功

8. **编辑：绑定客户并设置销售价格**
   - 切到"销售" Tab，选择 customerId（客户下拉），填写 salesPriceHardware
   - 保存
   - 切到"财务" Tab，设置 cost
   - 预期：
     - customerId 和 salesPriceHardware 保存成功（PUT /:id/sales）
     - cost 保存成功（PUT /:id/finance）
     - 页面显示更新后的值（含客户名称 join、按 currency 格式化金额）

9. **状态变更：IN_STOCK → RESERVED**
   - 执行状态变更操作，选择 RESERVED
   - 预期：
     - 状态更新为 Reserved
     - locationId 保持 `HQ_FZ`
     - 客户绑定确认

10. **状态变更：RESERVED → SOLD**
    - 执行状态变更操作，选择 SOLD
    - 预期：
      - 状态更新为 Sold
      - 销售看板/列表中 SOLD 徽标显示正确
      - 禁止跳过 SOLD 直接选 DELIVERED（下拉选项应不含 DELIVERED）

11. **状态变更：SOLD → DELIVERED**
    - 执行状态变更操作，选择 DELIVERED（可显式传 locationId 指向客户站点）
    - 预期：
      - 状态更新为 Delivered
      - deliveredDate 自动设置为当前日期
      - locationId 按传入值更新

12. **验证状态变更日志**
    - 查看设备的状态变更记录
    - 预期：
      - 可见 6 条状态变更日志（初始创建 + ORDERED→IN_TRANSIT、IN_TRANSIT→IN_STOCK、IN_STOCK→RESERVED、RESERVED→SOLD、SOLD→DELIVERED）
      - 每条日志包含：变更前状态、变更后状态、操作人、操作时间

**断言：**

- 设备完整经历 ORDERED → IN_TRANSIT → IN_STOCK → RESERVED → **SOLD** → DELIVERED 流程
- locationId 按 `RobotSystemConfig.status_default_location` 配置正确联动
- deliveredDate 在 DELIVERED 状态时自动设置
- 状态变更日志完整记录所有变更，共 6 条（含 SOLD）
- 金额按 currency 正确格式化显示（`Intl.NumberFormat` 输出）

---

### [E2E-RM-002] 维修与归还（经 REPAIR → REPAIRED → DELIVERED）

| 属性 | 值 |
|------|-----|
| **优先级** | P0 |
| **角色** | RobotManagerRLE |
| **前置条件** | 存在一台 DELIVERED 状态的设备（可沿用 E2E-RM-001 的设备或独立创建） |
| **预计耗时** | 4-6 分钟 |
| **覆盖功能** | F3.1 状态变更（REPAIR/REPAIRED 新阶段）、状态副作用（ServiceRecord 创建/完成） |

**步骤：**

1. **打开 DELIVERED 设备详情**
   - 导航到一台 DELIVERED 状态的设备详情页
   - 预期：设备状态为 Delivered

2. **状态变更：DELIVERED → REPAIR**
   - 执行状态变更操作，选择 REPAIR，填写 remark（如 "客户报修-电池异常"）
   - 预期：
     - 状态更新为 Repair
     - locationId 自动设为 `HQ_FZ`
     - 创建一条 ServiceRecord 记录，`serviceTypeCode` 取自 `repair_auto_service_type` 配置（默认 `REPAIR`）

3. **验证 ServiceRecord**
   - 在设备详情的售后模块中查看
   - 预期：
     - 存在一条维修记录，`serviceTypeCode = REPAIR`
     - 记录包含开始日期，completedDate 为空

4. **状态变更：REPAIR → REPAIRED（维修完成）**
   - 执行状态变更操作，选择 REPAIRED
   - 预期：
     - 状态更新为 Repaired
     - ServiceRecord 的 completedDate 被设置为今天
     - 详情页状态徽标显示 "Repaired / 维修完成"

5. **状态变更：REPAIRED → DELIVERED（归还客户）**
   - 执行状态变更操作，选择 DELIVERED（显式传 locationId 指向客户站点）
   - 预期：
     - 状态更新为 Delivered
     - customerId 保持不变（归还原客户）
     - deliveredDate 被覆盖为新的日期

6. **分支验证：REPAIRED → IN_STOCK**（可独立创建新设备流程化）
   - 另起一台设备流转至 REPAIRED
   - 执行状态变更，选择 IN_STOCK
   - 预期：
     - 状态更新为 In Stock
     - customerId 和 salesOrderId 被清空（设备重新入库，可再分配）
     - locationId 回到 `HQ_FZ`

**断言：**

- DELIVERED → REPAIR → REPAIRED → DELIVERED 完整链路执行成功
- ServiceRecord 在 REPAIRED 时 completedDate 被补齐
- REPAIRED 的两个分支（→ DELIVERED / → IN_STOCK）均可用
- REPAIRED → IN_STOCK 时 customer 绑定被正确清除
- 每步骤在 statusChangeLogs 有记录

---

### [E2E-RM-003] Guard 拦截验证

| 属性 | 值 |
|------|-----|
| **优先级** | P0 |
| **角色** | RLE |
| **前置条件** | 以 RLE 角色用户登录系统 |
| **预计耗时** | 3-5 分钟 |
| **覆盖功能** | F3.2 状态合法性校验、前置条件（Guard Conditions）校验 |

**步骤：**

1. **新建设备**
   - 创建一台新设备（ORDERED 状态）
   - 预期：设备创建成功，状态为 ORDERED

2. **尝试非法状态跳跃：ORDERED → DELIVERED**
   - 尝试将设备状态从 ORDERED 直接变更为 DELIVERED
   - 预期：
     - 操作被拦截
     - 显示错误信息，说明当前状态不允许直接转换到 DELIVERED
     - 设备状态保持 ORDERED 不变

3. **正常流转到 IN_STOCK**
   - 依次执行 ORDERED → IN_TRANSIT → IN_STOCK
   - 预期：设备状态变为 IN_STOCK

4. **尝试未绑定客户直接 RESERVED**
   - 在未设置 customerId 的情况下，尝试 IN_STOCK → RESERVED
   - 预期：
     - 操作被拦截
     - 显示错误信息，提示 customerId 不可为空
     - 设备状态保持 IN_STOCK 不变

5. **绑定客户后成功 RESERVED**
   - 编辑设备，设置 customerId
   - 执行 IN_STOCK → RESERVED
   - 预期：状态变更成功，设备状态为 RESERVED

6. **尝试无 remark 的 CANCELLED**
   - 先将设备流转至可取消的状态（如创建另一台 ORDERED 设备）
   - 尝试在不填写 remark 的情况下执行 → CANCELLED
   - 预期：
     - 操作被拦截
     - 显示错误信息，提示 remark 不可为空（取消/报废必须填写原因）
     - 设备状态保持不变

**断言：**

- 非法状态跳跃被正确拦截，返回明确错误信息
- 前置条件未满足时状态变更被拒绝
- CANCELLED 转换必须提供 remark
- 所有拦截后设备状态保持不变

---

## P1：重要功能

### [E2E-RM-004] 报表数据一致性

| 属性 | 值 |
|------|-----|
| **优先级** | P1 |
| **角色** | RLE |
| **前置条件** | 以 RLE 角色用户登录系统 |
| **预计耗时** | 5-8 分钟 |
| **覆盖功能** | F7.1 库存报表、F7.2 销售报表、F7.3 财务报表 |

**步骤：**

1. **准备测试数据：创建 3 台设备**
   - 设备 A：流转至 IN_STOCK 状态
   - 设备 B：流转至 DELIVERED 状态，设置 salesPrice 和 cost
   - 设备 C：流转至 ORDERED → CANCELLED（填写 remark）
   - 预期：3 台设备分别处于 IN_STOCK、DELIVERED、CANCELLED 状态

2. **导航到报表页**
   - 导航到 `/robot-manager/reports`
   - 预期：报表页加载完成

3. **验证库存报表**
   - 查看库存数据
   - 预期：
     - IN_STOCK 数量包含设备 A（+1）
     - 可分配库存包含设备 A
     - CANCELLED 设备不计入任何库存统计

4. **验证财务报表**
   - 查看财务数据
   - 预期：
     - Total Revenue 包含设备 B 的 salesPrice
     - Total Cost 包含设备 B 的 cost
     - Total Margin = 设备 B 的 salesPrice - cost
     - 未设置 salesPrice/cost 的设备不计入（无论状态）

**断言：**

- 库存报表正确反映 IN_STOCK 数量
- 财务报表 = SUM(salesPrice) / SUM(cost) / SUM(salesPrice - cost)，**不按 currentStatus 过滤**，仅当两者均非 null 才计入（v2 简化口径）
- Revenue、Cost、Margin 数值一致（Margin = Revenue - Cost）

---

### [E2E-RM-005] 列表筛选与搜索

| 属性 | 值 |
|------|-----|
| **优先级** | P1 |
| **角色** | RLE |
| **前置条件** | 系统中已有多台不同状态的设备 |
| **预计耗时** | 3-5 分钟 |
| **覆盖功能** | F1.4 设备列表、F8.3 多维筛选 |

**步骤：**

1. **导航到设备列表**
   - 导航到 `/robot-manager`
   - 预期：设备列表加载完成，显示所有设备

2. **按状态筛选**
   - 使用状态筛选器，选择 ORDERED
   - 预期：
     - 列表仅显示 ORDERED 状态的设备
     - 其他状态设备不显示
     - 设备数量与筛选结果一致

3. **按 FFSN 搜索**
   - 清除状态筛选
   - 在搜索栏输入某台已知设备的 FFSN（全部或部分）
   - 预期：
     - 搜索结果中包含目标设备
     - 搜索结果准确，不包含无关设备

4. **验证分页**
   - 清除搜索条件
   - 如果设备总数超过单页显示数量，验证分页控件
   - 预期：
     - 分页控件可见
     - 点击下一页后列表内容更新
     - 页码信息正确显示

**断言：**

- 状态筛选器正确过滤设备
- FFSN 搜索返回准确结果
- 分页功能正常工作

---

### [E2E-RM-006] 编辑与乐观锁

| 属性 | 值 |
|------|-----|
| **优先级** | P1 |
| **角色** | RLE |
| **前置条件** | 存在一台可编辑的设备 |
| **预计耗时** | 3-5 分钟 |
| **覆盖功能** | F1.2 编辑字段、乐观锁机制 |

**步骤：**

1. **打开设备详情**
   - 导航到一台设备的详情页
   - 记录当前 version 值
   - 预期：设备详情加载完成

2. **编辑供应链字段**
   - 切到"供应链" Tab，修改 supplierId（下拉选另一个供应商），保存（PUT /:id/supply-chain）
   - 切到"财务" Tab（如有权限），修改 cost，保存（PUT /:id/finance）
   - 预期：
     - 两次保存均成功
     - version 值各 +1
     - 页面 join 数据显示更新后的 supplier 名称和 cost

3. **验证字段不可变性**
   - 设置 poNumber（如果尚未设置）
   - 保存后再次编辑
   - 预期：
     - poNumber 设置后变为不可修改（或有明确提示）
     - salesOrderId 同理：一旦设置不可更改

4. **验证 version 递增**
   - 对比编辑前后的 version
   - 预期：每次成功保存后 version 递增 1

**断言：**

- 编辑保存后数据正确更新
- version 字段随每次保存递增
- poNumber 和 salesOrderId 一旦设置后不可修改

---

## P2：补充覆盖

### [E2E-RM-007] 设备软删除

| 属性 | 值 |
|------|-----|
| **优先级** | P2 |
| **角色** | RLE |
| **前置条件** | 存在一台可删除的设备 |
| **预计耗时** | 2-3 分钟 |
| **覆盖功能** | 软删除机制 |

**步骤：**

1. **打开设备详情**
   - 导航到目标设备的详情页
   - 记录设备的 FFSN
   - 预期：设备详情加载完成

2. **删除设备**
   - 点击删除按钮
   - 确认删除操作
   - 预期：
     - 删除成功，返回列表页
     - 列表中不再显示该设备

3. **验证列表中不可见**
   - 在设备列表中搜索该 FFSN
   - 预期：搜索无结果

4. **验证 URL 不可访问**
   - 直接通过 URL 访问已删除设备的详情页
   - 预期：
     - 页面显示 404 或"设备不存在"提示
     - 不显示已删除设备的数据

**断言：**

- 删除后设备从列表中移除
- 已删除设备不可通过 URL 直接访问
- 删除操作为软删除（数据库中 deletedAt 被设置）

---

### [E2E-RM-008] 导入导出页面（含动态字段模板）

| 属性 | 值 |
|------|-----|
| **优先级** | P2 |
| **角色** | RobotManagerRLE |
| **前置条件** | 以 RLE 角色账号登录；数据库中存在 ≥ 5 台设备 |
| **预计耗时** | 3-5 分钟 |
| **覆盖功能** | F8.1 Excel 导出、F8.2 Excel 导入、**F8.3 动态字段模板下载** |

**步骤：**

1. **导航到导入导出页面**
   - 导航到 `/robot-manager/import-export`
   - 预期：导入导出页面加载完成

2. **验证导出功能**
   - 点击导出按钮
   - 预期：
     - 浏览器触发文件下载
     - 下载文件为 Excel 格式（`.xlsx`），文件名含日期
     - 文件包含设备列表数据（≥ 5 行）

3. **验证模板下载（默认全字段）**
   - 点击"下载模板"按钮
   - 预期：
     - 浏览器触发下载 `robot-units-template-YYYY-MM-DD.xlsx`
     - 打开文件包含两个 sheet：`Robot Units`（主）+ `README`（列说明）
     - 主 sheet 第一行为表头，包含 `FFSN` / `Model Code` / `SKU Code`（必需）+ 所有可选固定列 + 全部启用的动态字段
     - README sheet 每行说明一列的 Required / Type / Description

4. **验证字段筛选模板（新功能）**
   - 在模板选择器中勾选 "Supplier / Current Status / PO Number"（模拟 `?fields=supplierId,currentStatus,poNumber`）
   - 点击下载
   - 预期：
     - 返回的 xlsx 表头仅包含必需列 + 选中字段对应列
     - 未选中的 Customer Code / Location Code 等**不出现**
     - README sheet 仅描述选中字段

5. **验证导入区域**
   - 查看导入功能区域
   - 预期：
     - 显示文件上传入口
     - 展示数据校验规则说明（FFSN 唯一、Enum 限制、数值非负等）
     - 模板下载链接可点击

6. **端到端闭环：下载模板 → 填写一行 → 上传**
   - 下载模板 → 在第一行填入合法数据 → 上传
   - 预期：导入成功，新设备出现在列表中

**断言：**

- 导出和模板下载均触发文件下载
- 模板含双 sheet（主 + README）
- `?fields=` 过滤参数正确裁剪可选列
- 必需列 FFSN / Model Code / SKU Code 始终保留
- 动态字段按当前启用的 FieldDef（scope=unit, enabled=true）生成列
- 模板 → 填写 → 上传闭环工作

---

### [E2E-RM-009] 多角色权限矩阵

| 属性 | 值 |
|------|-----|
| **优先级** | P0 |
| **角色** | RobotManagerRLE / SupplyChain / Sales / Finance 全覆盖 |
| **前置条件** | 四个角色的测试账号均已创建；数据库有 ≥ 1 台 IN_STOCK 设备 |
| **预计耗时** | 8-12 分钟 |
| **覆盖功能** | F10.1 角色权限、field-level write 权限、manage:* 拆分、盲区 #1 |

**步骤：**

1. **SupplyChain 登录**
   - 导航到设备详情
   - 预期：
     - 可见 Identity / Supply Chain Tab，有"编辑"按钮
     - Sales / Finance / After-Sales / Compliance Tab **只读**（编辑按钮隐藏或禁用）
     - 侧边栏管理菜单：Fields / Models / Customers **不可见**；Suppliers / Locations **可见**
     - 尝试直接访问 `/admin/customers` → 无权限提示或重定向

2. **Sales 登录**
   - 导航到设备详情
   - 预期：
     - Sales Tab 可编辑
     - Identity / Supply Chain / Finance / After-Sales / Compliance Tab **只读**
     - 侧边栏：Customers / Locations 可见；Suppliers / Models / Fields 不可见
     - 尝试 POST `/admin/suppliers` → 403

3. **Finance 登录**
   - 导航到设备详情
   - 预期：
     - 仅 Finance Tab 可编辑
     - 其他所有 Tab 只读
     - 所有 `manage:*` 管理页面不可见
     - 可使用 `/reports/finance` 和 Excel 导出

4. **Administrator 登录**
   - 预期：所有 Tab 均可编辑，所有管理菜单可见

**断言：**

- 每个角色的侧边栏可见项与 `ROLE_DEFS.permissionKeys` 精确匹配
- 非授权 Tab 的"编辑"按钮不可用（按钮隐藏或 disabled）
- 直接访问未授权端点返回 403，前端友好提示
- Admin 不应"路过所有权限"，每个角色独立验证（排除盲区 #1）

---

### [E2E-RM-010] 组织隔离（test.robot1）

| 属性 | 值 |
|------|-----|
| **优先级** | P0 |
| **角色** | test.robot1（其他组织 Employee） |
| **前置条件** | test.robot1 账号已建，归属于非 robot-manager 测试组织 |
| **预计耗时** | 2-3 分钟 |
| **覆盖功能** | 多租户隔离 |

**步骤：**

1. 以 test.robot1 登录
2. 访问 `/robot-manager`
   - 预期：列表为空（或仅见其自己组织的设备，看不到 RobotManager 组织创建的设备）
3. 尝试直接访问 RobotManager 组织某设备的详情 URL
   - 预期：404 或 403

**断言：**

- 跨组织数据不可见
- 组织过滤在列表 / 详情 / 报表 所有查询端点均生效

---

### [E2E-RM-011] 基础数据 · 字段定义（FieldDef Scope 切换）

| 属性 | 值 |
|------|-----|
| **优先级** | P1 |
| **角色** | RobotManagerRLE 或 Administrator |
| **前置条件** | 已存在至少 1 条 scope=unit 字段、1 条 scope=service_record 系统字典 |
| **预计耗时** | 4-5 分钟 |
| **覆盖功能** | FieldDef Scope 切换、Options Table 编辑器、code 自动生成、i18n 术语"基础数据" |

**步骤：**

1. 导航到 `/robot-manager/admin/fields`
   - 预期：
     - 页面顶部面包屑 / 标题使用"基础数据"而非"档案"术语
     - 2-Tab 界面：Custom（scope=unit）/ System（scope=service_record + location）

2. **Custom Tab：新增业务字段**
   - 点击"新增"
   - 输入 labelEn = "Warranty End Date"，label = "保修截止日"
   - type 选 `date`，group 选 `after-sales`
   - 保存
   - 预期：
     - key 自动生成为 `warrantyEndDate`（labelEn 转 camelCase）
     - 列表出现新字段

3. **Custom Tab：新增 select 类字段，用 Options 表格编辑**
   - 新建字段 labelEn = "Lifecycle Phase"
   - type = `select`
   - 展开 Options 编辑器
   - 添加 3 条选项：只输入 label（`Pre-sale` / `Active` / `Post-sale`）
   - 保存
   - 预期：
     - code 自动生成（`PRE_SALE` / `ACTIVE` / `POST_SALE`），Table 中不显示 code 列
     - API 响应的 options 数组含正确 code/label

4. **System Tab：编辑系统字典 serviceType**
   - 切到 System Tab
   - 定位 `serviceType`（scope=service_record）
   - 添加一条新选项 `INSPECTION` / Inspection
   - 保存
   - 预期：
     - FieldDef 更新成功，options 内嵌新增条目
     - 设备详情页维修 Tab 的 serviceType 下拉出现新选项

**断言：**

- Scope 切换正确（Custom vs System）
- code 按 labelEn 自动生成规则稳定
- 系统字典编辑即时生效于业务表单

---

### [E2E-RM-012] 基础数据 · 角色隔离写入

| 属性 | 值 |
|------|-----|
| **优先级** | P1 |
| **角色** | SupplyChain / Sales 对比 |
| **前置条件** | 两角色账号均已建 |
| **预计耗时** | 3-4 分钟 |
| **覆盖功能** | `manage:suppliers` vs `manage:customers` 拆分（v6 新增） |

**步骤：**

1. **SupplyChain 可写 Suppliers，不可写 Customers**
   - 登录 SupplyChain
   - 访问 `/admin/suppliers` → 可新增/编辑/删除
   - 访问 `/admin/customers` → 页面不可见或访问被拒
2. **Sales 反之**
   - 登录 Sales
   - 访问 `/admin/customers` → 可新增/编辑/删除
   - 访问 `/admin/suppliers` → 访问被拒
3. **两者共用 Locations**
   - 两个角色都应可管理 Locations（`manage:locations`）

**断言：**

- `manage:*` 权限拆分准确反映在前端 UI 和后端 403 拦截
- 盲区 #1（Admin 跳过权限）已通过非 Admin 角色真实验证

---

### [E2E-RM-013] 错误码 i18n 显示

| 属性 | 值 |
|------|-----|
| **优先级** | P1 |
| **角色** | RobotManagerRLE |
| **前置条件** | 存在一台 IN_STOCK 未绑定客户的设备 |
| **预计耗时** | 2-3 分钟 |
| **覆盖功能** | RobotError 枚举 + 前端 errorCodes i18n 字典 |

**步骤：**

1. 切换前端语言为中文
2. 尝试 IN_STOCK → RESERVED（未绑客户）
   - 预期：
     - 错误提示为中文翻译（如 "进入已预留状态前必须绑定客户"），而非英文 fallback 或裸 code
3. 切换到英文
4. 重现同样错误
   - 预期：英文翻译 "Must bind a customer before RESERVED"
5. 错误响应携带 `error.code = ROBOT_MUST_BIND_CUSTOMER_BEFORE_RESERVE`

**断言：**

- 前端按 `error.code` 查 `errorCodes` i18n 字典成功
- 中英文翻译对照完整
- 所有 18 个 RobotError 码在 zh/en locales 中有定义

---

### [E2E-RM-014] 角色页面可见性（菜单 + 详情）

| 属性 | 值 |
|------|-----|
| **优先级** | P1 |
| **角色** | SupplyChain / Sales / Finance |
| **前置条件** | 三个角色账号已建 |
| **预计耗时** | 3-5 分钟 |
| **覆盖功能** | 侧边栏权限过滤 + 详情页 Tab 权限 + 0 console errors |

**步骤：**

- 按表格逐一登录每个角色，检查侧边栏与详情页 Tab 可见性

| 路径 | Administrator | RLE | SupplyChain | Sales | Finance |
|------|---|---|---|---|---|
| `/robot-manager` 列表 | ✓ | ✓ | ✓ | ✓ | ✓ |
| 详情 · Identity Tab 可编辑 | ✓ | ✓ | ✓ | ✗ | ✗ |
| 详情 · Supply Chain Tab 可编辑 | ✓ | ✓ | ✓ | ✗ | ✗ |
| 详情 · Sales Tab 可编辑 | ✓ | ✓ | ✗ | ✓ | ✗ |
| 详情 · Finance Tab 可编辑 | ✓ | ✓ | ✗ | ✗ | ✓ |
| 详情 · After-Sales 可编辑 | ✓ | ✓ | ✗ | ✗ | ✗ |
| 详情 · Compliance 可编辑 | ✓ | ✓ | ✗ | ✗ | ✗ |
| 侧边栏 Fields | ✓ | ✓ | ✗ | ✗ | ✗ |
| 侧边栏 Models | ✓ | ✓ | ✗ | ✗ | ✗ |
| 侧边栏 Suppliers | ✓ | ✓ | ✓ | ✗ | ✗ |
| 侧边栏 Customers | ✓ | ✓ | ✗ | ✓ | ✗ |
| 侧边栏 Locations | ✓ | ✓ | ✓ | ✓ | ✗ |
| 侧边栏 Help | ✓ | ✓ | ✓ | ✓ | ✓ |

**断言：**

- 每个角色的可见矩阵与上表 100% 一致
- 全流程无 JS console error（盲区 #5 防护）

---

### [E2E-RM-015] Dashboard 概览页

| 属性 | 值 |
|------|-----|
| **优先级** | P1 |
| **角色** | RobotManagerRLE |
| **前置条件** | 系统中有 ≥ 5 台不同状态的设备 |
| **预计耗时** | 2-3 分钟 |
| **覆盖功能** | Dashboard 数字卡 + 状态分布图 + 最近变更列表 |

**步骤：**
1. 导航到 `/robot-manager/dashboard`
   - 预期：4 张数字卡（总设备数 / 在库 / 在途 / 已交付）均正确渲染
2. 点击"在库"卡片
   - 预期：跳转到 `/robot-manager?status=IN_STOCK`，列表自动筛选
3. 返回 Dashboard，检查状态分布图
   - 预期：饼图显示所有非零状态的分布
4. 检查最近变更列表
   - 预期：显示最近 10 条 StatusChangeLog

**断言：**
- 4 张卡片数字与后端 `/reports/inventory` 返回一致
- 点击卡片触发列表页筛选
- 空数据时显示"尚无设备"+ 新建按钮

---

### [E2E-RM-016] 多设备对比页

| 属性 | 值 |
|------|-----|
| **优先级** | P2 |
| **角色** | RobotManagerRLE |
| **前置条件** | 系统中至少 2 台设备 |
| **预计耗时** | 2-3 分钟 |
| **覆盖功能** | 列表多选 + 对比页并列展示 |

**步骤：**
1. 在设备列表页勾选 2-3 台设备，点击"对比"
2. 导航到 `/robot-manager/compare?ids=...`
   - 预期：横向 Table 展示所选设备的关键字段
3. 验证：空集或单选时对比页的降级表现（引导回列表）

**断言：**
- 列表多选 → 对比页 URL 参数正确
- 对比表列数 = 所选设备数
- 字段差异可视化（不同值高亮或 badge）

---

### [E2E-RM-017] 帮助页多角色导航

| 属性 | 值 |
|------|-----|
| **优先级** | P2 |
| **角色** | 全部 5 个角色 + test.robot1 |
| **前置条件** | — |
| **预计耗时** | 3-4 分钟 |
| **覆盖功能** | Help 9 个 Section + 快捷键 `?` |

**步骤：**
1. 以 Administrator 登录，按 `?` 快捷键
   - 预期：跳转 `/robot-manager/help`
2. 逐 Section 滚动检查（Roles / BusinessFlow / PermissionMatrix / Setup / Lifecycle / DailyOps / Roadmap / FAQ / Feedback）
   - 预期：每 Section 有 heading，权限矩阵 Table 的"查看 / View"按 i18n 渲染
3. 切换中/英文
   - 预期：全文无漏译
4. 以 Finance 账号登录，访问 `/robot-manager/help`
   - 预期：页面可见，不因权限被拦截
5. 以 test.robot1 账号（非 robot-manager 角色）访问
   - 预期：能进入 help 页（所有登录用户可读）

**断言：**
- 快捷键 `?` 在非输入状态下生效
- Help 页对所有登录角色可见
- PermissionMatrix ACCESS 数据与 09-test-scenarios.md §2 权限矩阵一致
- 中英文无漏译，errorCodes 字典覆盖所有 19 条 code（含 ATTACHMENT_NO_FILE / TOO_LARGE）

---

### [E2E-RM-018] 基础数据 · Model / SKU / Location CRUD（完整）

| 属性 | 值 |
|------|-----|
| **优先级** | P1 |
| **角色** | RobotManagerRLE（或 Administrator） |
| **前置条件** | — |
| **预计耗时** | 5-7 分钟 |
| **覆盖功能** | Model / SKU / Location Admin 页的 CRUD UI；补 E2E-009/012 未直接覆盖的 admin 页 |

**步骤：**
1. **Models** - `/robot-manager/admin/models`
   - 新建一个 Model（code / name / brand）
   - 编辑，修改 brand
   - 尝试删除被 SKU 引用的 Model → 预期失败提示（"Cannot delete: N SKU(s) still reference this model"）
   - 删除无引用的 Model → 成功
2. **SKUs** - `/robot-manager/admin/skus`
   - 新建 SKU（需选 Model）
   - 按 modelId 筛选列表
   - 编辑 defaultPrice / defaultCost
   - 删除无引用的 SKU → 成功
3. **Locations** - `/robot-manager/admin/locations`
   - 新建 Location（code / name / typeCode / customerId 可选）
   - typeCode 下拉来自 System FieldDef `locationType`
   - 编辑切换 typeCode
   - 尝试删除被 Unit 引用的 Location → 预期失败
4. **SupplyChain 权限跨 Model**：用 SupplyChain 账号访问 `/robot-manager/admin/models` → 预期重定向或访问被拒（Sales/SC 无 `manage:models`）

**断言：**
- 三个 admin 页的 CRUD 完整工作
- 外键保护生效（有引用时拒绝删除）
- SupplyChain / Sales 对 Models 无写权限（403）
- Locations 的 typeCode 下拉与 System FieldDef 数据联动

---

### [E2E-RM-019] 系统配置页（admin/settings）

| 属性 | 值 |
|------|-----|
| **优先级** | P2 |
| **角色** | Administrator 或 RobotManagerRLE |
| **前置条件** | 系统配置中存在 `ffsn_rule` / `status_default_location` / `repair_auto_service_type` 三项 |
| **预计耗时** | 2-3 分钟 |
| **覆盖功能** | RobotSystemConfig JSON 表单 CRUD |

**步骤：**
1. 导航到 `/robot-manager/admin/settings`
   - 预期：列出当前所有 RobotSystemConfig 条目
2. 编辑 `ffsn_rule`（修改 seqLength 从 5 → 6）
   - 保存
3. 新建一个自定义 config key
4. 删除刚创建的 key

**断言：**
- 配置列表可读
- JSON 表单 POST/PUT/DELETE 均成功
- SupplyChain / Sales / Finance 对 settings 页无访问权限（仅 `manage:fields` 可见）

---

## 测试执行说明

### 执行方式

- 通过 AI + Playwright MCP 执行，不编写自动化测试代码
- 使用 accessibility tree 定位页面元素（role/name）
- 每个流程独立执行，P0 流程之间可按序复用设备

### 执行顺序

1. 优先执行 P0 流程（E2E-RM-001 → 002 → 003 → 009 → 010）
2. P0 全部通过后执行 P1 流程（004 → 005 → 006 → 011 → 012 → 013 → 014 → 015 → 018）
3. P1 全部通过后执行 P2 流程（007 → 008 → 016 → 017 → 019）

### MANDATORY-CHECKPOINT 最低要求（test-main 铁律）

- ≥ 3 个角色覆盖（至少 Administrator + SupplyChain + Sales）
- test.robot1 组织隔离场景必测（E2E-RM-010）
- 遇到 bug 修复后从失败流程重跑，禁止用 API/curl 绕过前端

### 测试数据管理

- 每次测试使用独立的测试数据，设备名称/标识使用随机后缀避免冲突
- 测试完成后无需清理数据（软删除机制保证数据安全）

### 测试报告

- 测试报告输出到 `testing/reports/` 目录
- 报告包含：执行时间、通过/失败数、失败流程的截图与错误信息
