系列第二篇。上一篇讲了流水线的整体编排:5 个子 Agent + 看板驱动。这篇深入第一个真正"干活"的角色------Analyst,看它怎么把一份需求文档,变成一份结构化、可追溯、可确认的测试用例套件。
这篇解决什么问题
大多数 AI 生成用例的方案是:丢给 LLM 一句「帮我写 20 条测试用例」,然后它吐一堆 markdown 给你。问题来了:
- 无法追溯:这条用例覆盖了需求里的哪一条?说不清。
- 不可确认:生成一大堆,用户没法快速扫一遍确认"覆盖对不对"。
- 尺度失控:说好 core,结果写出来一堆边界;说好全量,结果 happy path 都没写全。
- 无法复用:这次生成的用例,下次换工具/换流程就废了。
Analyst 的目标,就是把「需求文档」加工成一份 带分层、带追溯、带覆盖索引、带确认闸门 的用例套件。
核心分层:L0 / L1 / L2(这是全文重点)
整套用例设计基于一个「分层」思想,产物真相只有一个,其他都是派生。
| 层 | 路径 | 角色 | 谁产生 | 能否手改 |
|---|---|---|---|---|
| L0 | cases/requirements.json |
需求原子,溯源用 | Analyst 解析 | --- |
| L1 | cases/suite.json |
用例真相,用户确认闸门 | Analyst 设计 | 确认前可改 |
| L1 旁路 | cases/coverage.json |
L0×L1 覆盖索引 | 脚本算出来 | ❌ 禁止手改 |
| L1 视图 | cases/suite.md |
只读渲染给人扫一眼 | 脚本渲染 | ❌ |
| L2 | specs/*.md、cases/cases-ai.json |
通道派生,确认后生成 | 脚本/主流程 | 可丢重生 |
一句话总结铁律:用例真相在 L1(suite.json);L2 的一切都是确认后的派生产物,丢了可以重新生成。

为什么要分这么细?因为不同产物生命周期完全不同:
- L0 是溯源------需求变了,能算出哪些用例受污染。
- L1 是对齐------人是靠这份确认用例方向的。
- L2 是生产------转成 Playwright spec、扁平 JSON,跑完就完,坏了重生。
Analyst 怎么干活:四步流水
Analyst 是一个标准的子 Agent,它的执行流程是固定的(见 agents/analyst.md):
markdown
1. 解析 task 参数(outputDir / reqSource / scope / goal)
2. 加载 skill(test-cases) // 用例生成规范
3. parse:node helpers/parse-md.js <req.md> → cases/requirements.json // 需求→L0原子
4. 基于 L0 + scope + goal 设计用例,写入 cases/suite.json // →L1
5. coverage:node helpers/coverage.js req.json suite.json → coverage.json
6. 渲染只读视图:node helpers/render-suite-md.js suite.json → suite.md
7. 返回 JSON(含统计与覆盖摘要)
它有明确的红线规则,我最喜欢两条:
- A1. 不编造未写明的 UI:需求没给的按钮文案/路由/接口,用语义占位符(如「登录按钮」),并标注「待页面校验」。禁止假装探索过页面。→ 这防止了"编需求"。
- A3. 场景原子化:一条 case 只验证一条可判定路径,正向/反向/边界拆开。→ 这防止了"大杂烩用例"。
还有一个硬约束:A6. 无论成功失败,最后输出必须是 JSON,不能有别的文本。跟前一篇说的"强制 JSON 返回"一脉相承。
2. 一条用例长什么样
suite.json 里每条用例是高度结构化的,而非一句自由文本:
json
{
"id": "S-003",
"title": "登录成功-正确凭证",
"priority": "P0",
"phase": "readonly",
"design": {
"phase": "readonly",
"automation": "ui"
},
"trace": {
"reqIds": ["R-001", "R-002"],
"origin": "literal" // literal=直译 / derived=边界反推 / exploratory=页面补洞
},
"steps": [
{ "action": "goto", "target": "/login", "value": "" },
{ "action": "fill", "target": "邮箱", "value": "test@example.com" },
{ "action": "fill", "target": "密码", "value": "Test@123456" },
{ "action": "click", "target": "登录", "value": "" }
],
"expects": [
{ "type": "url", "value": "包含 /dashboard" },
{ "type": "element", "value": "设备总览标题可见" }
]
}
几个关键字段:
trace.reqIds:这条用例追溯到哪些需求原子(L0)。必须非空------用例不能是"无源之水"。trace.origin:标注用例来源------直译需求 / 边界反推 / 页面补洞。审查时一眼看出哪些是需求的直接映射,哪些是 AI 发挥。expects[].type:url | text | element | api | data | custom。禁止空 expects,禁止「看起来正常」这种无法断言的预期。
3. 覆盖索引:L0×L1 交叉算覆盖率
Analyst 不算手动脉搏率,它调脚本算。coverage.js 做 L0×L1 的笛卡尔覆盖计算,产出 coverage.json。返回的 summary 会给主线程:
json
{
"summary": {
"reqTotal": 12, // 需求原子总数
"casesTotal": 18, // 用例总数
"p0": 5, // P0 用例数
"coveredReqs": 10, // 被用例覆盖的需求数
"uncoveredReqs": 2, // 漏掉的需求数 ← 审查重点
"orphanCases": 0 // 无需求来源的孤儿用例
}
}
uncoveredReqs: 2 这种数字极其重要------它告诉用户"有两条需求没被覆盖"。orphanCases: 0 防止 AI 写一堆不溯源的空用例。这两个数字是覆盖是否完整的第一道体检指标。
4. 确认闸门(这篇最核心)
Analyst 产出的 suite.json 默认是 meta.status: "draft"。在用户确认之前,禁止派生任何 L2 产物 (红线:不能在确认前调用 to-specs / project-flat)。
主线程会向用户展示覆盖摘要,等用户说「确认」。确认后才执行:
bash
# 1. 标记确认
node helpers/confirm-suite.js cases/suite.json
# 2. 派生平坦用例(给用例源用)
node helpers/project-flat.js cases/suite.json cases/cases-ai.json
# 3. 派生 Playwright 计划
node helpers/to-specs.js cases/suite.json specs/
这套设计有个非常专业的细节:陈旧检测 。如果需求文件的 SHA256 变了(meta.reqSourceHash 不一致),套件会被标为 stale,禁止静默覆盖已确认的用例------要先 diff 再重新生成。防止"需求改了,旧用例还在用"的隐性污染。
范围裁剪:scope 决定尺度
Analyst 不是每次都写满用例,而是按 scope 裁剪尺度:
| scope | 覆盖策略 |
|---|---|
smoke |
每模块最多 1--2 条 happy path |
core |
关键旅程 + 主要异常 |
full |
core + 边界 / 权限 / 并发等 |
这是贴近真实测试管理的做法------冒烟、核心回归、全量回归本就是三种不同尺度的测试活动,不允许用一套尺度糊弄。
抄走什么
这篇的可复用设计:
- 分层产物 + 唯一真相源:L0 溯源 / L1 确认闸门 / L2 派生,别把所有东西压在一个文件里。
- 用例必须可追溯 :
trace.reqIds必须非空,审查能算出uncoveredReqs和orphanCases。 - 不可确认就得卡住:draft 状态在用户确认前禁止派生下游产物;需求变了要标 stale 再 diff,不静默覆盖。
下一篇:用例生成后,测试跑挂了。怎么判断是定位器失效、真 Bug、还是环境问题?Healer 的「归因三分类」和自愈机制,我们下篇见。