# v2.6 测试计划

**版本**: v2.6.0-test-plan
**作者**: 司马懿（质量总监）🗡️
**日期**: 2026-05-17
**状态**: F1-F5 已评审，F6-F18 待编码

---

## 一、测试基础设施

### 1.1 当前配置

- **框架**: pytest（已有 `tests/` 目录，97 个用例全部通过）
- **Fixture**: `tmp_path`（SQLite 临时数据库）、`TestClient`（FastAPI）
- **Mock 策略**: 目前无 mock，全部用真实 SQLite 内存/临时文件
- **运行方式**: `python3 -m pytest tests/ -q`

### 1.2 测试分层

| 层级 | 覆盖范围 | 何时执行 |
|------|---------|---------|
| **单元测试** | 每个 F 的内部逻辑 | 每写完一个 F |
| **集成测试** | F1-F14 端到端链路 | F14 完成后 |
| **E2E 验收** | 完整系统含前端 | F18 完成后 |

### 1.3 测试规范

- 每个 F 必须有对应的 `test_*.py`，测试文件与模块同名
- 测试类按功能分组：`class TestTaskCRUD`、`class TestTransitions` 等
- P0 测试必须有，P1 测试建议有但不强阻塞
- 测试必须可重复运行（不依赖外部状态、不依赖时序）
- 新增测试必须在 `tests/` 目录下，不放在 `src/` 内

---

## 二、F1-F5 单元测试评审（已完成）

### 现有覆盖情况

| F | 测试文件 | 用例数 | P0 覆盖 | 评审 |
|---|---------|--------|---------|------|
| F1 骨架 | `test_main.py` | 10/10 | ✅ 启动、健康端点、配置、Swagger | 通过 |
| F2 黑板 | `test_blackboard.py` | 39/39 | ✅ CRUD、状态机、并发、WAL、事件 | 通过（附 2 条意见） |
| F3 多项目 | `test_registry.py` | 11/11 | ✅ 创建/删除/归档/持久化/黑板的联动 | 通过 |
| F4 CLI | `test_cli.py` | 14/14 | ✅ 8 子命令 + admin + 错误处理 | 通过 |
| F5 API | `test_api.py` | 23/23 | ✅ 全端点 CRUD + 状态码 + SSE 连接 | 通过（附 1 条意见） |

**总计**: 97/97 通过，质量合格。

### 评审意见（F1-F5）

#### BUG-1 [P1] `/api/daemon/status` 端点重复注册
- **位置**: `main.py` 第 99-111 行直接定义了 `daemon_status` 端点，同时 `daemon_routes.py` 也定义了同名路由
- **现象**: pytest 输出 `Duplicate Operation ID daemon_status` warning
- **修复**: 删除 `main.py` 中的 `/api/daemon/status` 端点定义，统一由 `daemon_routes.py` 提供
- **影响**: 不影响功能，但 Swagger 文档会混乱

#### OBS-1 [P2] `operations.py` 的 `_conn()` 每次操作都新建连接
- **位置**: `operations.py` 全文，每次方法调用 `self._conn()` → `sqlite3.connect()`
- **现状**: 单次操作用完即关，连接不池化
- **评估**: v2.6 阶段可接受（SQLite WAL + busy_timeout 足够），但如果后续 F6 ticker 30s 轮询 + 多项目并发，建议 F6 阶段引入连接池或 `per-thread` 连接复用
- **行动**: 不阻塞当前进度，F6 编码时再评估性能

#### OBS-2 [P2] `queries.py` 的 `blocked_tasks_with_deps()` 和 `pending_dispatchable()` 存在 SQL 注入风险
- **位置**: `queries.py` 第 52 行和第 71 行，使用 `f-string` 拼接 SQL
- **现象**: `f"SELECT ... WHERE id IN ({','.join('?' * len(deps))})"` — 虽然 `deps` 来自数据库的 `depends_on` 字段（不是用户输入），且用了参数化占位符，但 `f-string` 拼接 SQL 是坏习惯
- **评估**: 当前实际安全（因为值都来自参数化 `?`），但风格不好
- **行动**: 不阻塞，但后续统一改为纯参数化写法

---

## 三、F6-F18 测试计划（逐模块）

### F6 Daemon Ticker — `test_ticker.py`

| # | 测试用例 | P0 | 验证点 |
|---|---------|-----|-------|
| T1 | tick 循环正常运行 | ✅ | 30s 间隔（测试中加速），不阻塞 event loop |
| T2 | scan_tasks 检测 pending | ✅ | pending → 触发调度 |
| T3 | 依赖推进 | ✅ | blocked → pending（依赖 done 后） |
| T4 | events 写入 | ✅ | 每次 tick 写 `daemon_tick` 事件 |
| T5 | 多项目轮询 | ✅ | tick 遍历所有 active 项目 |
| T6 | tick 异常不中断 | P1 | 单次 tick 抛异常 → 下次 tick 继续 |
| T7 | 手动 tick 端点 | P1 | POST /api/daemon/tick 触发即时 tick |

### F7 Inbox JSONL Watcher — `test_inbox.py`

| # | 测试用例 | P0 | 验证点 |
|---|---------|-----|-------|
| T1 | 写入+消费 | ✅ | Agent 写 JSONL → 1s 内 Daemon 读取并处理 |
| T2 | truncate | ✅ | 消费后清空文件 |
| T3 | 并发写入 | ✅ | 多 Agent 同时写不同行 |
| T4 | 损坏行恢复 | P1 | 非法 JSON 行跳过不崩溃 |
| T5 | 空文件处理 | P1 | 空文件不触发处理 |

### F8 健康检查 — `test_health.py`

| # | 测试用例 | P0 | 验证点 |
|---|---------|-----|-------|
| T1 | 正常场景 | ✅ | 有变更 → 无告警 |
| T2 | 僵尸检测 | ✅ | 连续 N tick 无变更 → observation 写入 |
| T3 | 恢复场景 | ✅ | 僵尸后有变更 → 告警解除 |
| T4 | 多项目独立检测 | P1 | 项目 A 僵尸不影响项目 B |

### F9 Agent 调度器 — `test_dispatcher.py`

| # | 测试用例 | P0 | 验证点 |
|---|---------|-----|-------|
| T1 | 三级决策树 | ✅ | Daemon task → Full task → Sub task 正确分流 |
| T2 | 调度不阻塞 | ✅ | spawn 用 `asyncio.create_subprocess_exec` |
| T3 | 队列满拒绝 | ✅ | active agent 数达上限 → 不调度 |
| T4 | 任务优先级排序 | P1 | 高优先级先调度 |

### F9 Agent Spawner — `test_spawner.py`

| # | 测试用例 | P0 | 验证点 |
|---|---------|-----|-------|
| T1 | spawn 成功 | ✅ | 子进程启动 + session 注册 |
| T2 | 超时处理 | ✅ | 超时后 kill + task_attempt 记录 |
| T3 | spawn 失败 | ✅ | 命令不存在 → task_attempt 记录 spawn_failed |
| T4 | session 清理 | P1 | 子进程结束后 session 归档 |

### F10 ActiveAgentCounter — `test_counter.py`

| # | 测试用例 | P0 | 验证点 |
|---|---------|-----|-------|
| T1 | 全局上限 | ✅ | max_global=5 时第 6 个 acquire 失败 |
| T2 | per-agent 串行 | ✅ | 同一 agent 第二个 acquire 排队 |
| T3 | release 恢复 | ✅ | release 后等待中的 acquire 成功 |
| T4 | 并发竞争 | P1 | 多 agent 同时 acquire 不超限 |

### F11 Bootstrap 拼装 — `test_bootstrap.py`

| # | 测试用例 | P0 | 验证点 |
|---|---------|-----|-------|
| T1 | 各 role 拼装 | ✅ | 不同 role 得到不同 L0-L3 四层 |
| T2 | token 估算 | ✅ | 拼装结果 < 4096 tokens |
| T3 | 缺失组件降级 | P1 | 某层文件不存在 → 跳过不崩溃 |
| T4 | 模板变量替换 | P1 | `{{task_id}}` 等占位符正确替换 |

### F12 审查流水线 — `test_review_flow.py`

| # | 测试用例 | P0 | 验证点 |
|---|---------|-----|-------|
| T1 | 4 级分流 | ✅ | high/standard/low/research 正确路由 |
| T2 | confidence 计算 | ✅ | 分值在 0-1 区间，阈值逻辑正确 |
| T3 | 升级到庞统 | ✅ | confidence < 阈值 → 升级 |
| T4 | 审查通过 → done | ✅ | verdict=approved → 状态变 done |
| T5 | 审查拒绝 → 重新执行 | P1 | verdict=rejected → 回到 working |

### F13 Guardrail — `test_guardrail.py`

| # | 测试用例 | P0 | 验证点 |
|---|---------|-----|-------|
| T1 | L1 assert 通过 | ✅ | 合规产出 → 放行 |
| T2 | L1 assert 失败 | ✅ | 不合规产出 → 拦截 |
| T3 | L2 subagent 触发 | ✅ | 高风险 → 二次确认 |
| T4 | 配置加载 | P1 | `guardrails.yaml` 正确解析 |
| T5 | 误杀恢复 | P1 | L2 确认通过 → 放行 |

### F14 反驳权 — `test_rebuttal.py`

| # | 测试用例 | P0 | 验证点 |
|---|---------|-----|-------|
| T1 | 反驳触发 | ✅ | review rejected → 反驳协商开始 |
| T2 | 轮次计数 | ✅ | round 递增正确 |
| T3 | 超轮次升级 | ✅ | 超过 max_rounds → 庞统裁决 |
| T4 | 结果写入 | ✅ | 最终裁决写入 reviews 表 |

### F15 经验沉淀 — `test_experience.py`

| # | 测试用例 | P0 | 验证点 |
|---|---------|-----|-------|
| T1 | 一级蒸馏 | ✅ | 任务完成 → experiences 表写入 |
| T2 | tag 匹配 | ✅ | 相似 tag 的经验可被查询到 |
| T3 | 二级触发 | P1 | N 条同类经验 → Skill 草稿生成 |
| T4 | 过期清理 | P1 | deprecated 经验不被查询 |

### F16 Skill 体系 — `test_skill_loader.py`

| # | 测试用例 | P0 | 验证点 |
|---|---------|-----|-------|
| T1 | 加载 + 验证 | ✅ | 四要素完整才加载 |
| T2 | per-project 覆盖 | ✅ | 项目级 Skill 覆盖全局 Skill |
| T3 | 缺要素拒绝 | P1 | 缺少 name/description/triggers/actions → 拒绝 |
| T4 | 版本管理 | P1 | 同名 Skill 版本号正确比较 |

### F17 SSE + Hook — `test_sse.py` / `test_hooks.py`

| # | 测试用例 | P0 | 验证点 |
|---|---------|-----|-------|
| T1 | SSE 连接 | ✅ | 客户端连接 → 收到初始事件 |
| T2 | SSE 4 级推送 | ✅ | task/agent/daemon/system 四级 |
| T3 | 断线重连 | P1 | 断线后重连 → 收到后续事件 |
| T4 | 降级轮询 | P1 | SSE 不可用 → 轮询兜底 |
| T5 | Hook 触发 | ✅ | 3 个触发点正确执行 |
| T6 | Hook 过滤 | P1 | 条件不满足 → 不触发 |

### F18 前端 Dashboard — `test_frontend.py`（E2E）

| # | 测试用例 | P0 | 验证点 |
|---|---------|-----|-------|
| T1 | 页面渲染 | ✅ | 5 个页面均可加载 |
| T2 | API 集成 | ✅ | 页面操作 → API 调用正确 |
| T3 | SSE 实时更新 | ✅ | 后端事件 → 前端 UI 更新 |
| T4 | 项目切换 | ✅ | 切换项目 → 数据刷新 |

---

## 四、集成测试计划（F14 完成后）

### 端到端链路测试

```
用户提需求 → 庞统规划 → 任务写入黑板 → Agent 执行 → 
Guardrail 检查 → 审查流水线 → 反驳协商 → 完成 → 经验沉淀
```

| # | 场景 | 关键验证 |
|---|------|---------|
| I1 | 正常流程（happy path） | 全链路无阻断，最终 done |
| I2 | 依赖链 | A→B→C，A done 后 B 自动 pending，B done 后 C 自动 pending |
| I3 | 审查拒绝 → 反驳 → 通过 | review rejected → rebuttal → verdict 翻转 |
| I4 | 审查拒绝 → 反驳 → 超轮次 → 庞统裁决 | max_rounds 后升级 |
| I5 | Guardrail 拦截 → L2 确认 → 放行 | L1 fail → L2 confirm → proceed |
| I6 | Agent 僵尸检测 | 无 heartbeat → observation → reclaim |
| I7 | 多项目并行 | 项目 A/B 同时运行，互不干扰 |
| I8 | 失败重试 | failed → pending（retry_count +1）→ 再次执行 |

### 集成测试基础设施

```python
# conftest.py 需要的 fixture
@pytest.fixture
def full_stack(tmp_path):
    """完整系统：FastAPI + Ticker + Dispatcher + Spawner(mock)"""
    ...

@pytest.fixture
def mock_spawner():
    """Mock spawner，模拟 Agent 执行"""
    ...
```

---

## 五、E2E 验收场景（F18 完成后）

| # | 场景 | 验收标准 |
|---|------|---------|
| E1 | 创建项目 → 提交任务 → 查看任务板 | Dashboard 实时更新 |
| E2 | 全局监控页 | 所有项目状态汇总、活跃 Agent 数、最近事件 |
| E3 | 产物仓库页 | 任务产出物可浏览、可下载 |
| E4 | 系统配置页 | 运行时配置可查看、Skill 列表可管理 |
| E5 | AI 简报页 | 项目级 AI 总结可生成 |
| E6 | SSE 实时 | 所有页面操作后 SSE 实时反映 |

---

## 六、测试通过标准

### 每个 F 的单元测试通过标准

1. **全部 P0 用例通过**（硬性要求）
2. **P1 用例通过率 ≥ 80%**（可协商）
3. **无 crash / 无未捕获异常**
4. **测试可重复运行**（跑 3 次结果一致）

### 集成测试通过标准

1. **I1-I8 全部场景通过**
2. **无死锁、无资源泄漏**
3. **并发测试无数据竞争**

### E2E 验收标准

1. **E1-E6 全部场景通过**
2. **前端无 console error**
3. **SSE 延迟 < 2s**

---

## 七、进度追踪

| F | 测试计划 | 用例设计 | 编码 | 执行 | 评审 |
|---|---------|---------|------|------|------|
| F1 | ✅ | ✅ 10 | ✅ | ✅ 10/10 | ✅ 通过 |
| F2 | ✅ | ✅ 39 | ✅ | ✅ 39/39 | ✅ 通过（附 2 条意见） |
| F3 | ✅ | ✅ 11 | ✅ | ✅ 11/11 | ✅ 通过 |
| F4 | ✅ | ✅ 14 | ✅ | ✅ 14/14 | ✅ 通过 |
| F5 | ✅ | ✅ 23 | ✅ | ✅ 23/23 | ✅ 通过（附 1 条意见） |
| F6 | ✅ 本文档 | ✅ 17 | ✅ | ✅ 通过 | ✅ |
| F7 | ✅ 本文档 | ✅ 5 | ✅ | ✅ 通过 | ✅ |
| F8 | ✅ 本文档 | ✅ 4 | ✅ | ✅ 通过 | ✅ |
| F9 | ✅ 本文档 | ✅ 16 | ✅ | ✅ 通过 | ✅ |
| F10 | ✅ 本文档 | ✅ 13 | ✅ | ✅ 通过 | ✅ |
| F11 | ✅ 本文档 | ✅ 10 | ✅ | ✅ 通过 | ✅ |
| F12 | ✅ 本文档 | ✅ 13 | ✅ | ✅ 通过 | ✅ |
| F13 | ✅ 本文档 | ⬜ 待设计 | ⬜ | — | — |
| F14 | ✅ 本文档 | ⬜ 待设计 | ⬜ | — | — |
| F15 | ✅ 本文档 | ✅ 4 | ✅ | ✅ 通过 | ✅ |
| F16 | ✅ 本文档 | ⬜ 待设计 | ⬜ | — | — |
| F17 | ✅ 本文档 | ✅ 6 | ✅ | ✅ 通过 | ✅ |
| F18 | ✅ 本文档 | ⬜ 待设计 | ⬜ | — | — |

---

## 八、v2.8 新增测试（2026-06-01 更新）

> 覆盖 #07 Spawner Acquire-First + Prompt v3.0 + _check_timeouts 统一

### F6-ext: Ticker _check_timeouts 增强（新增 4 个测试）

| # | 测试用例 | P0 | 验证点 |
|---|---------|-----|-------|
| T-ext1 | crash_limit 统一检查（working） | ✅ | executor crash 3 次/30min → _check_timeouts 标 failed |
| T-ext2 | crash_limit 统一检查（review） | ✅ | reviewer crash 3 次/30min → _check_timeouts 标 failed |
| T-ext3 | updated_at fallback | ✅ | mail auto-working 无 started_at → updated_at fallback → 超时回收 |
| T-ext4 | process_dead 对 review 状态 | ✅ | review agent 进程死 → 保持 review（不推 pending） |

### F9-ext: Spawner Acquire-First（新增 6 个测试）

| # | 测试用例 | P0 | 验证点 |
|---|---------|-----|-------|
| T-ext1 | Phase 0 Pre-acquire revive | ✅ | timeout/failed → revive → acquire 成功 |
| T-ext2 | Phase 0 假死检测 | ✅ | status=running + lock PID 死 → revive |
| T-ext3 | Phase 1 Counter acquire 互斥 | ✅ | 并发 spawn → 第二个 AgentBusyError(counter_blocked) |
| T-ext4 | Phase 2 Session check 锁保护 | ✅ | acquire 后 → session locked → release counter → AgentBusyError(session_locked) |
| T-ext5 | Phase 2 多 blockers 收集 | ✅ | locked + compact → detail.blockers 列表 |
| T-ext6 | Phase 3 on_checks_passed 异常回滚 | ✅ | 回调异常 → counter 自动 release |

### F9-ext2: Compact Hanging + Retry（新增 3 个测试）

| # | 测试用例 | P0 | 验证点 |
|---|---------|-----|-------|
| T-ext1 | compact_hanging release counter | ✅ | compact 超限 → 不标 failed → release counter → 任务保持 working |
| T-ext2 | compact_hanging ticker 重新 dispatch | P1 | compact_hanging → ticker 超时 → pending → 重新 dispatch |
| T-ext3 | retry 遇 busy 释放 counter | ✅ | _do_retry 遇 AgentBusyError → release counter → 任务保持 working |

### F9-ext3: AgentBusyError 分类（新增 3 个测试）

| # | 测试用例 | P0 | 验证点 |
|---|---------|-----|-------|
| T-ext1 | counter_blocked | ✅ | counter acquire 失败 → reason=counter_blocked |
| T-ext2 | session blockers | ✅ | session locked/running/compacting → reason + detail.blockers |
| T-ext3 | Dispatcher 错误区分 | ✅ | AgentBusyError → 路由决策记录具体原因 |

### 设计文档引用

- `docs/design/07-spawner-acquire-first.md`：#07.1 Acquire-First、#07.2 Executor/Review 统一、#07.3 Startup Recovery
- `docs/design/03-prompt-evolution.md`：Prompt v3.0 任务式指挥
- `docs/design/06-pm2-crash-recovery.md`：PM2 crash 恢复设计（启动恢复机制，待实施）

### E2E 测试文件映射

| 文件 | 覆盖范围 | 状态 |
|------|---------|------|
| `tests/test_e2e_v27.py` | E1-E10 API + 状态机 + 真实 Agent | ✅ 已有 |
| `tests/test_e2e_v31.py` | v3.1 E2E 集成 | ✅ 已有 |
| `tests/test_e2e_four_phase.py` | #01 四相循环 E2E | ✅ 已有 |
| `tests/test_four_phase_unit.py` | #01 四相循环单元测试 | ✅ 已有 |
| E11-E15 新增用例 | 待添加到 test_spawner.py / test_ticker.py / test_dispatcher.py | ⬜ 本轮更新 |
