MCP 工具
Hover 的编写引擎是一个 MCP server(@hover-dev/mcp),把它接入你已经在用的编码 agent 即可。接好之后,你的 agent 就获得一小组带落地约束的浏览器工具,外加一个 test_app prompt。agent 通过这些工具驱动你的应用 —— 而由于每个动作都是按 role+name → testId → text 从页面 snapshot 上读出来去定位控件的,所以驱动某个动作的那个 selector,就是 Hover 最终保存的那个。这正是 record == replay 成立的原因。
安装
先全局安装 MCP,再注册这个 bin(这里以 Claude Code 为例 —— 任何支持 MCP 的 agent 都行):
npm i -g @hover-dev/mcp
claude mcp add hover -- hover-mcp
加上这个 server 之后,工具和 /mcp__hover__test_app 命令都会带进来。其他 agent 的接法见 安装。
想让 agent 用另一种语言跟你交流?注册时加上 -e HOVER_LANG=zh(一个语言代码或语言名称)—— 各个 workflow 随后就会用那种语言提问、给出总结,而代码和 spec 名称仍保持英文。见 安装 → 想用另一种语言?。
MCP prompt
server 把它的各个 workflow 作为 MCP prompt 发布出来,Claude Code 会把它们呈现为 slash command:
/mcp__hover__test_app # 映射整个应用并 crystallize 出一套测试
/mcp__hover__test_app login # …或只限定到某一块区域
/mcp__hover__guard "游客不能进结账" # 在动手实现前先声明一条回归
/mcp__hover__build checkout # 把已声明的 guard 驱动到绿、经 CI 验证
/mcp__hover__heal # 重放每一个保存的 spec,修掉任何漂移
/mcp__hover__heal login.spec.ts # …或只自愈一个 spec
/mcp__hover__optimize checkout # 用观察到的断言丰富某一个 spec
/mcp__hover__optimize # …或丰富每一个 spec(各生成一个候选)
/mcp__hover__lint # 给 .hover/ 测试 wiki 做健康检查
/mcp__hover__ask "does a guest see the cart?" # 带引用地向 wiki 提问
test_app [scope] 带着 agent 走一遍随规模而变的循环:映射业务线(agent 用它自己的文件工具读取你的路由 + 导航)、选定一个范围、一次覆盖一条流程(navigate → snapshot → 操作 → 断言 → crystallize),可选地加上一个 API 层的检查(Phase 3.5)并把 共享流程提取成 Page Object(Phase 5),然后在 .hover/hover-map.md 里更新覆盖情况。一个大型应用不必一次跑完 —— 覆盖一批、更新地图,本身就是一个可续跑的单元。
heal [spec] 跑的是 self-heal 循环:把某个保存的 spec 里录制下来的步骤对着线上应用重放,找到第一个不再能定位的步骤,只对那一步重新落地约束。不带参数则扫描每一个 spec。
ask <question> 从 .hover/ wiki 里回答关于你应用的问题 —— business map、记下来的规则、crystallize 出来的 spec,以及运行日志 —— 并注明它是从哪些内容得出的。它是只读的(不开浏览器),而当某个回答确立了一条 wiki 里原本缺失的持久规则时,它可以用 record_fact 把这条规则归档回去,让下一次提问更省。
lint 给你的 .hover/ 测试 wiki 做健康检查。lint_map 跑机械式检查:指向已删除 spec 的业务线、已覆盖但其 spec 上次运行为失败或 flaky 的业务线(一个自愈候选),以及没有任何业务线映射到的 spec。这个 prompt 在其上叠加了那些交给 agent 更擅长判断的检查 —— .hover/memory/ 里相互矛盾的规则,以及你代码里存在但地图从未提及的路由 —— 然后逐项给出修复建议:自愈这个回归、补上缺失的业务线、订正规则。它让地图随你的应用成长而保持诚实。
先声明 guard 的开发方式
有两个 prompt 把顺序反了过来:先声明回归,再让 agent 把功能实现到通过为止。这就是先定义行为的闭环。
guard <intent> 在你实现功能之前先声明一条回归。agent 会访谈你意图里那些模糊的边界,把答案记成按行锚定的业务规则(record_fact),并通过 declare_guard 工具在业务图上写下一条待办的 - [ ] 行与验收标准。不伪造任何东西:这是一条声明式的红,真正可执行的 spec 之后仍从真实流程里 crystallize 出来,所以 record == replay 依然成立。
build <line> 把已声明的 guard 驱动到绿。agent 会实现、用接地工具在真实应用里逐条验证验收标准、crystallize_spec 这条流程、跑完整本地回归、push,然后轮询 Hover Cloud 的逐条 spec 裁定并分派:bug → 改代码,drift → 自愈过时的 spec,unclear / 裁判把握不足 → 停下来升级给你。它有预算上限(约 10 轮本地、约 3 轮 CI),从不为了通过而弱化断言,合并始终留给人。
optimize [spec] 丰富一个已 crystallize 的 spec。它把这个 spec、录制会话观察到的结果,以及你现有的 Page Object 一并交给你的 agent,请它为观察到的反馈补上断言、把本次运行的值(一个生成的 id、一个订单号)替换成稳定的值,并在某段步骤序列匹配时复用一个 Page Object。思考由你的 agent 完成;Hover 不挑任何模型。结果会经过 save_optimized_spec,它会校验并把候选写到 .hover/cache/optimized/ 下。在你审阅 diff 并提升候选之前,你的 spec 保持原样不变。传一个 slug 优化单个 spec,或不传以优化每一个 spec —— 每个都通过 optimize_brief 工具得到各自的审阅候选。一旦你审阅过某个候选的 diff 并想要它,promote_optimized_spec 一步就能应用(不用手动 mv)。
工具
| 工具 | 用途 |
|---|---|
browser_navigate(url) | 在被测应用(调试用的 Chrome)里打开一个 URL。 |
browser_snapshot() | 把页面读成一个 ARIA snapshot(role + accessible-name 树)。agent 在操作之前调用它,拿到给落地约束工具用的精确 role+name。 |
click_control(target) | 用一个落地约束的 target 点击某个控件(优先 role+name → testId → text)。 |
fill_control(target, value) | 用一个落地约束的 target 往某个 textbox / 字段里输入一个值。 |
select_control(target, value) | 用一个落地约束的 target 在某个 <select> 里选一个选项。 |
check_control(target, checked?) | 用一个落地约束的 target 勾选 / 取消勾选某个 checkbox / radio(能处理 sr-only / 隐藏的 input)。 |
assert_visible(target) | 断言某个控件 / 文本此刻可见 —— 会把一个 expect(...).toBeVisible() 捕获进保存的 spec。 |
recall_business_knowledge() | 回忆早先几次运行学到的关于本应用的东西(业务规则、预期行为、访问策略)。agent 在一开始就调用它。一旦一个应用记下的规则很多,它返回的是一个索引(每条规则一行),而不是每一条的全文 —— 渐进式披露,这样随着 wiki 复利增长,recall 依然省。 |
recall_fact(name) | 当索引里的某条规则与正在测试的内容相关时,按名字读出这条规则的全文。它是 recall 索引背后的按需层。 |
record_fact(title, rule, type?) | 记下一条持久的业务规则,这样本次运行和将来的运行都不用再问一遍。只记规则 —— 绝不记密码、token 或个人数据等 secret。 |
crystallize_spec(name, description?) | 把 agent 刚刚执行的流程(自上次 crystallize 以来的那些落地约束动作)保存成 __vibe_tests__/ 下的一个纯 @playwright/test spec。 |
API 层工具
在 agent 驱动 UI 的同时,Hover 在同一条 CDP 连接上被动捕获应用的浏览器流量 —— 没有 MITM proxy,没有证书。这些工具把那些流量变成一个纯粹的 *.api-test.spec.ts。见 API testing。
| 工具 | 用途 |
|---|---|
capture_requests(urlContains?, method?) | 返回驱动过程中观察到的应用 xhr / fetch 调用 —— method、url、status、content-type、请求 body、响应结构。可选的 urlContains / method 过滤条件能缩小列表。 |
replay_request(...) | 在 crystallize 之前重新发送一个(可能被改动过的)请求以验证某个检查,这样不会凭空编造 status code。authenticated: false 会用一个没有 session 的全新 context —— 这是证明"需要 auth"边界会返回 401 / 403 的办法。 |
crystallize_api_spec(name, description?, checks[]) | 用 Playwright 的 request fixture 把验证过的检查写成一个纯 @playwright/test 的 *.api-test.spec.ts。 |
Self-heal 工具
| 工具 | 用途 |
|---|---|
replay_spec(slug) | 把某个保存的 spec 的录制下来的落地约束步骤对着线上应用重放,报告第一个不再能定位的步骤 —— 它的序号,以及它当时要找的 role+name。无需跑 playwright test 就能检测漂移。见 Self-heal。 |
lint_map() | 给 .hover/ 测试 wiki 做健康检查:把 business map 与真实的 spec 文件、运行台账交叉核对。报告指向已删除 spec 的引用、回归的覆盖(一个自愈候选),以及没有映射的 spec。确定性 —— 无 LLM。 |
套件工具
| 工具 | 用途 |
|---|---|
detect_shared_flows() | 报告在你保存的多个 spec 里重复出现的非登录流程。 |
extract_page_objects() | 把那些共享流程提取到 pages/ + fixtures.ts 并把相关 spec 折叠到它们上面。确定性 —— 无 LLM。见 Page Object 抽取。 |
optimize_brief(slug) | 拿到某个 spec 的改进 brief(它的代码 + 观察到的结果 + 你的 Page Object + 规则)。/mcp__hover__optimize 不带参数时用它在循环里丰富每一个 spec;单个 spec 的调用则直接内联得到这个 brief。 |
save_optimized_spec(slug, code) | 把一个被 agent 改进过的 spec(来自 optimize prompt)作为审阅候选归档。Hover 会校验它(语义化 selector、无 waitForTimeout / XPath、能解析)、软批量处理末尾的断言,并写入 .hover/cache/optimized/<slug>.spec.ts.draft。绝不覆盖你的 spec —— 你审阅完 diff 后再提升候选。 |
promote_optimized_spec(slug) | 应用一个已审阅的候选:用它的 draft 覆盖 __vibe_tests__/<slug>.spec.ts 并删掉 draft。所以一旦你看过 diff 并认可,"提升它"就是一步 —— 不用手动 mv。VS Code 驾驶舱里的候选审阅做的是同一件事,只是可视化的。 |
Cloud 工具(已登录)
在 agent 关联到某个 Hover Cloud 项目后可用。它们只读 Cloud,从不跑浏览器。
| 工具 | 用途 |
|---|---|
cloud_context() | 一次调用完成定位:agent 以谁的身份连接、这个 repo 是不是 Cloud 项目(org、环境 + URL + 账号),以及编辑器里当前激活的是哪个环境——也就是一次 drive 或 heal 会打向哪里。 |
cloud_failures() | Cloud 从 CI 排进队列的漂移 spec,每条都带环境 + URL,好让 agent 在自愈前先激活正确的目标。 |
cloud_run_result() | 一次已摄入的 CI 运行,以及每个失败意味着什么:状态、drift / bug / unclear 裁定,以及裁判的建议分与理由。轮询 Cloud 的 /api/v1/runs;repo 从 git origin 自动识别。 |
declare_guard(...) | 在业务图上写下一条待办的 guard 行 + 验收标准(供 /mcp__hover__guard 使用)。确定性、幂等——绝不会把 [x] 翻回去。 |
落地约束的 target
那些操作工具接收一个从 snapshot 上读出来的小 target 结构:
{
role?: string; // ARIA role,例如 'button'、'textbox'、'link' —— 与 name 搭配
name?: string; // accessible name,一字不差按显示的来 —— 与 role 搭配
testId?: string; // 一个 data-testid,当没有干净的 role+name 可用时
text?: string; // 真实的可见文本 —— 最后手段
within?: { role: string; name: string }; // 先限定到某个容器里
}
这个结构与面向 agent 的 schema 一致,所以你在这里看到的,就是 agent 在它的工具目录里收到的。
为什么要落地约束式操作
一句松散的"点击 Submit 按钮"指令没法往返回一个可重放的 selector —— 它会 crystallize 成一个凭空编造的 getByText,下次未必匹配得上。Hover 的落地约束工具拿的是直接从 snapshot 读出来的 target,通过 CDP 走 page.getByRole(...) / getByTestId(...) / getByText(...)。驱动动作的那个 selector,就是被保存下来的那个,所以你重放的就是你录制的。保存时没有任何 LLM 在改写代码 —— crystallization 是确定性的。
agent 从不徒手写 spec
只有 crystallize_spec 会写文件,而它并不请模型去撰写 Playwright —— 它是把录制下来的落地约束步骤确定性地翻译出来。做探索的智能是那个调用方 agent;保真度由 Hover 在输出端保证。Hover 不捆绑任何模型、不持有任何 key(BYO-CLI):是 agent 在跟它自己的供应商对话。