See what your Vite pipeline actually did β with receipts.
Restart, hard-refresh, console.log, pray π β Vite knew what happened the whole time, it
just never told you. Async Witness runs your real pipeline and writes a receipt for every run:
proof you, CI, or an AI agent can act on.
β οΈ Early release β the specs are the product truth. Designed by Jack, implemented by Mythos, reviewed by Codex.
A box is a small file that runs inside your real Vite pipeline and asserts what the pipeline did, in the pipeline's own vocabulary:
import { box } from '@async/witness';
export default box('message updates without reload', async ({ browser, project, expect }) => {
// Visit a real route β this auto-starts your real Vite dev server.
const page = await browser.visit('/demo');
// Edit a real source file, like a developer saving in their editor.
const change = await project.edit('src/message.ts', {
replace: ['before', 'after'],
});
// Declare what Vite should have done about it.
await expect.edit(change, {
client: { hmr: 'accepted' }, // hot update applied, no full reload
});
// Confirm the browser actually shows the new text.
await expect.page.text(page, '#message', 'after');
});witness hmrAsync Witness runs the box, restores the edited file, and writes a receipt to .witness/receipts/ β
pass or fail, human- and machine-readable. If the box fails, the receipt explains why in
Vite's own terms: what payload Vite sent, whether the server restarted, what the console said.
- A route renders with every asset loaded and a clean console
- An edit hot-updated, full-reloaded, or silently did nothing β per environment
- A server-only edit left the browser alone
- A config edit restarted the server with the new plugin active
- SSR renders and hydrates without console errors
- The built app behaves like dev (build + preview parity)
- Artifacts are right β manifest entries, no stale placeholders, no
node:fsin worker bundles - A workflow stayed inside a performance budget
Assertions only answer the questions you thought to ask. The interesting failures live in what you didn't ask about β the console error that didn't break your selector, the request that died while the page still rendered, the full reload that happened to land on the right text anyway. Async Witness treats a box run like a case: the receipt is the case file, and everyone who observed the run testifies in it.
Three witnesses watch every browser box from different vantage points. The pipeline
witness is Vite's server side β it knows what hot payloads were actually sent, whether the
server restarted, what each environment did with your edit. The client witness is the page
itself β its console, its uncaught errors, the custom events your framework fired. The
driver witness watches from outside the page over CDP β failed requests with their real
net::ERR_* reasons, navigations the page can't hide, the screenshots. None of them can see
what the others see, which is the point: they corroborate or contradict each other
independently.
Each witness gets a verdict on every box. When a box passes but a witness holds evidence against the run, the pass is marked contested:
pass noisy page records console and network evidence [pipeline+ client! driver!] contested
client reported: 1 console error driver reported: 1 failed request
Every assertion passed here. The page also logged an error and a request died β facts no assertion asked about, preserved instead of swallowed. That's the gap between "tests pass" and "the app is fine", made visible on every run.
Cross-examine any box straight from a stored receipt:
witness evidence 'noisy page'crimes reported (2):
! console error Failed to load resource: net::ERR_ABORTED β reported by client
! request failed GET http://127.0.0.1:5173/missing.js β reported by driver
Witness identity and verdicts are plain data in the receipt JSON β stable, greppable, safe for CI and agents to consume. Color is just how your terminal paints them.
They're great at what they own β but they see the page, not the pipeline that produced it. That gap is how "all tests pass" and "the app is broken" happen at the same time. Async Witness owns the pipeline: the chain from an edit to a Vite environment event to what you see, with a receipt preserving the whole story.
There's no automation framework underneath, either. Async Witness drives the browser itself over the Chrome DevTools Protocol β a couple thousand lines of WebSocket and JSON it owns outright, zero dependencies. One pooled browser serves the whole run and every box gets its own isolated browser context, the same architecture playwright uses internally, which is how witness ended up faster per test than playwright-core on every platform CI measures.
Async Witness drives a Chromium-family browser already on your machine over the Chrome DevTools Protocol β no playwright dependency, no download at install time. Discovery order:
WITNESS_BROWSER_PATHβ explicit override. If it's set but doesn't point at an executable, discovery fails with an error instead of silently falling through to another browser.- System installs of Chrome, Edge, or Chromium, in the usual per-OS locations.
- Playwright's browser cache as a courtesy fallback (
~/Library/Caches/ms-playwrighton macOS,$XDG_CACHE_HOMEor~/.cache/ms-playwrighton Linux,%LOCALAPPDATA%\ms-playwrighton Windows) β fullchromium-<revision>downloads only, newest first, the headless shell is skipped.
macOS, Linux, and Windows are all exercised in CI. When nothing is found, witness fails with the exact paths it checked.
- Specs β product direction and the source of truth
Built in slices. Box authoring, dev/build/preview runs, browser evidence, witness verdicts
with the witness evidence drill-down, the CLI, and JSON receipts work today. The
state-gallery UI, generated types, and receipt replay are coming.
The workspace runs on pnpm (the library itself is runtime-agnostic β it runs wherever Vite runs):
pnpm install # install dependencies
pnpm run test # run the test suite (drives real Vite pipelines)
pnpm run check # format check + lint + typecheckSee CONTRIBUTING.md for the full setup. Start with
specs/ for intent, and .ruler/ for the working agreements.