API testing

Hover tests your API layer in the same run as the UI. As the agent drives a flow, Hover watches what the app actually calls — and turns the calls worth pinning into a plain @playwright/test *.api-test.spec.ts.

Passive capture, no proxy

While the agent drives the UI, Hover passively captures the app's xhr / fetch traffic off the same CDP connection — no MITM proxy, no cert to install, nothing in front of your network. The requests the browser makes as you exercise a flow are simply observed.

The honest trade-off: this sees browser-originated requests (what the page's JS fetches), not server-to-server calls that never touch the browser. If the check you want to assert is a request the page actually makes, Hover can see it.

The flow

  1. Drive the UI with the grounded tools, as usual.
  2. capture_requests to see what the flow hit — method, url, status, content-type, request body, response shape. Filter with urlContains / method to narrow a busy page.
  3. The agent picks the checks worth keeping — a contract (a GET returns the expected shape), a data mutation (a POST persists), an authz boundary (an endpoint rejects an unauthenticated caller). Most flows need 0–1 API specs; this is selective, not mechanical.
  4. Verify with replay_request before crystallizing, so no status code is confabulated. To prove a "requires auth" boundary, re-send with authenticated: false — a fresh context with no session — and confirm it returns 401 / 403.
  5. crystallize_api_spec writes the verified checks to __vibe_tests__/<slug>.api-test.spec.ts using Playwright's request fixture.

record == replay holds for the API layer too: the captured call is the asserted call. The agent decides whether an API spec is worth it during test_app Phase 3.5 — a distinct phase from the UI crystallize.

What lands on disk

A short *.api-test.spec.ts — a GET that should return 200, plus an authz check that an unauthenticated caller is rejected:

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());
});

Credentials and sessions are never embedded — the authenticated case runs under the suite's stored auth state (see Login & credentials); the unauthenticated case deliberately carries none.

Still plain Playwright

An *.api-test.spec.ts is standard @playwright/test using the built-in request fixture. It runs in CI with npx playwright test, no agent and no Hover runtime in the loop.