# agent-web **Repository Path**: liu-everything_impossble/agent-web ## Basic Information - **Project Name**: agent-web - **Description**: 一句话:给 Agent 写机器可判的集成测试——同一份案例,跑两个实现,判决必须一致。 - **Primary Language**: Unknown - **License**: MIT - **Default Branch**: main - **Homepage**: https://liuzheng6621.github.io/agent-web/ - **GVP Project**: No ## Statistics - **Stars**: 1 - **Forks**: 0 - **Created**: 2026-11-03 - **Last Updated**: 2026-11-10 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # agent-web v0.78.3
一份案例,跑两个实现,判决必须一致。不一致的地方,要么实现有洞,要么断言在空转。
我们给被测实现注入了 72 种谎话——谎报成功、伪造账单、冒充别人、偷偷挪走别人的订单。 适用的 46 种里,断言当场抓住 44 种;漏掉的 2 种写在台账里,不在脚注里。 (怎么验证的 · 哪些抓不到 · 打开能点的判决页 ↗)
当前版本 0.78.3 · 21 种断言 · 33 条命令 · 72 个变异体
这一行是算出来的:版本取自 package.json,断言种类取自 KINDS,
命令数与变异体数取自代码本身。scripts/status-line.mjs 对不上就红,CI 在跑。
在线演示 ↗ · 三十秒判断 · 六十秒上手 · 三种判决 · 六个域 · 命令 · 边界 · 文档地图
## 三十秒判断该不该用 **如果你的 Agent 测试长这样,这个工具帮不上忙**:只有一份实现、断言靠模型看输出、失败时靠人读日志。 **如果长这样,它就是给你写的**: | 你的现状 | 这里对应什么 | |---|---| | 有 mock 实现,也有真实后端,但没人证明两者行为一样 | `compare`:同一份案例跑两遍,判决必须逐条一致 | | 测试挂了,你分不清是代码坏了还是环境没起来 | 三值判决:`unknown` 是一个独立结论,不是失败的别名 | | 断言写了很多,但不知道哪条真的在起作用 | `certify` + 变异体:故意写坏实现,看哪条断言抓得住 | | 想让 Agent 自己改代码,但不敢让它自己判断改对没有 | 断言机器可判,禁 LLM judge;`selftest` 13 条标签由机制产出 | | 手上有接口但没测试,不知道从哪开始 | `discover`:输入网址,生成能跑的适配器和案例草稿 | 一句话:**你在为「Agent 改的代码到底对不对」找一个不依赖信任的答案。** 不适合的场景也说清:它不做性能测试、不做 UI 测试、不做安全扫描。它只回答一个问题——**某次业务操作的行为,是否符合一份写死的、机器能判的预期。** ## 六十秒上手
第一条命令不需要任何准备,也不用装依赖。从仓库跑:
```bash
git clone https://gitee.com/liu-everything_impossble/agent-web.git
cd agent-web
node bin/agent-web.mjs selftest
```
> **npm 包尚未发布。** 包名 `agent-case` 已定好(仓库与命令都叫 agent-web,同一份东西),
> `package.json`、`bin` 双命令名、`files` 白名单都就位,`npm pack` 装到干净目录里跑过 `--version` 与 `selftest`。
> 卡在发布凭据(本机 `npm whoami` 仍是 need auth,registry 上 `agent-case` 现在返回 404)。
> 所以下面这行**现在跑会 404**,发布完成后它才成立——届时本段与 `docs/LIMITS.md` 的对应条目一并删掉:
>
> ```bash
> npx agent-case selftest # 发布前会 404
> ```
`selftest` 用内置内存实现跑内置案例:pass、fail、unknown 三种判决各出现一次。
fail 的那条会直接告诉你「订单 1001 当前 MAKING,不允许直接变成 PICKED」——不是「测试失败」四个字。
手上已经有一个能跑的系统?看你想省哪一步:
```bash
./quickstart.sh http://127.0.0.1:8080 # Windows: .\quickstart.ps1 那个地址
# 只读接口 → 直接生成能跑的适配器 + 案例草稿 + NEXT.md 施工单
# 有流量可录 → 用 tap + learn,适配器、孪生、案例一起掉出来(见下两节)
```
`quickstart.sh` 就是 `discover` 加三条「接下来做什么」的提示;它不猜业务规则,
猜不出来的会写在 `NEXT.md` 里,而不是悄悄编一条。
想看它动起来:
52 秒介绍片(判决三种、输入网址、录流量、断言准入、出题批改):[docs/media/promo.mp4](docs/media/promo.mp4)
## 它解决的问题
Agent 改代码改得很快,但「改对了没有」经常只能靠人肉点一遍。单元测试管不到跨请求的状态流转,端到端测试又慢、还不稳。
agent-web 的做法:把一次真实业务操作写成**案例**(做什么 + 结果该是什么),然后拿它同时打两个实现——一个内置的内存实现,一个你真实的系统。两边判决必须一致。
一致,说明案例可移植、断言没写死实现细节。不一致,就有一处要修,而且你能立刻知道是哪一处。
## 判决只有三种
- **pass**:所有断言成立。
- **fail**:有断言不成立,并指出哪一条、期望什么、实际什么。
- **unknown**:缺观测、环境故障、案例还没人确认。**留在分母里,不算通过。**
可判定率 = (pass + fail) / N。unknown 不回收,所以这个数字只会因为观测被修好而上升,不会因为掩盖而上升。
## 输入一个网址
没有适配器也能起步。`discover <网址>` 探根路径、试 8 个常见 OpenAPI 位置,然后:
- 只读接口 → 一个能跑的只读适配器(写能力一律 `unsupported`)
- 每个资源 → 一条案例草稿,断言只有 `unchanged`:读不该改变任何东西
- create 接口(`--writes`)→ 往返案例:建一条,断言 `count_delta +1`
- 一份 `NEXT.md` 施工单:要接写操作,改哪四处、每步怎么验收
它不猜三件事:写操作该保证什么、带参数接口的路径、没有 OpenAPI 时的路由。
猜出来的 404 会变成假失败,而假失败会让工具失去信任。
## 录一段流量,长出一个适配器
写适配器是这套东西最贵的一步。这一节讲怎么把它压到十分钟。
```
agent-web tap --target=http://127.0.0.1:8199 --port=8197 --out=.tmp-tap/rec
# 另开一个终端,正常点几下:登录、建三条任务、完成一条、看列表
agent-web learn .tmp-tap/rec/traffic.jsonl --out=.tmp-tap/learned --name=tasks
```
四样东西一起掉出来:
| 产物 | 是什么 |
|---|---|
| `adapter.yml` | 声明式适配器,不写代码 |
| `twin.json` | 回放孪生:这五分钟里应用答应过什么 |
| `cases/*.json` | 案例草稿,断言只有从观测直接算得出来的 |
| `unclassified.jsonl` | 认不出的形状,一条不藏 |

`twin.json` 是白送的第二个实现。同一批案例同时打真实系统与孪生:
```
agent-web compare .tmp-tap/learned/cases --adapters=.tmp-tap/learned/adapter.yml,.tmp-tap/learned/twin.json
```
```
跨适配器判决一致率 100.0% (1/1)
视图契约
learned/adapter.yml 与 learned/twin.json:兼容(2 个集合逐字相同)
```
然后是最要紧的一步——**把应用改坏,不重录**:
```
TODO_APP_BUG=1 node examples/todo-app/server.mjs &
agent-web verify .tmp-tap/learned/cases --adapter=.tmp-tap/learned/adapter.yml
```
```
FAIL todo/recorded assertion_false
目标未达成:tasks 变化 2(期望 3)
```
这条红说明案例抓的是**应用的行为**,不是录制的文本。如果 tap 只是把响应存下来,改坏的应用会照样绿。
三条硬规矩:
- **录状态,不录动作。** 同类工具录动作得到一份脚本,我们录状态得到孪生与判决。
- **口令与 token 不落盘。** 换成 `password#1` 这种稳定伪名,同一个值始终同一个名字。正文里用户自己填的敏感内容不在脱敏范围内,录制文件仍然是内部资料。
- **生成的每一样都标 `inferred`,案例默认草稿。** 不 `accept` 就不出判决——机器观测不许冒充成人的期望。
做不到的三件:不猜业务规则(录到下架成功不等于知道「库存不足该拒绝」);不重放没录到的路径(`unknown`,不是 `pass`);只支持单角色链路(录到管理员才做得成的删除,回放会 403)。
## 断言能说的,比「第 0 行等于 56」多
21 种 kind,全部机器可判,没有一种是「让模型看看对不对」:
| kind | 说什么 |
|---|---|
| `entity_exists` / `field_equals` | 某行存在 / 某字段等于某值 |
| `count_delta` | 某集合数量变化了多少 |
| `status_transition` | 状态按给定序列走过 |
| `unchanged` | 某段观测初态到终态没变 |
| `event_absent` | 某类事件一次都没发生 |
| `relation` | 字段之间的关系:合计等于明细之和、单价乘数量等于小计、列表有序 |
| `sorted_by` / `unique` / `type_of` / `subset` | 形状:有序、不重、类型对、取值在集合内 |
| `composite` | 与 / 或 / 非 |
| `window` / `every_step` | 圈定步骤区间 / 每一步都检查不变量 |
| `pagination` | 跨页检查重复、漏行、顺序与 total |
| `payload` | 检查某一步的 HTTP 响应体 |
| `linkage` | 检查跨集合实体关联与对应数量 |
| `timeliness` | 终态 | 两个事件的真实时间戳之差不得超过上限(`max:"24h"`),必须声明 `timezone` |
| `state_machine` | trace | 只许走声明的那些边(`transitions`);`walk:"rows"` 把流水集合当序列 |
| `recurrence` | 定时任务:窗口内按固定间隔铺槽,抓重复跑、跑偏、漏跑、总数不符 |
| `precedence` | 流水相对序:expect 按先后列事件做贪心子序列匹配,抓「被跳过」(precedence_missing)与「顺序反了」(precedence_inverted)——插行不误报,位置型断言做不到 |
关系式比写死具体值更耐移植:`totalAmount == Σ items.subTotal` 在哪个实现上都该成立,
而 `totalAmount == 56` 只在菜单价格不变时成立。形状断言同理——「列表不重」不依赖任何具体编号。
## 判据准入:一条断言要证明自己有用
断言不是描述,是武器。**杀不掉任何变异体的断言不许入库。**
做法:把「能力」拆成一组语义变异(金额少一分、数量全变 1、流水被吞、越权写放行……),
让每条断言去杀它们。在正确实现上成立、在坏实现上不成立,才算抓得住;两边都成立,就是摆设。
抓获率的演进,每一步都有对应机制上线:
| 阶段 | 抓到 | 靠什么 |
|---|---|---|
| 12 个手写变异体 | 8 / 12 | 基础断言 |
| + 关系式 | 11 / 12 | `relation` |
| + 形状断言 | 14 / 16 | `sorted_by` / `unique` / `type_of` / `subset` |
| + 机器生成 | 38 / 40 | `breed` 照着视图形状造变异体 |
抓不住的分三档,不许混在一起算:**欠账**(看得见差异,只差断言)、
**说不清**(连差异都造不出来,先确认案例走没走到)、**不适用**(该适配器没有它声明的能力)。
「说不清」不等于等价——等价变异体问题四十年没解决,这里只要求投稿人给出可区分性证明。
它不止说哪里有洞,还说补哪条:`--suggest` 给出的每条建议断言,都当场在两个实现上验过
(正确实现 true、坏实现 false)。这四行不是想出来的,是跑出来的。
### 跑两遍,不一样的地方不算故障
同一条案例跑两遍,自增 id、时间、取餐号本来就会变。此前这些差异被算成「变异体被看见了」,
真实后端上 `m-duplicate-row` 的见证是订单号从 196 长到 210,还会据此建议写 `orders.0.id == 196`。
现在每案例先跑两遍正确实现求出漂移集,再从见证里剔除。
顺带的优化:state 层变异体只改观测,certify 不再重放整条轨迹,直接用正确实现的 ctx 变换求值。
真实后端 3 条案例 44.5s → 15.1s(2.95×;计时口径:作者自己的机器与 CI,自证)。等价性由 `scripts/state-equivalence.mjs` 逐格证明并进 CI。
## 变异体不再只靠人想
`breed` 照着规范视图的形状造故障:数值字段差一 / 翻倍 / 丢分,字符串置空 / 大写,
数组丢尾 / 逆序,枚举换值,嵌套叶子逐个试。10 个可造位置出 29 个变异体,4 个恒等算子自动跳过。
**生成不等于入库**:每个都要过 certify,杀不掉的自动落「说不清」,不会污染抓获率。
变异体还能声明 `applies_to`——账本域的变异体在咖啡域就是「不适用」,不进分母。
## 分页:单看每一页都对,拼起来不对
翻页的四种坏法都藏在页与页之间:同一行出现在两页里、边界少一行、翻页顺序忽前忽后、
`total` 报的数字和实际条数不符。**单看任何一页都是好的**,逐页的 `count_delta` 也都是对的
—— 每页确实拿到了 `pageSize` 条。
```json
{ "kind": "pagination", "collection": "orders", "steps": [6, 7, 8],
"total": 5, "pageSize": 2, "ordered_by": "id" }
```
它读的是那几步的**执行返回**(`trace[i].payload`),不是规范视图——视图是整份状态快照,
一条翻过页的列表在视图里和没翻过一模一样,拿视图判分页永远判不出来。
三个只在返回体上下手的变异体,视图完全不变,所以只有它能抓:
| 变异体 | 怎么坏 | 被抓时说什么 |
|---|---|---|
| `m-page-duplicate` | 第二页把上一页末行又带一遍 | 重复行:orders 的 id=1003 第 7 步的返回里出现了两次 |
| `m-page-drop` | 每页少给尾行 | 第 7 步不是最后一页,却只有 1 条(每页 2) |
| `m-page-shuffle` | 第一页内部倒序 | 翻页乱序:按页次拼接后 id 从 1002 掉到 1001 |
顺带修掉一个真 bug:`memory` 适配器的 `order.list` 原来在**规范视图**上按 `ownerId` 过滤,
而视图里只有 `username` —— 顾客的订单列表恒为空,只有店员看得见全部。之前没有案例用过
`order.list`,所以一直没露出来。
## 出题与批改:标签由机制产出,不由模型
一个变异体加一条案例出一道题,题面只给症状和现有断言,**不给 `catch_hint`、不给变异体 id**。
批改不看提交说了什么,只把它的断言放到两个实现上各跑一遍:正确实现上成立、坏实现上不成立,才算解出。
同一批题两份提交:认真写的 11/11、exit 0;把案例原有断言抄一遍的那份 0/11、exit 1——
它交的全是真断言,读代码看不出区别,只能跑。这就是「标签不需要人来标」的意思。
为什么 good 只有 11 份答案,题却有 22 道:建议断言只能表达「集合内某个字段的取值差」,
整实体替换与集合外差异表达不了。剩下 11 道得人写答案。于是拆成两条校验:
默认「必须答满」(漏一题就 exit 1),`--allow-missed` 是「答了就必须对」(没答的照实报,
答错或交不上来的仍然失败)。CI 用后者——防瞎写和防偷懒是两件事,不能混成一件。
`drill grade --record` 把每次批改记进台账,`drill stats` 出统计。
台账只存题号、状态、每个断言的判定、提交指纹——不存提交内容。
所以 11/11 会显示成 `100.0% [74.1%, 100.0%]`,而不是「100% 稳了」;
每题至少 5 次批改才分层,不够就记 `insufficient_data`。
`drill --order=empirical` 按台账里的经验难度出题,难的在前。
## 判决长什么样:一个能打开的页面
不想自己跑?这一节的两个页面已经托管在 GitHub Pages 上,点开就是本次 CI 真跑出来的判决 ↗。
`flow` 把一条案例渲染成自包含 HTML:执行流程逐步可展开、状态事件、每条断言的三值结果、
被拒的步骤直接标红「期望 applied 实际 rejected」。点「只看偏差」只剩有问题的行。
给一个目录,出总览页:判决分布、可判定率、带锚点的案例目录,点目录跳到对应案例。
## 换个命名体系,字段还是那些字段
`learn` 学得出来的前提是「字段名认得」。别人的系统里 `totalAmount` 可能叫 `sum`、
`pickupNo` 叫 `no`、`orders` 叫 `tickets`。`align` 干的就是这件事——**给建议,不替你决定**。
```
$ agent-web align our-view.json their-view.json --out=align.yml
字段映射 · 3 个集合 · 17 / 17 条字段 → 100.0%
orders → tickets 置信 95.0%
createTime → at 94.0%
id → key 94.0%
items → lines 94.0%
pickupNo → no 88.0%
totalAmount → sum 94.0%
```
五路信号,权重写死不调参:名字(复数归一到同义词组)、逐行取值一致、取值多重集一致
(行序不同也认)、枚举重合、基数一致。类型不同直接出局。
**低置信度一律不猜**:低于 0.75 进 `unresolved` 并给 top-3 候选。改名的夹具恢复 17/17、
零错配;把行打乱、`totalAmount` 改成 `x9`、`pickupNo` 删掉之后恢复 15/17,**仍然零错配**,
剩下两条如实写着「需要人签」。宁可说不清,也不许错配——一个错配的字段会让案例在
「看起来对、其实错」的地方判 pass。
## 事故档案:这个仓库踩过的坑,不许踩第二次
14 条档案,每条四要件:症状、根因、指纹(结构 / 见证 / 探测器)、**判据**(哪条命令或测试守着它)。
缺判据不收——没人守的档案只是回忆录。
`recall` 在跑批后比对指纹:第一次命中只提醒,**第二次犯同一条就 exit 1 拦下**。
加深记忆不是靠自觉,是靠退出码。
## 在六个域上验过
| 域 | 实现 | 它证明了什么 |
|---|---|---|
| 咖啡店点单 | 内置内存 + 真实 Spring Boot 后端 | 同一份案例打两个实现,判决一致率 100%;假通过能被抓出来 |
| 流浪动物追踪 `cases/animal` | Spring Boot 3 + MySQL 真实后端(第三方项目) | Spring Page 信封、大写状态、三角色;**审批接口没有角色检查**——案例判 fail,[细节](docs/domains/animal.md) |
| 复式记账 | 两个自造实现(整数分 vs 两位小数字符串) | 内部表示完全不同,视图逐字相同;借贷平衡这类不变式能表达 |
| 词库检索 `cases/lexicon` | 两个自造实现(字典树 + 贪心分词 / 二分 + 反向 DP) | 900 条数据、每页 10 条翻三页:跨页重复、边界丢行、`total` 说谎这三种坏法只有在这个规模上才真的暴露,[细节](docs/domains/lexicon.md) |
| RealWorld 契约 `cases/realworld` | 两份独立实现(`fake-server.mjs` 用 Map,`fake-server-b.mjs` 用数组) | 一份 OpenAPI 契约 + 13 个官方 hurl 套件原样转成案例:100% 判决一致;把 B 故意弄坏之后,`compare` 立刻指出是哪一条案例分歧 |
| 定时任务调度 `cases/cron` | 两个自造实现(数组 + 槽位 Set + 存 nextDue / 槽位键 Map + lastSlot 现算) | 时间是槽位序列,不是标量:`recurrence` 抓重复跑、跑偏、漏跑;4/4 判决一致,[细节](docs/domains/cron.md) |
**另外还接过一个真实后端**:实验室预约平台(`cases/labaccess`,Spring Boot 3 + MySQL)。它带来六个域都没有的形状——**状态被时间推着走**:时段有容量,批准才发凭证,凭证只能在 `startAt <= now < endAt` 里签得出来,有效期还被预约结束时间截断。但只有一份实现、没有 `compare`,按 L11 的门槛**不计入「验过的域」**,[细节](docs/domains/labaccess.md)。
第三个域逼出了一个机制:变异体可以声明 `applies_to`。
账本域的变异体在咖啡域是「不适用」,不进分母——否则抓获率会被无关变异体砸成假坑。
第四个域(词库)补的是**数据规模**这一维:前三个域的分页断言都只在小结果集上跑过,
`total` 说谎、边界丢行这类坏法在小数据上看着全绿。
词库域还顺手补上了一个断言盲区——`pagination` 原先只数各页行数,
从不读实现自己报的 `total`,于是「30 条报成 900」在每一页都满的情况下全绿。
### 第五个域为什么算域:两份实现 + 一个能看见故障的对照
RealWorld(84k star,一份 OpenAPI 契约,990+ 个实现)的官方 13 个 hurl 套件已经原样转成 13 条案例。
这个域的门槛是「两个独立实现 + `compare` 100% 一致」,所以补了第二份实现:
`examples/realworld/fake-server-b.mjs`——同一份契约,内部全换(数组存储、不同的 slug 规则、
增量计数器、单调时钟、表驱动校验),只保留可观测行为。
```bash
node examples/realworld/fake-server.mjs 8299 --allow-reset &
node examples/realworld/fake-server-b.mjs 8298 --allow-reset &
node bin/agent-web.mjs compare cases/realworld --adapters=realworld,realworld-b
```
结果:**13/13 一致,3 个集合逐字相同**。
但「一致」本身可能是案例在睡觉,所以 B 带一个故障开关,专门用来证明对照看得见:
```bash
REALWORLD_B_BUG=order node examples/realworld/fake-server-b.mjs 8297 --allow-reset &
REALWORLD_B_BASE_URL=http://127.0.0.1:8297 node bin/agent-web.mjs compare cases/realworld --adapters=realworld,realworld-b
# → 12/13,realworld/pagination 分歧(pass vs fail),退出码 1
```
`REALWORLD_B_BUG` 还有 `count`(收藏数恒 0)与 `owner`(跳过作者校验)两档。
这三档都写进了 `test/realworld-compare.test.mjs`——一个只验「一致」的测试,
一个什么都不做的适配器也能通过。
## 命令一览
| 命令 | 干什么 |
|---|---|
| `selftest` | 零依赖自测,三种判决各来一次 |
| `verify` | 跑案例出判决 |
| `report` | 由判决结果出指标;`--counts` 只数轨迹(步数 / 重复读 / 绕路比 / 撤销的写 / 重试),不进判决 |
| `accept` | 确认草稿;没确认的案例不出判决 |
| `capabilities` | 打印能力清单与证据等级 |
| `coverage` | 能力覆盖率对账,`--min` 设门槛 |
| `probe` | 探针:查适配器自己有没有在撒谎 |
| `certify` | 判据准入:变异体 × 断言杀伤矩阵,`--suggest` 给补法 |
| `breed` | 照着视图形状自动生成变异体 |
| `drill` / `drill grade` | 出题 / 批改,标签由机制产出 |
| `compare` | 同一份案例喂多个实现,比判决 |
| `drift` | 比较两次跑批的初态形状 |
| `record` / `prepare` / `draft` | 从真实动作日志还原轨迹、生成案例草稿 |
| `flow` | 单案例判决页;给目录出总览页 |
| `discover` | 探一个站点:只读适配器 + 案例草稿 + 施工单 |
| `crosscheck` | — | 不经过适配器自己的 fetch 再发一遍,比对响应:一致 / 不一致 / 查不了 |
| `import hurl` | 把 hurl 套件转成案例草稿(不联网、不执行) |
| `tap` / `learn` | 反向代理录一段流量,长出适配器 / 孪生 / 案例草稿 |
| `mutants` | 变异体目录:谁的坏法、怎么抓、怎么投稿 |
| `ingest` | 把别家的测试报告翻成三值判决,数清「跑绿了但什么都没证明」的比例 |
| `audit` | 假绿审计:拿真实后端当尺子量候选实现 |
| `gate` | 四行口径门禁:契约覆盖率/判据抗变异率/独立证据判决率/unknown 占比一次出齐,缺任一行 exit 1;独立证据行带可比分母(可比为零同缺行),`--min-comparable` 可设下限 |
| `traps` | 陷阱普查:这批判决有多少是盲的 |
| `knowledge` / `recall` | 事故档案 / 重犯检测(第二次 exit 1) |
| `init` | 生成案例模板 |
| `version` | 版本、最近变更、版本号三处是否一致 |
## 几个设计选择
- **没有 LLM judge。** 判不了就记 `unknown` 并留在分母里。引入打分模型等于把自己要解决的问题搬回来。
- **规范视图是契约。** 案例只写视图里的字段,不写实现细节;映射故意写在适配器里,可移植性靠 `compare` 验。
- **「谁来做」是案例的一部分。** 每一步带 `as`,适配器忽略角色会被 traps 指出来。
- **建议断言必须当场验过。** 正确实现 true、坏实现 false 才给出来;位置相关的建议会额外警告。
- **三档状态不混算。** 欠账、说不清、不适用各有各的出口;混在一起,基线表三天就没人看了。
- **两条校验不混用。** 「必须答满」防偷懒,「答了就必须对」防瞎写——合成一条就会互相放水。
## 现成的案例从哪来
自己写案例是最贵的一步。`import` 承认这件事,给了一个不靠猜的入口:**把别人已经写好的接口测试套件转成案例草稿**。