# v3.0 测试体系设计

> **作者**: 庞统（综合） + 司马懿（v30 草案 + 背靠背审查）
> **日期**: 2026-06-05（v3.0 合并版）
> **状态**: 待主公确认
> **覆盖设计文档**: #01 四相循环 / #07 Acquire-First / #08 Classify / #09 Rebuttal / #06 PM2 恢复 / #04 黑板协作 / #10 T3 需求探索 / #12 Pipeline / architecture-v3.0
> **替代**: 本文档替代 `docs/test-plan-e2e-v27.md`（v2.7 初版，2026-05-18）

---

## 〇、设计原则（主公三原则 + 仲达补充）

### 原则一：精准测试，最小代价覆盖修改内容

- 改了什么模块就跑什么测试，不默认跑全量
- 全量测试由用户手动触发
- 测试文件命名要让人一看就知道改了什么该跑什么

### 原则二：从设计文档正推覆盖

- 不是"有什么函数测什么"，而是"设计定义了哪些行为就测哪些"
- 每个设计文档对应的测试覆盖要显式标注

### 原则三：真实环境测试 + 测完清理

- E2E 不搞独立 daemon、独立进程、独立数据
- 最终汇聚点是真实 Agent，独立环境测不出真实链路
- 正确做法：**真实环境跑测试 → 测完清掉测试数据**

### 原则四：串行场景 + 并行压力分离（主公新增）

- **场景测试**：逐个执行，一个跑完收集结果、分析根因、解决后再跑下一个。用时间换可靠性
- **压力测试**：单独一轮，多任务并行，验证并发控制、全局上限等
- 两类测试不混在一起

---

## 一、问题诊断

### 1.1 现有测试的致命缺陷

| 缺陷 | 根因 | 后果 |
|------|------|------|
| E2E 全部写真实 data_root | TestClient 直连生产 DB，无隔离 | 每次跑 `pytest tests/` 残留 e2e-v27 项目，PM2 重启触发广播风暴 |
| E9-E14 cleanup 调不存在的 API | `/projects/{pid}/archive` 未实现 | 异常被吞，数据不清理 |
| 无分层 | 单文件 62K，所有测试混在一起 | 改个小 bug 要跑全量 |
| Agent 无意义响应 | 残留 pending 任务被 daemon 广播 | 浪费 token + 干扰正常工作 |
| 无数据清理能力 | 项目删不掉、测试邮件堆积 | 测试和生产数据混杂 |

### 1.2 Review 发现的额外问题

| 问题 | 说明 |
|------|------|
| **E14 编号冲突** | 设计文档 E14="AgentBusyError 分类"（3 个测试），代码 E14="Rollback"（1 个测试），同名不同义 |
| **设计文档严重滞后** | v31 新增 9 个测试类 + test_four_phase.py ~20 个测试未入文档 |
| **E15.2/E15.3 完全缺失** | Executor prompt / Reviewer prompt 无任何层级测试 |
| **E2E gate 不统一** | E1-E8 无 RUN_INTEGRATION gate，E9-E14 有。现已统一加 gate |

---

## 二、分层架构

### 2.1 三层测试金字塔

```
          ┌─────────────────────────────────┐
          │           E2E 测试              │  marker: e2e
          │    场景测试（串行）+ 压力测试     │  gate: RUN_INTEGRATION=1
          │    真实 daemon + 真实 Agent      │  测完清理
          ├─────────────────────────────────┤
          │          集成测试                │  marker: integration
          │       API 端点 + DB 交互        │  TestClient + tmp_path 隔离
          ├─────────────────────────────────┤
          │          单元测试                │  marker: unit (default)
          │         纯逻辑·快               │  mock 所有外部依赖
          └─────────────────────────────────┘
```

**关键规则：只有 E2E 层 spawn 真实 Agent。Unit 和 Integration 层不 spawn。**

### 2.2 各层定位

| 维度 | 单元测试 | 集成测试 | E2E 测试 |
|------|---------|---------|---------|
| **目标** | 验证单个函数/类的逻辑正确性 | 验证 API 端点 + DB 交互 | 验证完整链路（真实 Daemon + Agent） |
| **数据** | mock/fixture | tmp_path + 独立 SQLite | 真实 data_root + 测试前缀隔离 |
| **Agent** | ❌ 不 spawn | ❌ 不 spawn | ✅ 真实 spawn |
| **清理** | N/A（不产生数据） | tmp_path 自动清理 | 清理 API teardown 删除 |
| **速度** | <1s/测试 | 1-5s/测试 | 30-300s/测试 |
| **频率** | 每次提交 | 改了 API 层跑 | 部署前（手动触发） |

### 2.3 改动 → 测试映射（原则一）

**改了 X，应该跑哪些测试？**

| 改动的模块 | 必跑测试 | pytest 命令 |
|-----------|---------|------------|
| `src/blackboard/db.py`（数据模型） | unit/test_models + integration | `pytest tests/unit/test_models.py tests/integration/` |
| `src/blackboard/db.py`（状态机） | unit/test_state_machine + integration | `pytest -m state_machine` |
| `src/daemon/spawner.py`（classify） | unit/test_classify_outcome | `pytest -m classify` |
| `src/daemon/spawner.py`（acquire/release） | unit/test_spawner + e2e/test_spawner_e2e | `pytest tests/unit/test_spawner.py tests/e2e/test_spawner_e2e.py` |
| `src/daemon/ticker.py`（广播） | unit/test_broadcast_tracker + integration | `pytest -m broadcast` |
| `src/daemon/ticker.py`（超时） | unit/test_timeout + integration | `pytest tests/integration/test_ticker_integration.py -k timeout` |
| `src/daemon/router.py` | unit/test_router | `pytest tests/unit/test_router.py` |
| `src/daemon/counter.py` | unit/test_counter | `pytest tests/unit/test_counter.py` |
| `src/daemon/dispatcher.py` | unit/test_dispatcher + integration | `pytest tests/unit/test_dispatcher.py tests/integration/test_dispatcher_integration.py` |
| `src/api/mail_routes.py` | integration/test_api_mail | `pytest -m mail` |
| `src/api/blackboard_routes.py` | integration/test_api_tasks + reviews | `pytest tests/integration/ -k task` |
| `src/daemon/review.py` | unit/test_rebuttal + integration | `pytest -m review` |
| `src/api/toolchain_routes.py` | unit/test_toolchain + integration | `pytest tests/unit/test_toolchain.py tests/integration/ -k toolchain` |

**快速验证**：`pytest -m "not e2e"`（~2-5 分钟，不含 E2E）

---

## 三、E2E 执行策略（原则四展开）

### 3.1 两大类别

| 类别 | 目标 | 执行方式 | 文件 |
|------|------|---------|------|
| **场景测试** | 逐个验证每个场景的正确性 | **严格串行**：一个 case 跑完 → 收集结果 → 根因分析（如有问题）→ 修 → 再跑下一个 | `test_e2e_scenarios.py` |
| **压力测试** | 验证并发控制、全局上限、资源竞争 | **并行**：多任务同时 broadcast/dispatch/spawn | `test_e2e_stress.py` |

### 3.2 场景测试流程（串行）

```
┌─────────────────────────────────────────────────────┐
│  对每个 E2E 场景：                                    │
│                                                      │
│  1. 创建测试任务（e2e- 前缀）                          │
│  2. 等待任务完成（轮询状态直到终态）                     │
│  3. 收集结果                                          │
│     ├── ✅ 通过 → 记录 → 清理 → 执行下一个场景          │
│     └── ❌ 失败 → 根因分析 → 修复 → 重跑当前场景         │
│  4. 清理测试数据（与诊断分离）                          │
│                                                      │
│  关键：一个场景彻底完成（通过或确认不修）才进入下一个      │
└─────────────────────────────────────────────────────┘
```

**实现方式**：
- pytest 默认串行执行（不用 xdist）
- 每个测试函数是独立场景
- `scope="module"` 的 fixture 共享 daemon 连接
- 测试间无共享状态，各自创建/清理数据

### 3.3 压力测试流程（并行）

```
┌─────────────────────────────────────────────────────┐
│  压力测试（单独执行，场景测试全通过后）：                 │
│                                                      │
│  1. 同时创建 N 个任务（e2e-stress- 前缀）              │
│  2. 验证并发行为：                                     │
│     - counter 竞争：同 agent 并发 acquire → 第二个 blocked │
│     - 全局上限：5/5 满载 → 新任务排队                   │
│     - broadcast 并发：多任务同时广播 → 无死锁           │
│     - spawn 互斥：同 session 不重复 spawn              │
│  3. 全部完成后清理                                     │
└─────────────────────────────────────────────────────┘
```

### 3.4 数据清理与诊断分离

```
测试执行阶段 → 结果记录 → [可选] 数据保留用于诊断 → 清理阶段

- 测试执行时：记录所有创建的资源 ID（manifest）
- 通过时：立即清理
- 失败时：保留数据供诊断，诊断完成后手动/脚本清理
- 兜底：session 结束时 atexit 全量清理 e2e- 前缀
```

### 3.5 前置依赖：数据清理 API

E2E 测试能安全运行的前提：

| 需求 | API | 状态 |
|------|-----|------|
| 项目物理删除 | `DELETE /api/projects/{pid}?physical=true` | ✅ 已实现（src/api/project_routes.py） |
| 邮件批量删除 | `DELETE /api/mail?prefix=e2e-` | ✅ 已实现（src/api/mail_routes.py） |

---

## 四、E2E 场景清单

> 编号规则：S = Scenario（场景测试），ST = Stress（压力测试）
> 原文档 E1-E15 重新编号，解决 E14 冲突

### 4.1 场景测试（S1-S15，串行执行）

#### S1: 项目管理（4 个测试）| 对应 design: architecture-v3.0 §5

- S1.1 创建项目（POST /api/projects）
- S1.2 项目列表（GET /api/projects）
- S1.3 自动发现（含 blackboard.db 的目录自动注册）
- S1.4 归档项目（status=archived，目录不移动）

#### S2: Task CRUD + 状态机（5 个测试）| 对应 design: architecture-v3.0 §5.2

- S2.1 创建 Task（POST /api/projects/{pid}/tasks）
- S2.2 查询 Task（GET，expand=all）
- S2.3 合法状态转换（pending → claimed → working → review → done）
- S2.4 非法转换拒绝（409 + valid_transitions）
- S2.5 列表筛选（status/assignee/parent_task）

#### S3: SubTask 父子关系（4 个测试）| 对应 design: architecture-v3.0 §5

- S3.1 创建父 Task + 3 个子 Task
- S3.2 list_subtasks 验证
- S3.3 top_level_tasks 排除子 Task
- S3.4 子 Task 的 stage 字段

#### S4: Stage 进度（3 个测试）| 对应 design: architecture-v3.0 §5

- S4.1 带 stages_json 的父 Task → 子 Task 分配到各 stage → progress 端点
- S4.2 空 stage 的父 Task 进度
- S4.3 stage 分组统计验证

#### S5: 父 Task 状态聚合（6 个测试）| 对应 design: architecture-v3.0 §6.1

- S5.1 all done → 父 done
- S5.2 has review → 父 review
- S5.3 has working → 父 working
- S5.4 all pending → 父 pending
- S5.5 cancelled 子 Task 排除
- S5.6 手动状态（cancelled 父 Task）不被覆盖

#### S6: 依赖链（3 个测试）| 对应 design: architecture-v3.0 §6.1

- S6.1 Task B depends_on Task A，A done 后 B 自动 pending（Ticker 推进）
- S6.2 A 未完成时 B 保持 blocked
- S6.3 多层依赖（A → B → C）

#### S7: 超时回收（2 个测试）| 对应 design: architecture-v3.0 §6.1

- S7.1 claimed 超时 → pending
- S7.2 working 超时 → failed

#### S8: Mail（7 个测试）| 对应 design: architecture-v3.0 §12

- S8.1 发送 Mail（inform 类型 → 自动 done）
- S8.2 发送 Mail（task-assign 类型 → pending）
- S8.3 Mail 列表 + 筛选（from/to/unread）
- S8.4 Mail 详情（含 comments）
- S8.5 标记已读
- S8.6 标记已执行
- S8.7 Mail summary + agents 列表

#### S9: 真实 Agent 调度（3 个测试）| 对应 design: #01 四相循环 + architecture-v3.0 §14

- S9.1 创建简单 Task → Ticker 调度 → Agent spawn → Agent 回写 working → 完成任务（#01）
- S9.2 创建 review 类型 Task → 调度到 simayi-challenger → 回写 review 结果（#01）
- S9.3 Guardrail 拦截（不合法任务 → Agent 拒绝执行）（architecture-v3.0 §14）

**Agent 任务内容**：简单的"echo hello"级别任务，10-30 秒完成

#### S10: 全链路集成（2 个测试）| 对应 design: #01 四相循环

- S10.1 逻辑链（创建项目 → 父 Task + 子 Task → Ticker tick → 依赖推进 → 聚合刷新 → Mail 通知 → 验证完整状态链）
- S10.2 Agent 全链（含真实 Agent spawn + 执行 + 产出 + review）

#### S11: Spawner Acquire-First（6 个测试）| 对应 design: #07

- S11.1 Phase 0 Pre-acquire revive：任务 timeout/failed → Phase 0 自动 revive → Phase 1 acquire 成功
- S11.2 Phase 0 假死检测：status=running + lock PID 死 → Phase 0 自动 revive
- S11.3 Phase 1 Counter acquire 互斥：同 agent 并发 spawn → 第二个 AgentBusyError(reason=counter_blocked)
- S11.4 Phase 2 Session check 锁保护：counter acquire 后 → session check 在锁保护下执行 → session locked → release counter → AgentBusyError(reason=session_locked)
- S11.5 Phase 2 Blockers 收集：多 blocker 并列收集（locked + compact）→ 返回全部 blockers
- S11.6 Phase 3 on_checks_passed 异常回滚：on_checks_passed 抛异常 → counter 自动 release

#### S12: 超时统一处理（4 个测试）| 对应 design: #08 §3.7

- S12.1 crash_limit 统一检查（working）：executor crash 3 次/30min → _check_timeouts 标 failed
- S12.2 crash_limit 统一检查（review）：reviewer crash 3 次/30min → _check_timeouts 标 failed
- S12.3 updated_at fallback：mail auto-working 无 started_at/claimed_at → updated_at fallback 生效 → 超时回收
- S12.4 process_dead 对 review 状态：review agent 进程死 → 保持 review 状态 → 等 _dispatch_reviews 处理

#### S13: Compact Hanging（3 个测试）| 对应 design: #07 §4.5

- S13.1 compact_hanging release counter：compact 等超限 → compact_hanging → release counter → 任务保持 working
- S13.2 compact_hanging 后 ticker 重新 dispatch：compact_hanging → ticker _check_timeouts 检测超时 → 推回 pending → 重新 dispatch
- S13.3 retry 遇 session busy 释放 counter：_do_retry 遇 AgentBusyError → release counter → 任务保持 working → ticker 重新 dispatch

#### S14: AgentBusyError 分类（3 个测试）| 对应 design: #07 §2

> ⚠️ 原设计文档 E14="AgentBusyError 分类"与代码中 E14="Rollback" 冲突，此处统一修正

- S14.1 counter_blocked：counter acquire 失败 → AgentBusyError(reason=counter_blocked)
- S14.2 session_locked/running/compacting：Phase 2 各种 blocker → AgentBusyError 携带具体 reason + detail.blockers
- S14.3 Dispatcher 错误区分：dispatcher 捕获 AgentBusyError → 日志记录具体原因 → 路由决策写入 routing_decisions

#### S15: Crash Rollback（1 个测试）| 对应 design: #07 §4.5

> ⚠️ 原代码中 E14="Rollback"，现独立编号为 S15

- S15.1 crash 后 current_agent 回退验证：Agent crash → current_agent 指针回退 → 任务可被重新 dispatch

#### S16: 广播认领扩展（2 个测试）| 对应 design: architecture-v3.0 §8.3

- S16.1 不匹配 Agent 写 observation comment（非 NO_REPLY）
- S16.2 匹配 Agent 正常认领

#### S17: Pause/Resume（1 个测试）| 对应 design: architecture-v3.0 §5.2

- S17.1 paused → resumed_from 状态流转

#### S18: Cancelled Restart（1 个测试）| 对应 design: architecture-v3.0 §5.2

- S18.1 cancelled → pending 重启

#### S19: Retry Chain（1 个测试）| 对应 design: #08 §3.3

- S19.1 failed → pending retry 链路

#### S20: Full Lifecycle with Review（1 个测试）| 对应 design: #09

- S20.1 创建 → 认领 → 执行 → review → rebuttal → done 全链路

#### S21: Cache Headers（2 个测试）| 对应 design: architecture-v3.0 §7

> 注：原代码有 3 个独立测试（html_no_cache, js_immutable, css_immutable），设计中 S21.2 将 JS+CSS 合并为"静态资源 immutable 缓存"一个场景，因为两者验证逻辑一致

- S21.1 HTML 不缓存
- S21.2 JS/CSS immutable 缓存（原 2 个测试合并）

#### S22: 工具链事件中枢（13 个测试）| 对应 design: #13 工具链与开发流程 §16

- S22.1 PR opened → 司马懿收到 Review 请求 Mail [integration]
  - 验证：Webhook 返回 200，Mail 创建成功，收件人 simayi-challenger，模板填充正确（PR号、标题、作者、分支）
- S22.2 Review APPROVED → PR 作者收到通过通知 [integration]
  - 验证：Mail 收件人正确（PR 作者），模板填充 review result
- S22.3 Review REQUEST_CHANGES → PR 作者收到不通过通知 [integration]
  - 验证：Mail 包含 review body，收件人正确
- S22.4 Review COMMENTED → 不创建 Mail（忽略普通评论） [integration]
  - 验证：返回 200 但无 Mail 创建
- S22.5 Issue assigned → 被指派人收到任务通知 [integration]
  - 验证：Mail 收件人正确（被指派人），模板含 Issue 标题/编号信息
  - 验证：assigned 给非 Agent 用户 → 忽略，返回 200
- S22.6 CI 失败评论 → PR 作者收到 CI 失败通知 [integration]
  - 验证：新格式（`[CI]` 前缀）和旧格式（`❌ **CI 失败**`）均能匹配，Mail 含错误摘要，收件人为 PR 作者
- S22.7 非 CI 评论 → 忽略不处理 [integration]
  - 验证：不匹配 CI 评论格式的普通评论不创建 Mail，返回 200
- S22.8 部署失败 Issue（标题含"部署失败"）→ 庞统+姜维收到通知 [integration]
  - 验证：两个收件人都收到 Mail（pangtong-fujunshi、jiangwei-infra），模板含 Issue 信息
- S22.9 幂等检查：同一 delivery_id 重复投递 → 忽略 [unit]
  - 验证：第二次返回 duplicate/ignored，不重复创建 Mail
- S22.10 签名验证：错误签名 → 403 [unit]
  - 验证：配置 secret 后，signature 不匹配返回 403；secret 为空时跳过验签
- S22.11 未知事件类型 → 忽略返回 200 [unit]
  - 验证：未知 event 不报错，返回 200，不创建 Mail
- S22.12 畸形 payload → 返回 200 不崩溃 [unit]
  - 验证：非法 JSON / 缺少必要字段 → 返回 200，不崩溃，不创建 Mail
- S22.13 风险级别自动判定：改动含高风险文件 → high [unit]
  - 验证：changed_files 包含 spawner/dispatcher/router 等高风险路径时 risk_level=high；普通文件为 standard

#### S23: 真实 Gitea Webhook 端到端验证（8 个测试）| 对应 design: #13 §16

> 需要 Gitea admin 权限或已配置 Webhook 的仓库，真实 PR 流程触发 Webhook
> 前置条件：Gitea Webhook active=true（姜维已配）、GITEA_TOKEN 已配、测试账号可用
> 全部完成后统一清理：关闭 PR、删除测试分支和 Issue

- S23.1 创建真实 PR → Gitea 发送 Webhook → daemon 收到并创建 Review Mail [E2E]
  - 前置：在 moziplus-v2 仓库创建分支 + 提交（改动含高风险文件如 spawner.py）+ 开 PR
  - 验证：daemon 日志有 `POST /webhook/gitea 200`，司马懿收到 Review 请求 Mail
  - 验证：Mail 内容含正确的 PR 号、标题、作者、分支名
- S23.2 Gitea Review 提交 → PR 作者收到通知 [E2E]
  - 操作：在 Gitea 上用 simayi-challenger 账号提交 APPROVED review
  - 验证：PR 作者收到 Review 通过 Mail
- S23.3 Gitea Review 驳回 → PR 作者收到通知 [E2E]
  - 操作：在 Gitea 上用 simayi-challenger 账号提交 REQUEST_CHANGES review
  - 验证：PR 作者收到 Review 驳回 Mail，包含 review body
- S23.4 Gitea [CI] 评论 → PR 作者收到 CI 失败通知 [E2E]
  - 操作：在 Gitea PR 上发 `[CI] 构建失败` 格式的评论
  - 验证：PR 作者收到 CI 失败通知 Mail
- S23.5 Gitea Issue 指派 → 被指派人收到通知 [E2E]
  - 操作：在 Gitea 创建 Issue 并 assigned 给 zhangfei-dev
  - 验证：zhangfei-dev 收到 Issue 指派 Mail
- S23.6 风险级别判定：PR 改动含高风险文件 → risk_level=high [E2E]
  - 前置：S23.1 的 PR 改动文件包含 `src/daemon/spawner.py` 或 `src/daemon/ticker.py`（高风险路径）
  - 验证：GITEA_TOKEN 已配，daemon 能调 Gitea API 获取 PR 文件列表
  - 验证：Review Mail 中标注 risk_level=high（而非 standard）
- S23.7 幂等检查真实场景：Gitea 重试 Webhook → 不重复创建 Mail [E2E]
  - 操作：对同一 PR 触发重复 Webhook（或利用 Gitea Webhook 的 Test Delivery 功能重复发送）
  - 验证：daemon 日志显示 duplicate，Mail 不重复创建
  - 目的：从 E2E 角度验证 S22.9 的 unit 测试，真实场景才能暴露并发竞态问题
- S23.8 清理：关闭 PR，删除测试分支和 Issue
  - 操作：关闭所有测试 PR，删除测试分支，关闭测试 Issue
  - 验证：Gitea 仓库无测试残留（e2e- 前缀的 PR/Issue/分支均已清理）

**场景测试合计：85 个测试**

### 4.2 压力测试（ST1-ST3）

#### ST1: 并发 Counter 竞争

- 同时创建 3 个任务指定同一 Agent → 第 2、3 个 AgentBusyError(reason=counter_blocked)
- 第 1 个完成后 counter release → 第 2 个 acquire 成功
- 验证 counter release 后下一个 acquire 成功

#### ST2: 全局并发上限

- 同时创建 5 个任务指定不同 Agent → 全部 acquire 成功（5/5）
- 第 6 个任务 → 被拒绝或排队
- 验证全局计数器准确

#### ST3: Broadcast 并发

- 同时广播 3 个任务 → 全部任务在 5min 内到达终态
- 验证无死锁（通过全部终态判定）
- 验证广播计数器正确递增

**压力测试合计：3 个测试组**

### 4.3 Prompt 集成测试（P1，延后）

> 以下场景需要验证 Agent prompt 行为，标 P1 延后

- P1.1 Executor prompt 任务式指挥：executor 不再收到步骤列表，收到意图+终态+约束 → 自主决策步骤
- P1.2 Reviewer prompt 挑战者思维：reviewer 审查时用挑战者视角 + confidence 自评

---

## 五、测试文件组织

### 5.1 目录结构

> 说明：以下为迁移完成后的目标结构。标注"待创建"的文件目前逻辑在其他文件中，迁移时需拆出。

```
tests/
├── conftest.py                          # 全局 fixture + marker 注册 + atexit 清理兜底
├── unit/                                # 单元测试（不 spawn 真实 Agent）
│   ├── test_state_machine.py            # 状态机转换（architecture-v3.0 §5.2）
│   ├── test_classify_outcome.py         # Classify 场景（#08 §3）
│   ├── test_spawner.py                  # Spawner + Acquire-First（#07 §2）
│   ├── test_router.py                   # Router 确定性路由
│   ├── test_counter.py                  # Counter 并发控制
│   ├── test_broadcast_claim.py          # BroadcastRound 逻辑 + bug fix 回归
│   ├── test_dispatcher.py               # Dispatcher 调度逻辑
│   ├── test_review.py                   # Review + Rebuttal 逻辑
│   ├── test_inbox.py                    # Inbox JSONL 解析
│   ├── test_models.py                   # 数据模型（待创建，当前逻辑在 test_blackboard.py）
│   ├── test_experience.py               # 经验蒸馏
│   ├── test_bootstrap.py                # Bootstrap 上下文
│   ├── test_health.py                   # 健康检查
│   ├── test_cli.py                      # CLI 命令
│   ├── test_skill_system.py             # Skill 匹配/推荐
│   └── test_guardrails.py               # Guardrail 规则（待创建，当前逻辑在 test_router.py + test_review_integration.py）
│   ├── test_toolchain.py                # 工具链事件中枢：模板引擎 + 幂等检查 + 风险判定（#13 §16）
├── integration/                         # 集成测试（不 spawn 真实 Agent）
│   ├── test_api.py                      # 项目管理 + Daemon API
│   ├── test_api_mail.py                 # 邮件 API（A1-A10 防御 + 功能）
│   ├── test_main.py                     # 健康端点 + 配置
│   ├── test_dispatcher_integration.py   # Dispatcher 回滚
│   ├── test_review_integration.py       # Review pipeline + scoring
│   ├── test_ticker_integration.py       # Ticker 主循环 + 广播 + 超时 + 依赖推进
│   ├── test_sse.py                      # SSE broker + hook manager
│   ├── test_toolchain_integration.py    # 工具链 Webhook API 端点 + Mail 创建
│   └── test_v27_subtasks.py             # 子任务集成
└── e2e/                                 # E2E 测试（spawn 真实 Agent）
    ├── conftest.py                      # E2E fixture：清理 API + atexit 兜底 + manifest
    ├── test_four_phase.py               # #01 四相循环
    ├── test_e2e_scenarios.py            # S1-S22 场景测试（串行）
    ├── test_e2e_stress.py               # ST1-ST3 压力测试（并行）
    └── test_spawner_e2e.py              # Spawner 进程管理（timeout kill + spawn failure）
```

### 5.2 现有测试迁移映射

| 现有文件 | 去向 | 处理 |
|---------|------|------|
| test_e2e_v27.py E1-E8 | integration/ | 重写为 API 集成测试，用 tmp_path 隔离 |
| test_e2e_v27.py E9-E14 | e2e/test_e2e_scenarios.py | 按 S9-S15 重新编号 |
| test_e2e_v31.py E94-E98 | e2e/test_e2e_scenarios.py | 按 S16-S21 编号 |
| test_e2e_v31.py E10c/E10d | e2e/test_e2e_scenarios.py | 按 S19-S20 编号 |
| test_e2e_v31.py E15a/E15b | e2e/test_e2e_scenarios.py | 按 S16.1-S16.2 编号（Prompt v3 broadcast） |
| test_four_phase.py | e2e/test_four_phase.py | 加 RUN_INTEGRATION gate |
| test_spawner_e2e.py | e2e/test_spawner_e2e.py | 已有 gate ✅ |
| test_api.py | integration/ | 保持 |
| test_api_mail.py | integration/ | 保持 |
| test_main.py | integration/ | 保持 |

### 5.3 pytest marker 定义

```python
# conftest.py
def pytest_configure(config):
    markers = {
        "unit": "单元测试：纯逻辑，mock 外部依赖",
        "integration": "集成测试：API 端点 + 隔离数据",
        "e2e": "端到端测试：真实环境（RUN_INTEGRATION=1 触发）",
        "slow": "慢测试（>5s）",
        "broadcast": "广播认领相关",
        "mail": "邮件系统相关",
        "state_machine": "状态机转换",
        "classify": "Classify Outcome 相关",
        "review": "审查/Rebuttal 相关",
    }
    for name, desc in markers.items():
        config.addinivalue_line("markers", f"{name}: {desc}")
```

### 5.4 Gate 保护

| 层级 | Gate | 说明 |
|------|------|------|
| unit | 无 | 纯 mock，安全 |
| integration | 无 | tmp_path 隔离，安全 |
| e2e | `RUN_INTEGRATION=1` | 所有 e2e 测试统一 gate，`@skip_no_integration` |

---

## 六、设计文档 → 测试覆盖映射（原则二）

### 6.1 覆盖矩阵

| 设计文档 | 关键行为 | 单元测试 | 集成测试 | E2E 测试 |
|---------|---------|---------|---------|---------|
| **architecture-v3.0.md** | 状态机、数据模型、API 契约 | P0 | P0 | — |
| **#01 四相循环** | spawn→execute→classify→retry/advance | P1 | — | P0（S9-S10） |
| **#07 Acquire-First** | Phase 0-4、AgentBusyError 区分 | P0 | P1 | P1（S11-S15） |
| **#08 Classify Outcome** | A0-A14 + B1-B4 场景 | P0 | P1 | P1（S12） |
| **#09 Rebuttal** | 审查流水线、2 轮反驳 | P1 | P0 | P1（S20） |
| **#06 PM2 恢复** | 崩溃后状态恢复、zombie 检测 | P1 | — | P0 |
| **#04 黑板协作** | @mention、comment、产出、review | P1 | P0 | P1 |
| **#10 T3 需求探索** | 苏格拉底式对话、需求规格化 | P1（暂无测试文件） | — | — |
| **#12 Pipeline** | task_type 路由、状态机约束 | P2 | P2 | — |
| **#13 工具链与开发流程 §16** | Webhook 接收、事件路由、模板填充、Mail 创建、幂等检查、签名验证 | P0 | P0 | P1（S22） |

### 6.2 模块 → 测试层级 → 优先级

| 模块 | 单元 | 集成 | E2E | 对应设计文档 |
|------|------|------|-----|-------------|
| 状态机转换 | P0 | P0 | — | architecture-v3.0 §5.2 |
| Classify Outcome | P0 | P1 | P1 | #08 §3 |
| 广播认领 | P0 | P1 | P0 | architecture-v3.0 §8.3 |
| 邮件系统 | P1 | P0 | P1 | architecture-v3.0 §12 |
| Acquire-First | P0 | P1 | P1 | #07 §2 |
| 四相循环 | P1 | — | P0 | #01 §2 |
| Spawner 进程管理 | P0 | P1 | P1 | architecture-v3.0 §6.3 |
| Counter 并发控制 | P0 | P1 | — | architecture-v3.0 §8.2 |
| Router 路由 | P0 | P1 | — | architecture-v3.0 §8.1 |
| Review/Rebuttal | P1 | P0 | P1 | #09 §2 |
| Guardrail | P0 | P1 | — | architecture-v3.0 §14 |
| 数据模型 | P0 | P0 | — | architecture-v3.0 §5 |
| 依赖推进 | P0 | P1 | — | architecture-v3.0 §6.1 |
| 超时检测 | P0 | P1 | P1 | architecture-v3.0 §6.1 |
| @mention / 协作 | P1 | P0 | P1 | #04 |
| Inbox JSONL | P0 | P1 | — | architecture-v3.0 §15 |
| SSE 推送 | P1 | P0 | — | architecture-v3.0 §15 |
| Bootstrap 上下文 | P1 | P1 | — | architecture-v3.0 §10 |
| PM2 恢复 | P1 | — | P0 | #06 |
| 工具链事件中枢 | P0 | P0 | P1 | #13 §16 |

---

## 七、E2E 数据隔离与清理

### 7.1 各层隔离策略

| 层级 | 策略 | 清理方式 |
|------|------|---------|
| 单元 | mock / 不涉及 IO | 无残留 |
| 集成 | `tmp_path` + 独立 SQLite | fixture teardown 自动清理 |
| E2E | 真实 data_root + `e2e-` 前缀隔离 | 清理 API + atexit 兜底 |

### 7.2 E2E 清理机制

```python
# tests/e2e/conftest.py

@pytest.fixture(scope="session")
def e2e_session_prefix():
    """Session 级唯一前缀"""
    return f"e2e-{uuid.uuid4().hex[:6]}-"

@pytest.fixture(autouse=True)
def e2e_cleanup(request):
    """每个测试后清理，与诊断分离"""
    created_pids = []
    
    def _track(pid):
        created_pids.append(pid)
    
    yield  # 测试执行
    
    # 清理阶段
    for pid in created_pids:
        requests.delete(f"http://localhost:8083/api/projects/{pid}?physical=true")

@pytest.fixture(scope="session", autouse=True)
def e2e_session_cleanup(e2e_session_prefix):
    """Session 结束时兜底清理"""
    yield
    # 批量清理所有 e2e- 前缀
```

### 7.3 atexit 兜底

```python
# tests/conftest.py（全局）

import atexit

def _emergency_cleanup():
    """进程崩溃时的最后防线"""
    # 批量删除所有 e2e- 前缀的项目和邮件
    pass

atexit.register(_emergency_cleanup)
```

---

## 八、通过标准

| 类别 | 标准 |
|------|------|
| **场景测试（S1-S23）** | 85 个全部通过。失败必须分析根因并修复后才继续 |
| **压力测试（ST1-ST3）** | 3 个全部通过。验证并发控制无死锁 |
| **P0 必须全部通过** | S1-S8 + S10 + S11-S14 |
| **P1 通过率 ≥ 80%** | S9 + S15-S21 + ST1-ST3（依赖真实 Agent，可能受网络/环境干扰） |
| **P1 延后** | P1.1/P1.2（Prompt 集成，待 Agent prompt 稳定后补） |
| **速度** | 单元 < 30s，集成 < 5min，E2E 场景 < 60min，E2E 压力 < 30min |
| **零残留** | 测试运行前后，`GET /projects?search=e2e-` 返回 0 条 |

---

## 九、实施计划

### Phase 0：数据清理 API（前置依赖）

| 任务 | API | 优先级 |
|------|-----|--------|
| 项目物理删除 | `DELETE /api/projects/{pid}?physical=true` | P0 |
| 邮件批量删除 | `DELETE /api/mail?prefix=e2e-` | P0 |

### Phase 1：基础设施

| 任务 | 说明 |
|------|------|
| conftest.py 更新 | marker 注册 + atexit 清理兜底 + tmp_path fixture |
| E2E conftest | 清理 API fixture + session 兜底 + manifest |
| Gate 统一 | 所有 E2E 文件加 `@skip_no_integration` |

### Phase 2：P0 单元测试（已有 394 passed）

| 任务 | 对应设计文档 |
|------|------------|
| test_state_machine.py | architecture-v3.0 §5.2 |
| test_classify_outcome.py | #08 §3 |
| test_spawner.py（含 Acquire-First） | #07 §2 |
| test_broadcast_claim.py | architecture-v3.0 §8.3 |
| test_counter.py | architecture-v3.0 §8.2 |
| test_guardrails.py（实际在 test_router.py + test_review_integration.py） | architecture-v3.0 §14 |

### Phase 3：集成测试（已有 155 passed）

| 任务 | 说明 |
|------|------|
| test_api_mail.py | A1-A10 + 批量删除（已到位） |
| test_ticker_integration.py | 广播 + 超时 + 依赖推进 |
| test_spawner_e2e.py | acquire/release + 监控（E2E 层） |
| test_dispatcher_integration.py | Router + Guardrail 调度 |

### Phase 4：E2E 场景测试（串行）

| 任务 | 覆盖设计文档 |
|------|------------|
| S1-S8 API 层场景 | architecture-v3.0 |
| S9-S10 Agent 调度 | #01 四相循环 |
| S11-S15 Spawner/Acquire | #07 + #08 |
| S16-S21 扩展场景 | 各设计文档 |

### Phase 5：E2E 压力测试（并行）

| 任务 | 说明 |
|------|------|
| ST1 Counter 竞争 | 同 agent 并发 |
| ST2 全局上限 | 多 agent 并发 |
| ST3 Broadcast 并发 | 多任务广播 |

---

## 十、约束和风险

### 10.1 约束

1. **只有 E2E spawn 真实 Agent**：Unit 和 Integration 层不 spawn
2. **E2E 用真实环境**：真实 daemon + 真实 Agent，`e2e-` 前缀隔离
3. **场景测试串行**：一个完成再下一个，用时间换可靠性
4. **压力测试并行**：单独一轮，场景测试全通过后执行
5. **测试不依赖执行顺序**：每个测试独立创建/清理数据
6. **全量 E2E 手动触发**：`RUN_INTEGRATION=1 pytest tests/e2e/`
7. **测试数据前缀规范**：所有 E2E 项目名以 `e2e-` 开头

### 10.2 风险

| 风险 | 缓解 |
|------|------|
| E2E 场景测试影响生产 Agent | `e2e-` 前缀项目，串行执行减少干扰 |
| 清理 API 失败导致残留 | atexit + session fixture 双保险 |
| E2E 测试不稳定（Agent 响应慢） | 充分 timeout + retry + 失败时保留数据供诊断 |
| 迁移现有测试引入回归 | 先迁移后验证，不删旧文件直到新测试全绿 |

---

## 十一、变更记录

| 日期 | 版本 | 变更 |
|------|------|------|
| 2026-05-18 | v2.7 | 初版 E1-E10（仲达） |
| 2026-06-01 | v2.8 | 新增 E11-E15（庞统） |
| 2026-06-05 | v3.0 | 全面重写：三层分离 + 主公四原则 + 背靠背 review 修正 + E14 编号冲突修复 + 场景/压力分离 + 串行执行策略（庞统综合，基于仲达 v30 草案） |
| 2026-06-05 | v3.0.1 | 仲达 review 修订：修正幽灵文件 4 处 + 补 E15a/b 迁移映射 + 清理 API 状态改已实现 + S9.3 设计文档标注 + ST 验收标准细化 + #10 暂无文件标注 + S21 合并说明 |
| 2026-06-07 | v3.0.3 | 新增 S23 真实 Gitea Webhook E2E（8 个测试），覆盖 T02 真实投递 + T03 风险级别判定 + 幂等真实场景 + 统一清理。采纳仲达 S1（幂等 E2E）+ S2（统一清理） |

### v3.0 vs v2.8 关键变化

| 维度 | v2.8 | v3.0 |
|------|------|------|
| E2E 执行策略 | 无明确规定 | 串行场景 + 并行压力分离 |
| 编号体系 | E1-E15（E14 冲突） | S1-S21 + ST1-ST3（无冲突） |
| Agent spawn | E1-E8 无 gate | 统一 RUN_INTEGRATION gate |
| 数据隔离 | 未提及 | 真实 data_root + 清理 API + atexit 兜底 |
| 设计覆盖 | 只管 E2E | 全三层 + 设计文档正推映射 |
| 文件组织 | 两大文件 | 按模块拆分 + 命名规范 |
