---
name: eo-test
description: |
  为某个 change 编写并执行测试（以其验收清单为锚），产出 test.md 报告。严禁修改业务代码。触发：给这个 change 写测试 / 出测试报告 / test.md / /eo-test。
  NOT FOR: 与 change 无关的日常跑测试或补单测（直接做即可，不产报告）。
---

# eo-test — 测试

以 change 的验收清单为锚编写并执行测试，产出结构化测试报告。核心目标是**验证**而非**修复**。

## 职能边界

1. **绝对禁令**：**禁止修改任何非测试相关的业务源码**。发现 bug 只记录并建议修复，不自行改源码。
2. **操作范围**：仅允许修改、新增测试文件与测试目录内容。
3. **输出目标**：测试代码 + `test.md` 报告 + 对话速报。

## 核心原则

1. **以 AC 为锚**：change.md §2 验收清单逐条推导——每条 AC 至少一份**验证证据**（不必是本 skill 新写的测试：implement 已留下且审计通过的测试、一次性执行记录都算数）；§3 各 TODO 的完成判据（如有）作为补充覆盖点。AC 规范见 [../eo-shared/ac-spec.md](../eo-shared/ac-spec.md)
2. **审计 + 补缺，不重写**：implement 落下的测试是本 skill 的**输入**而非空白——先盘点已有覆盖并审计其真实性，只为缺口编写。独立性来自**独立审计与独立执行**，不来自重新编写
3. **AC 决定验什么，代码决定拿什么验**：AC 是**断言**的来源，不是**输入**的唯一来源。只按 AC 文字正向推导，验的永远只是「AC 想到的路径」——AC 写的是用户视角的期望行为，不会写出 `""` / `null` / 越界值 / 并发时序这类输入，它们只存在于实现的分支、默认值与空值处理里。**再多跑几遍环境矩阵也发现不了 AC 没想到的输入**
4. **回归资产分层**：永久测试文件只给逻辑密集、边界多、会被后续变更反复触碰的验证点；其余验证点以一次性执行证据（命令 + 关键输出记入 test.md）作证——测试深度随风险浮动，与 AC 的「条数不模板化」（ac-spec）同一原则
5. **重验证的唯一执行者**：auto-heavy AC（起服务 / 多环境组合 / 点击流）由本 skill 跑并勾选，implement 不跑——环境矩阵在这里一次跑完，不在三个阶段各重演一遍
6. **验证驱动**：所有职能围绕「证明代码是否符合 AC」展开
7. **独立报告**：固定产出到 `eo-doc/changes/<change-id>/test.md`
8. **复验按证据失效范围付费**：首轮 Test 仍完整审计；Review 修复后的复验不机械重跑全部。不含 auto-heavy 且影响能映射到有限 AC、用例及依赖闭包时定向复验，未受影响的通过证据从旧基线沿用；任一 auto-heavy AC 被弄脏、影响跨共享面或范围无法圈定时完整复验
9. **测试资产进入交付基线**：测试文件、fixture、mock、harness 与测试配置都是受审交付物；本 skill 改动它们时，必须先提交再执行最终验证，不能让未提交测试改动绕过 Review 新鲜度门

## 前置条件

- **必须能找到 `.eo-project.json`**。同目录存在 `.eo-project.local.json` 时顶层字段覆盖合并（local 优先）。找不到 → 报错退出，提示运行 `/eo-project-init`
- `eo-doc/changes/<change-id>/change.md` 已存在，且相关代码已实现（status: implementing 或 reviewed）
- `tier: light` 的 change 默认不经本 skill（重验证出现即扩档，见 eo-implement 轻模式）。用户显式要求时以 §2 AC 为锚执行，但为**只读语义**：测试代码照常落库，结论走对话速报，**不写 test.md 进 change 目录、不动 AC 勾选、不改 status**；发现缺陷 → 已 archived 的走 /eo-fix

## 工作流程

### 阶段一：单元测试（审计 + 补缺）

1. **消费 lessons**：按 [../eo-shared/lessons.md](../eo-shared/lessons.md) §1 扫 INDEX 匹配 trigger，命中读「规则」节带入——**尤其是环境相关的项目 lesson**（起停命令、单次代价、环境由谁起属项目特异知识，只可能在项目 lesson 里，不在本 skill）
2. **确定证据范围并盘点覆盖**：
   - 首轮（无 test.md）→ 读 frontmatter `base_commit..H` 或全部 `[<change-id>]` 提交，**含 implement 落下的测试文件**，建立「AC → 已有测试」映射
   - Review 修复后由最新 review 轮处置为 `复验` → 记录精确触发来源 `Review 第 R 轮 @ H`，读取其中的旧 Test 轮次与基线 `第 N 轮 @ S`、当前交付基线 `H` 与受影响 AC / 测试，验证来源轮及其定向来源链均属于当前 `plan_revision`、结构完整、结论通过，且 `S` 是 `H` 的祖先，再审计完整 `S..H`；字段缺失、含糊、revision 过期、来源链不通过、基线不成立或影响圈不住 → 直接升级完整复验
   - Test FAIL 修复 → 从对应 FAIL 的修复 commit 与其影响面起算，至少覆盖待核销 FAIL、受影响 AC / 用例及依赖闭包；Review 的证据沿用不得缩掉未核销 FAIL

   对范围内已有测试逐条审计真实性——断言是否真实覆盖对应 AC、有无被弱化/删除、有无过拟合/硬编码特判。审计通过的**不重写**；测试代码问题由本 skill 直接修正
3. **提取缺口验证点**：首轮/完整复验读 change.md §2 全部 AC 与 §3 TODO 完成判据；定向复验只读 review 指出的受影响 AC、用例及依赖闭包。对照步骤 2 的映射列出所选范围内的**无覆盖**验证点，逐条标注 **light / heavy**（heavy = 起服务 / 多环境组合 / 点击流）与适合单测 / 需集成验证；不得借“补缺”把定向复验静默扩成全量，发现范围外风险应显式升级完整复验
4. **读实现取输入**：从步骤 2 已读的 diff 中，从**分支、默认值、空值与边界处理、类型转换**提取 AC 没写、已有测试也没碰、但代码实际会遇到的输入，补进缺口清单——AC 给断言，代码给输入（核心原则 3）
5. **补缺编写**：只为缺口写测试，遵循项目既有测试目录与命名约定；按**回归资产分层**（核心原则 4）取证据形态——逻辑密集 / 边界多 / 共享路径的落成测试文件，其余以一次性执行证据作证（命令 + 关键输出记入 test.md「一次性执行证据」节，不落测试文件）
6. **提交测试资产并锁定交付基线**：本轮新增或修改了测试文件、fixture、mock、harness 或测试配置时，先把这些测试资产提交为带 `[<change-id>]` 前缀的 commit，再把最后一个触及业务代码或测试资产的本 change commit 记为当前交付基线 `H`；`test.md`、change 元数据等纯流程工件提交不计入 `H`。最终执行开始时不得残留本 change 的未提交交付改动：测试资产由本 skill 提交；发现未提交业务代码则停下退回 eo-implement 结算，Test 不得代提交。执行中若修正测试资产，须再次提交、刷新 `H`，并在新 `H` 上重新执行受影响范围。报告的 `测试资产提交` 列出本轮执行区间 `A..B` 内所有触及测试资产的本 change commit（不论作者）；`A` = 首轮的 `base_commit` / Review 触发基线 / Test FAIL 触发基线，`B` = 最终执行基线
7. **选择并执行最终范围**：
   - **首轮完整**：执行全部已映射测试、补缺测试与必要回归
   - **定向复验**：受影响集合不含 auto-heavy，且可准确映射到有限 AC、用例及依赖闭包时，只重跑该闭包 + 必要边界冒烟。把来源轮的已通过证据全集记为 `E`、触发影响集记为 `I`（Review 触发 = 该 Review 轮的受影响 AC / 测试；Test FAIL 触发 = `触发来源` 指向轮结束时的 open/fixed FAIL + 当时受影响 AC / 用例 / 依赖闭包）、重跑范围记为 `R`、沿用范围记为 `U`；必须证明 `I ⊆ R`，且来源证据被 `R` 与 `U` 无遗漏、无重叠地分区（本轮新增证据另列入 `R`）。无法机械证明就升级完整复验
   - **完整复验**：任一 auto-heavy AC 被弄脏，或影响跨共享路径 / 契约 / 状态机 / schema / 并发 / 权限安全 / 外部集成 / 环境矩阵 / 测试基础设施，或无法可靠圈定时，执行全量；tester 可从定向升级完整，不得无证据缩小 reviewer 指出的影响集
8. **失败分析**：测试代码自身写错 → 直接修正测试资产并回到步骤 6 提交、刷新 `H` 后重跑；业务代码 bug → **停手**，记录到报告并建议走 /eo-implement 修复循环

### 阶段二：集成 / 场景验证（重验证，一次跑完）

首轮/完整复验覆盖全部 **auto-heavy AC** 与其余不适合单测的 AC；定向复验只覆盖受影响集合及必要依赖闭包。均按验证口径（「验证」栏，省略时按声明本身）描述操作并捕获命令输出、退出码、运行结果作为执行证据；未受影响 heavy 证据只有在来源轮通过、`S` 为 `H` 的祖先且沿用范围明确时才能继承。

**环境纪律**（详见 [../eo-shared/ac-spec.md](../eo-shared/ac-spec.md)「重验证的环境纪律」）：环境**假定已就绪**——先探测复用、**不主动停**，只在需要换环境组合时重启；**按环境组合分组跑**，一次起环境跑完该组合下的全部验证点，不为单条 AC 反复起停。

验证通过 → **勾选对应 auto-heavy AC**（本 skill 独有的勾选权）；light 项 implement 已勾不重跑，manual 项不碰。

### 阶段三：报告与速报（轮次留痕，追加不覆盖）

1. 按 [references/test-template.md](references/test-template.md) 维护 `eo-doc/changes/<change-id>/test.md`：
   - **首轮**（文件不存在）：全量写入并建立 `第 1 轮记录`，失败项同步建入 FAIL 台账（FAIL-x 编号跨轮稳定；根因区分 `implementation` / `test-harness`——测试代码错误由本 skill 修正后直接核销，不留给修复循环）
   - **复审轮**：**不重写报告**——先核销台账（`fixed` 项复测：过 → `verified`，仍失败 → 回 `open` 一句话说明；`verified` 后再失败 = reopen，刷新最近轮）；新失败建条（编号沿用同一序列）；在「速报」前追加 `## 第 N 轮记录（revision R · 日期）` 节，原地更新末尾速报。遇**无台账的旧格式报告** → 按当前内容一次性补建台账再继续
   - 台账写入权见模板 writer matrix：本 skill 建条与核销；`fixed` + 修复 commit 归 eo-implement 回写；用户当场裁决不修的项 → 状态置 `waived`（附原话要点，不阻塞归档）；历史轮次节不改；轮次编号全文件单调递增，跨 revision 不清零
   - 每轮记录与末尾速报固定写 `验证方式：首轮完整 / 定向复验 / 完整复验`、`触发来源：首轮 / Review 第 R 轮 @ H / Test 第 R 轮 FAIL[FAIL-x,...] @ F`、`来源 Test：无 / 第 N 轮 @ S`、`当前交付基线：B`、`测试资产提交`、`重跑范围`、`沿用范围`、`范围校验`。这里 `B` 是本轮最终执行锁定的 `H`；定向复验的通过结论 = 本轮重跑证据 + 从通过的来源轮明列继承的未受影响证据，组合后统一锚定 `B`。不得只写“部分通过”冒充整体验证通过，也不得省略触发轮次或来源轮次只写裸 commit
2. **status 回退与恢复边界**：结论不通过且当前 status 为 `reviewed`（review 先过、test 后翻车）→ **当场置回 `implementing`**（回退边，见 [../eo-shared/conventions.md](../eo-shared/conventions.md) §3）。后续 Test 通过不自行改回 `reviewed`（该状态写入权仍归 eo-review）；即使 `H` 未变化，只要 status 仍为 `implementing`，也须回原 reviewer 在同一基线上确认并恢复状态
3. **对话速报（硬性——缺速报 = 流程未完成）**，报告写盘后在对话最后输出：

```
结论：通过 / 不通过（失败 x 项）［第 N 轮 · revision R · 基线 <short-sha>］
⚠️ 复发：<ID> 第二次失败（无则省略此行）
验证方式：首轮完整 / 定向复验 / 完整复验
触发来源：首轮 / Review 第 R 轮 @ <H> / Test 第 R 轮 FAIL[<FAIL IDs>] @ <F>
来源 Test：无 / 第 N 轮 @ <S>；当前交付基线：<B>
测试资产提交：无 / <本轮测试资产 commit 清单>
重跑范围：<AC / 用例 / 依赖闭包>；沿用范围：无 / <从来源轮继承的 AC / 用例>
范围校验：<触发影响集 I ⊆ 重跑 R；来源证据 E = (R ∩ E) ⊎ 沿用 U>
失败用例：
1. <一句话> — <用例/文件>
未覆盖 AC：<AC-x（原因）>；全覆盖则省略此行
下一步：<失败 → 回 /eo-implement 后必须由原 tester 复验 / 通过但 status 非 reviewed 或最新 Review 未覆盖 B（含本轮提交测试资产）→ 原 reviewer 增量审查 / 通过且 status=reviewed、Review 已覆盖 B → /eo-archive>
（详细报告见 <test.md 路径>）
```

每条一句话加定位，不在速报里展开分析；全绿时省略失败明细，但 `验证方式`、触发来源、来源轮、当前交付基线、测试资产提交、`重跑范围`、`沿用范围`、`范围校验` 与下一步仍必须保留，不能压缩成只有结论的两行。

## 关键约束

- **禁止跨权**：严禁修改业务逻辑代码
- **不重写已覆盖项**：已有测试审计通过的 AC 不重复编写；独立性靠审计 + 独立执行保证，不靠重写
- **测试独立性**：用例独立运行，不依赖执行顺序
- **不修改 change 的内容**：AC/TODO 的文字不动——发现写漏/写偏记录到报告并建议回 /eo-implement（其流程含「确认后就地补 AC」；循环内问题不出循环）。**勾选 auto-heavy AC 是本 skill 的职权**（勾是验证结果的落点，不是改 change）；light 项已由 implement 勾、manual 项归用户，两者都不碰
- **环境不归你所有**：探测复用、用完不停；按环境组合分组跑，不为单条 AC 反复起停
- **输入不只来自 AC**：跳过「读实现取输入」= 只验 AC 想到的路径，这正是重验证跑再多遍也漏缺陷的原因
- **证据完整**：关键场景必须有可验证的执行证据
- **测试资产先提交后取证**：本轮改过测试资产就先提交并刷新交付基线，再在该精确基线上执行最终验证；报告提交不算交付基线。测试资产未提交、提交后未重跑或最新 Review 未覆盖该提交，都不能进入归档
- **复验范围可审计**：定向复验必须指向一个明确通过的来源轮 `第 N 轮 @ S`，同时列重跑范围与沿用范围，并证明 `S` 可沿用到当前交付基线；影响不清升级完整复验。Test FAIL 未核销时不得用 Review 的沿用结论绕过复测
