# E2E 测试约定（总纲）

LLMS 索引： [llms.txt](/llms.txt)

---

> 本项目 E2E 体系的**初始世界定义与规则手册**。场景覆盖地图见 [e2e-medilink-场景清单.md](/test/e2e-medilink-%E5%9C%BA%E6%99%AF%E6%B8%85%E5%8D%95/)；功能行为的 as-is 权威 = `prd/fsd-medilink/`（FSD）。
> 实现原则：API-first、workers 1、断业务结果不断实现细节、事件链轮询非 sleep。

## 1. 初始世界定义

### 1.1 环境矩阵

| 环境 | 库 | 端口 | 配置 | 触发 |
|---|---|---|---|---|
| dev | `medilink` @ 192.168.50.139:3308 | 18031 | `configs/config.yaml` | `make start` |
| **e2e** | `medilink_e2e` @ 同实例 | **18032** | `configs/config.e2e.yaml` | `bash scripts/dev/e2e.sh`（自管起停）或 `make e2e` |
| 浏览器冒烟 | 同 e2e（吃十段终态库） | web 5173 / app 8080 | `MEDILINK_PROXY_TARGET` 指向 18032 | `ci.sh --with-env`（冒烟前置位见 §1.6） |

Redis 两世界共用 127.0.0.1:6379，靠键命名空间隔离（限流 `rl:<库名>:*`、TOTP `totp:<库名>:*`、RBAC `rbac:<库名>:*`、翻译缓存 `tr:<库名>:*`）。MinIO bucket dev=`medilink-dev` / e2e=`medilink-e2e` 独立隔离。

### 1.2 单册策略

单库单册（tech-design §1 裁决）：E2E 世界 = `make db-reset` 重建独立 e2e 库，无租户克隆、无 ID 重写，隔离问题退化为「独立测试库」。

### 1.3 账号矩阵（seed 预置，`scripts/dev/seed.sql`）

| 账号 | id | 角色 | 凭据（dev/e2e only） | TOTP |
|---|---|---|---|---|
| consultant@medilink.local | 9001 | CONSULTANT | `Consultant#2026` | 密文预生成写死（python3 stdlib 现算 code） |
| doctor@medilink.local | 9002 | DOCTOR | 同策略 | secret 独立 |
| admin@medilink.local | 9003 | ADMIN | 同策略 | 二态重置（见 §1.6） |
| （注册链创建） | 自增 | PATIENT | 注册表单自设 | — |

患者账号不由 seed 预置——由 F-REG 段注册事务链真实创建（全链保真）。

### 1.4 预置种子

4 所医院（3 PUBLISHED + 1 DRAFT——列表 total=3 断言的依据）+ 医院科室 + 专家（PUBLISHED，医生选人消费）+ 3 账号 + 14 权限码字典 + FAILED 邮件样例行（last_error 带 `seed:` 前缀，幂等清理自造行）。

### 1.5 确定性规则

1. **TOTP 现算**：python3 stdlib 按 seed 密钥现算 6 位码；防重放 SETNX EX 90 → **90s 内重跑必撞重放**——失败时睡到下一 30s 边界重算重试恰一次。
2. **限流相位浮动**：60/min 固定窗，前段请求消耗随窗口相位浮动 → **限流段永远放最后**（[10/10]）；冒烟前置位清 `rl:*`。
3. **时延断言口径**：Send 路径 7 次串行 DB 往返，dev MySQL RTT≈50-70ms → 上界 2.5s（环境如实界）；「原文送达 <500ms」为同机房部署目标（DIF-002 口径），3G 模拟 P95 归人工验收。
4. **翻译轮询**：mock 译文前缀 `[zh]`/`[en]`；worker 异步 → 轮询等待非固定 sleep。
5. **SSE 捕获**：`curl --max-time` 后台捕获防悬挂；补拉先于实时（after_id=0 拉全量）。

### 1.6 隔离与清理

`ci.sh --with-env` 冒烟前置位：**ADMIN 二态重置**（seed 的 ADMIN totp_status 归 NONE——Playwright 登录无 TOTP 第二段）+ **清 `rl:*` 限流键**（吃十段终态库时限流窗口已耗尽）。

### 1.7 明令禁止

- 禁 `toISOString()`（时区漂移是 E2E 假红第一来源）——时间断言用相对时间。
- 禁固定 sleep 替代轮询（翻译/邮件等异步链）。
- 禁断言实现细节（SQL 形态/内部字段序）——断业务结果（状态码/业务字段值/终态）。
- 禁跳过重建直接跑（十段假设初始世界）。
- 禁在限流段之前发无谓 /pub 请求（预算：60/min 含 F-HOS+聊天+F-REG 全部匿名请求）。

## 2. 可测性硬规则

- e2e.sh 是**一次性长链**（非独立用例）：段间有状态依赖（患者=注册创建、工单=注册开出、支付单=方案确认后创建），**不支持单段重跑**——重跑=整链重跑（make e2e-reset 后 bash scripts/dev/e2e.sh）。
- 每段头 `echo "==> [n/10] ..."`；断言失败即 `exit 1`（expect_status 非零即档位失败——ci.sh --with-env 闸门）。
- 幂等锚：重复注册 409、重复已读 404/幂等、白名单跳段 409——负路径断言与正路径同等地位。

## 3. 运行手册

```bash
# API 级全链（自管起停，~2min）
make e2e                      # = bash scripts/dev/e2e.sh
make e2e-reset                # 仅重建 e2e 库 + seed

# 浏览器冒烟（需先起 e2e 后端或走 ci.sh 编排）
make e2e-playwright           # 缺省只跑 web project（app project 归 ci.sh --with-env）

# 全档
bash scripts/ci.sh --with-env # 默认档 + e2e 十段 + 冒烟前置位 + Playwright + teardown
```

## 4. flaky 处理规则

1. TOTP 重放（90s 窗）→ 睡到下一 30s 边界重算（已内置）。
2. 限流相位 → 限流段移最后（结构性解决，已内置）。
3. 翻译/邮件异步未达 → 轮询 + 超时上限（禁无条件重跑）。
4. 连续 2 次假红 → 按确定性规则排查（时延口径/种子漂移），修脚本不修断言。

---

反链：

- [E2E 测试](/test/)
- [E2E 场景清单（medilink 册）](/test/e2e-medilink-%E5%9C%BA%E6%99%AF%E6%B8%85%E5%8D%95/)
