feat(tester): add catalog-driven black-box test skill

Maintain and run project test catalogs under .pouch/tester, filtered by
category, id, and risk (safe/act/harm). Scripts generate the human CSV
from cases YAML.
This commit is contained in:
2026-09-10 16:34:38 +08:00
parent ef22c6829e
commit a47e899702
10 changed files with 1219 additions and 0 deletions
+40
View File
@@ -0,0 +1,40 @@
# suite 与 risk
维护或用户问套件/安全级别时读本文件。两套词对所有项目相同,不要自造名字。
## suite:跑哪一批
只表示耗时和依赖,不表示危不危险。危不危险看 `risk`。
业界常见还有 sanity(窄回归,和 smoke 重叠)、regression(≈本 skill 的 full)、acceptance、nightly。本 skill **不采用** sanity/acceptance:和 smoke/full 分不清,且已有 category、id、risk 三层筛选。
固定三个词:
| 值 | 何时用 |
|----|--------|
| `smoke` | 最短路径证明系统还活着。必须快、少依赖、**`risk` 只能是 `safe`**。 |
| `full` | 日常回归里其余自动化用例。说「跑 full」仍受风险上限约束。 |
| `slow` | 太慢或环境太重,不进日常 full:真实焊接、付费 API、要工件/账号、跑很久。 |
口诀:
1. 只回答「活着吗、主入口能不能用」→ `smoke`。
2. 要特殊现场/很久才能跑完 → `slow`。
3. 其余自动化 → `full`。
4. 拿不准 → `full`。不要把会动真机或会删数据的放进 `smoke`。
未指定套件时只跑 `smoke`。`跑 full` 不含 `slow`。
## risk:副作用有多大
三个短词,网站 / App / 机器人共用。`catalog.yaml` 的 `risks` 必须按此顺序,且以 `safe` 开头:
| 值 | 含义 | 网站 | App | 机器人 |
|----|------|------|-----|--------|
| `safe` | 只看,不留下变化 | 打开页、只读 API | 打开界面 | 读位姿、查状态 |
| `act` | 会动手,一般可补救 | 提交表单、改配置 | 发消息、保存 | 点动、使能、开盖扫描 |
| `harm` | 难恢复或伤人/伤钱/伤环境 | 删库、真实扣费 | 清数据、内购 | 急停、断电、真焊 |
默认只跑 `safe`。项目细分闸(点动 vs 焊接)用可选 `gate`,不要再发明 risk 名。
`suite=smoke` 且 `risk!=safe` 为非法。
+38
View File
@@ -0,0 +1,38 @@
# tester 维护用例
只在用户要加、改、退役时读本文件。写之前必须把完整条目亮给用户,确认后再落盘。
## 加一条
1. 分类必须已在 `catalog.yaml` 的 `categories` 里。没有就先改 catalog(仍要确认)。
2. 取编号:
```bash
python3 <skill-dir>/scripts/catalog.py next-id --root <root> --category <CAT>
```
3. 拟一条符合 [schema.md](schema.md) 的用例。`suite`/`risk` 按 [levels.md](levels.md):`smoke` 只能 `safe`;会动手用 `act`,难恢复用 `harm`。细分授权用 `gate`,不要发明新的 risk 名。
4. `surface=browser|mobile` 必须填 `session`,且 catalog 里已声明。
5. `what` 和 `expected` 写成可观察信号,禁止「应该正常」。
6. 给用户看拟写内容。确认后写入 `cases/<category小写>.yaml` 的 `cases` 列表末尾。
7. `validate`。失败则修好再结束。通过后跑 `generate_cases_csv.py --root <root>` 刷新 `.pouch/tester/cases.csv`。不要手改这份 CSV。
## 完善 / 更新
1. `show --id <id>`。
2. 只改用户点名的字段(常见:`how`、`expected`、`risk`、`notes`)。
3. 展示 diff 或更新后的整条。确认后写回。
4. `validate`。通过后跑 `generate_cases_csv.py` 刷新 `cases.csv`。
## 退役
把 `status` 改为 `retired`,不要删文件、不要复用 id。`notes` 写原因。确认后写回,再跑 `generate_cases_csv.py`。
## 禁止
- 未确认就写
- 把 browser/mobile 改成 script 来「省事」
- 为了能在默认风险下跑而把 `risk` 改低
- 一次塞进无关的一批用例
- 手改 `runs/` 冒充执行记录
- 手改 `cases.csv`(必须从 `cases/*.yaml` 生成)
+75
View File
@@ -0,0 +1,75 @@
# tester 运行
只在用户要跑测试时读本文件。
## 1. 选范围
未指定范围 → `--suite smoke`,风险上限 = `default_max_risk`。
用户说分类、id、suite、风险上限时,原样传给 select:
```bash
python3 <skill-dir>/scripts/catalog.py select --root <root> \
[--category CAT] [--id ID]... [--suite smoke|full|slow] \
[--max-risk <risk>] [--surface script|browser|mobile]
```
`select` 已排除 `retired` 和风险高于上限的条目(后者若用户点了具体 id,会在 JSON 的 `skipped` 里说明,不执行)。
无命中:停下说明筛选条件,不要扩大范围。
## 2. 运行前闸
- `HOST` 未设(环境变量 / `HX_TEST_HOST` / `local.yaml` 都没有):全部停,environment-fail。
- 命中条的 `risk` 在 catalog `auth_env` 里有对应变量:变量不是 `true` 则该条 `skipped`。有 `gate` 时还要 `catalog.gates` 对应变量为 `true`。即使用户点了 id 也不执行。
- 若 catalog 声明了 `status_probe`:先请求它。命中 `stop_if` → 整轮停,environment-fail。
## 3. Session
看命中条用到的 `session` 键:
- 尚未初始化:按 catalog `sessions.<name>.init` 做。`reuse: persist` 的先 `probe`,活着就附着。
- 本轮已初始化:直接用,不要重开浏览器或模拟器。
- `reuse: run`:整轮结束再关。`persist`:不关。
- 中途 session 挂了:后续同 surface 标 environment-fail,不要标产品失败。
## 4. 执行
按 select 返回的顺序,一条一条来。默认失败即停(产品 `fail` 或探测到的不安全状态)。用户说继续 / keep-going 才往后跑。
**script**
有 `how.run`:
```bash
python3 <skill-dir>/scripts/catalog.py run-script --root <root> --id <id>
```
`run-script` 只跑命令,返回 `executed` + stdout/exit(或 skipped / environment-fail)。**pass/fail 仍由 Agent 对照 `expected` 判定**,不要把 curl 退出码 0 当成产品通过。
没有 `run`:按 `how.steps` 做,证据对照 `expected`。不要发明 steps 里没有的调用。
**browser / mobile**
挂到已有 session,执行 `how.steps`,对照 `expected`。必须留下可见文本、URL 或交互证据。
## 5. 记录与报告
每条:`id, result, evidence, notes`。`result` 只能是 `pass` / `fail` / `environment-fail` / `skipped`。
写入 `.pouch/tester/runs/<UTC时间>-<范围>.yaml`,包含筛选条件、HOST 是否已设(可以写 HOST 值)、session 是否复用、逐条结果。
对话报告:
```text
## tester 运行:完成 | 部分完成 | 阻塞
范围: …
风险上限: …
session: browser=复用|新开|未用;mobile=…
计数: pass=N fail=N environment-fail=N skipped=N
失败: id + 一句证据
跳过: id + 原因
快照: .pouch/tester/runs/<file>
下一步: 一句话
```
+68
View File
@@ -0,0 +1,68 @@
# tester 目录字段
项目根 `.pouch/tester/`:
```
catalog.yaml 分类、风险阶梯、session、变量
local.yaml.example HOST 与授权开关模板
local.yaml 本机覆盖,不提交
cases/<category>.yaml 一个分类一个文件(执行源)
cases.csv 给人看的扁平清单;skill 的 generate_cases_csv.py 读 cases/*.yaml 生成
runs/ 每次运行快照
```
## catalog.yaml
| 字段 | 含义 |
|------|------|
| `categories` | `{id, title}` 列表,id 与 `cases/<id小写>.yaml` 对应 |
| `risks` | 从低到高,只能是 `safe` → `act` → `harm` 的一段,且以 `safe` 开头。含义见 [levels.md](levels.md) |
| `default_max_risk` | 未指定时的运行上限,必须是 `risks` 里的值 |
| `vars` | 可替换变量,如 `API_BASE: http://${HOST}:8095` |
| `auth_env` | 风险名 → 环境变量名。该档执行前变量必须为 `true` |
| `gates` | 可选。项目细分闸名 → 环境变量。用在用例的 `gate` 上,不要拿它代替 `risk` |
| `sessions` | 可复用会话。`kind`: `browser` / `mobile`;`reuse`: `run`(本轮结束关掉)或 `persist`(probe 活着就附着) |
| `id_pattern` | 用例 id 正则 |
`local.yaml` 与环境变量覆盖 `HOST` 和授权开关。查找顺序:环境变量 > `local.yaml` > `catalog.yaml` 的 vars。也认 `HX_TEST_HOST` 作为 `HOST`。
## 一条用例
写在对应分类文件的 `cases:` 列表里。
| 字段 | 必填 | 含义 |
|------|------|------|
| `id` | 是 | 匹配 `id_pattern` |
| `category` | 是 | 必须等于文件声明的 category |
| `title` | 是 | 短名 |
| `what` | 是 | 一句可观察的「测什么」 |
| `status` | 是 | `active` / `retired` |
| `suite` | 是 | `smoke` / `full` / `slow`,判定见 [levels.md](levels.md) |
| `risk` | 是 | `safe` / `act` / `harm` |
| `gate` | 否 | 项目细分闸,必须在 catalog `gates` 里 |
| `surface` | 是 | `script` / `browser` / `mobile` |
| `session` | browser/mobile 必填 | 指向 catalog `sessions` 的键 |
| `how` | 是 | 怎么测,见下 |
| `expected` | 是 | 怎么过,至少一条 `{kind, value}` |
| `maps_to` | 否 | 对人文档的功能项 id |
| `requires` | 否 | 前置标签 |
| `skip_unless` | 否 | 额外跳过条件(环境变量表达式) |
| `setup` | 否 | 人话前置 |
| `cleanup` | 否 | 跑完回退 |
| `notes` | 否 | 备注 |
## how
- `script`:`how.run` 是一条可在项目根执行的命令(可多行脚本)。没有 `run` 时 Agent 按 `how.steps` 做,仍不得发明未写明的请求。
- `browser`:`how.url` + `how.steps`。`expected.kind` 用 `visible-text` / `url` / `interaction`。
- `mobile`:`how.steps`,挂到 `session` 指定的模拟器。`expected` 同样是可观察信号。
可选 `how.timeout`,秒,默认 60。
## expected.kind
`api-status` / `api-field` / `script-exit` / `visible-text` / `url` / `interaction`
## 判定
`pass` / `fail` / `environment-fail` / `skipped`