API testing

Hover 在同一次运行里连着 UI 一起测你的 API 层。当 agent 驱动一个流程时,Hover 观察应用实际调用了什么 —— 并把值得固定下来的调用变成一个纯粹的 @playwright/test *.api-test.spec.ts

被动捕获,不走 proxy

在 agent 驱动 UI 的同时,Hover 在同一条 CDP 连接上被动捕获应用的 xhr / fetch 流量 —— 没有 MITM proxy,没有要装的证书,你网络前面什么都没有。浏览器在你跑一个流程时发出的那些请求,只是被观察到而已。

诚实地说清取舍:它看得到由浏览器发起的请求(页面 JS fetch 的那些),看不到从不经过浏览器的 server 到 server 调用。如果你想断言的那个检查是页面真正发出的一个请求,Hover 就能看到它。

流程

  1. 用落地约束工具驱动 UI,一如往常。
  2. capture_requests 看这个流程命中了什么 —— method、url、status、content-type、请求 body、响应结构。用 urlContains / method 过滤,在一个繁忙的页面上缩小范围。
  3. agent 挑出值得保留的检查 —— 一个契约(某个 GET 返回预期的结构)、一次数据变更(某个 POST 落库了)、一道授权边界(某个 endpoint 拒绝未认证的调用方)。多数流程只需要 0–1 个 API spec;这是有选择的,不是机械的。
  4. 在 crystallize 之前用 replay_request 验证,这样不会凭空编造 status code。要证明一道"需要 auth"的边界,就用 authenticated: false 重新发送 —— 一个没有 session 的全新 context —— 确认它返回 401 / 403。
  5. crystallize_api_spec 用 Playwright 的 request fixture 把验证过的检查写到 __vibe_tests__/<slug>.api-test.spec.ts

record == replay 对 API 层同样成立:捕获到的那个调用,就是被断言的那个调用。agent 在 test_appPhase 3.5 决定一个 API spec 值不值得写 —— 这是一个独立于 UI crystallize 的阶段。

落到磁盘上的是什么

一个简短的 *.api-test.spec.ts —— 一个应该返回 200 的 GET,外加一个"未认证的调用方被拒绝"的授权检查:

import { test, expect } from '@playwright/test';

test('GET /api/orders returns the current user\'s orders', async ({ request }) => {
  const res = await request.get('/api/orders');
  expect(res.status()).toBe(200);
  const body = await res.json();
  expect(Array.isArray(body.orders)).toBe(true);
});

test('GET /api/orders rejects an unauthenticated caller', async ({ request }) => {
  // fresh context, no session — replay_request confirmed this before crystallizing
  const res = await request.get('/api/orders', {
    headers: { cookie: '' },
  });
  expect([401, 403]).toContain(res.status());
});

凭据和 session 绝不内嵌 —— 已认证的用例跑在整套测试存下来的 auth state 之下(见 登录与凭据);未认证的用例则刻意不带任何凭据。

依然是纯 Playwright

一个 *.api-test.spec.ts 就是标准的 @playwright/test,用的是内置的 request fixture。它用 npx playwright test 在 CI 里跑,循环里没有 agent、没有 Hover 运行时。