第 12 章 | 编写 spec——Requirement 与 Scenario
💡 进群学习加 wx: agentupdate
(申请发送: agentupdate)
(申请发送: agentupdate)
第 12 章:编写 spec——Requirement 与 Scenario
学习目标
写出可测试的契约。每个 Scenario 都该能成为一个 pytest 函数。
Spec 的层次
flowchart TB
Cap["Capability
(specs/<name>/spec.md)"] --> R1["Requirement 1
(SHALL 句式)"]
Cap --> R2["Requirement 2"]
Cap --> Rn["Requirement N"]
R1 --> S1a["Scenario 1.1
(WHEN/THEN)"]
R1 --> S1b["Scenario 1.2"]
R2 --> S2a["Scenario 2.1"]
R2 --> S2b["Scenario 2.2"]
R2 --> S2c["Scenario 2.3"]
style Cap fill:#fce4ec
style R1 fill:#fff9c4
style R2 fill:#fff9c4
style Rn fill:#fff9c4→ Capability 包含多个 Requirement,Requirement 包含至少一个 Scenario。
Requirement 的写法
### Requirement: 命令完成检测
工具 SHALL 通过 marker 输出识别命令结束时机与 exit code,不依赖 prompt 字符串匹配。
关键词:
| 强度 | 关键词 | 何时用 |
|---|---|---|
| 强制 | SHALL / MUST | 系统必须做到 |
| 禁止 | SHALL NOT / MUST NOT | 系统不允许做 |
| 推荐 | SHOULD(少用) | 强建议但允许例外 |
| 可选 | MAY(少用) | 可做可不做 |
→ MVP 阶段所有 Requirement 都用 SHALL/MUST。SHOULD 和 MAY 是"以后再扯皮"的口子,少用。
Scenario 的写法
#### Scenario: 命令完成检测
- **WHEN** 工具向 tmux 发送一条命令
- **THEN** 工具通过 marker 输出识别命令结束时机与 exit code,不依赖 prompt 字符串匹配
WHEN/THEN 句式 = 可被翻译成 pytest 用例:
def test_terminal__command_completion_detection():
"""Scenario: 命令完成检测"""
# WHEN 工具向 tmux 发送一条命令
terminal.send("echo hi")
# THEN ... 通过 marker 识别 ...
assert terminal.last_exit_code == 0
完整映射链:
flowchart LR
Spec["spec.md
### Requirement"] --> S1["#### Scenario A
WHEN ... THEN ..."]
Spec --> S2["#### Scenario B"]
Spec --> S3["#### Scenario C"]
S1 -.tester 翻译.-> T1["test_xxx__a()"]
S2 -.tester 翻译.-> T2["test_xxx__b()"]
S3 -.tester 翻译.-> T3["test_xxx__c()"]
T1 --> Pytest["pytest 执行"]
T2 --> Pytest
T3 --> Pytest
Pytest --> Report["test-reports/N.md
**Status:** PASS|FAIL"]
style Spec fill:#fce4ec
style Pytest fill:#c8e6c9
style Report fill:#fff9c4→ 一个 Scenario 一个 test 函数——名字最好包含 Requirement 标识 + Scenario 名。
⚠️ 格式陷阱:4 个 # 不能少
OpenSpec 的 schema 严格要求:
✅ #### Scenario: ... 4 个 #
❌ ### Scenario: ... 3 个 # 会被静默忽略!
❌ - **Scenario:** ... bullet 不行
这是 OpenSpec 教训第一名的坑。写完跑 openspec status --change <name> 看 spec 是否被认到。
Delta 操作(4 种)
change 里的 spec 文件不是完整 spec,是变更:
## ADDED Requirements
(新增)
### Requirement: <new name>
...
## MODIFIED Requirements
(修改——必须复制完整原文 + 改)
### Requirement: <existing name>
...全部内容(含原 Scenario)...
## REMOVED Requirements
### Requirement: <name>
**Reason**: ...
**Migration**: ...
## RENAMED Requirements
- FROM: 旧名
- TO: 新名
MODIFIED 的关键陷阱
❌ 只写 diff:"Change X to Y"
✅ 复制全部原内容 + 改 = 完整新版本
OpenSpec archive 时是"拿 MODIFIED 段替换原 Requirement",所以省略原内容会让信息丢失。
实例:doc2video 的部分 spec
## ADDED Requirements
### Requirement: Markdown 协议解析
工具 SHALL 把 markdown 文档解析为有序的 step 列表,规则:每个二级标题划分一个 step;
连续段落合并为讲解文本;```bash 代码块依序作为终端命令;:::manual 容器标记此 step 为
纯讲解步骤。
#### Scenario: 终端 step 解析
- **WHEN** 文档包含 `## 第一步:安装 Node` 后跟讲解段落和一个 ```bash 代码块
- **THEN** 工具产生类型为 `terminal` 的 step,narration 为段落文本,commands 为代码块
内的命令列表
#### Scenario: Manual step 解析
- **WHEN** 文档某个二级标题下出现 `:::manual` 容器
- **THEN** 工具产生类型为 `manual` 的 step,narration 为容器内文本,无 commands
#### Scenario: 同一 step 多代码块
- **WHEN** 同一二级标题下出现多个 ```bash 代码块
- **THEN** 工具按文档顺序依次执行所有代码块中的命令,全部成功才认为该 step 通过
→ 一条 Requirement,三个 Scenario,每个 Scenario 都是一个测试用例。
Scenario 数量参考
| Requirement 复杂度 | Scenario 数 |
|---|---|
| 简单("系统 SHALL 返回 200") | 1~2 |
| 中等 | 2~4 |
| 复杂(多分支、多状态) | 4~7 |
→ 超过 7 个 Scenario,考虑拆 Requirement。
反模式
❌ Scenario 描述"内部状态" → THEN 必须是可观测行为
❌ 一个 Scenario 测多个行为 → 拆开
❌ Scenario 含 implementation → "调用 X 函数" 不行,要说"用户看到 Y"
❌ Scenario 没有 WHEN → 缺少触发条件
❌ MODIFIED 不带原内容 → archive 时丢信息
你现在能做什么
- 区分 Capability / Requirement / Scenario
- 用 4 个 # 写出格式正确的 Scenario
- 把每个 Scenario 心里映射到一个测试函数
下一章把规约翻译成可执行的"任务清单"。