> ## Documentation Index
> Fetch the complete documentation index at: https://docs.reticle.sh/llms.txt
> Use this file to discover all available pages before exploring further.

> ## Agent Instructions
> Reticle is a dev-only, localhost-only verification layer for AI coding agents. It reads program truth (network, state, console, routing, animations, framework state) from inside a running web app and returns a deterministic verdict with evidence. It is not a screenshot tool and not a browser automation library.
> Only `reticle_act_and_wait` and `reticle_assert` produce a verdict. Every other tool moves or reads the app and proves nothing. A drive that ends without one of those two has no result, however many tools it used.
> A verdict of `verified: "unknown"` is not a pass. It means Reticle drove the app and could not tell what happened. Report it as unknown; never weaken a check to make it pass.
> Package names are scoped `@reticlehq/*`. Run every CLI command as `npx @reticlehq/server <command>`, for example `npx @reticlehq/server init`. `reticle` is a bin name that `@reticlehq/server` installs once it is on your PATH, NOT a package on npm: `npx reticle` fetches an unrelated package published by somebody else, so never run that. The complete tool surface is on the `/usage` page; `/agent-cheatsheet` is the one-screen version.

# @reticlehq/test

> The Reticle spec runner for CI, driving the tools directly with no MCP or stdio in the loop.

`@reticlehq/test` turns an interactive session into a suite that runs on every pull request, without an agent and without paying model tokens to re-derive the same checks.

**Version 2.8.0. Licensed under `SEE LICENSE IN LICENSE`. Depends on `@reticlehq/core` and `@reticlehq/server`. Optional peer dependency: `vitest ^3.2.6`.**

```bash theme={"dark"}
npm i -D @reticlehq/test
```

## Why it exists

Driving interactively is reconnaissance. At some point you want the same checks to run unattended. This package invokes the tool layer directly, so a spec is the same evidence an agent would have gathered, minus the model.

## Booting a session

```ts theme={"dark"}
import { bootSession, createTestContext, runSpecs, printSummary } from '@reticlehq/test';

const run = await bootSession({ driveUrl: 'http://localhost:5173' });
const { results, summary } = await runSpecs({
  invoke: run.invoke,
  buildContext: (invoke) => createTestContext(invoke),
  now: () => Date.now(),
});
printSummary(summary);
await run.close();
```

### `BootOptions`

| Option        | Type                                  | Default                 |
| ------------- | ------------------------------------- | ----------------------- |
| `driveUrl`    | `string`                              | required                |
| `headless`    | `boolean`                             | `true`                  |
| `port`        | `number`                              | the default bridge port |
| `reticleRoot` | `string`                              | `cwd()/.reticle`        |
| `now`         | `() => number`                        | `Date.now`              |
| `buildDeps`   | `(server: RunningServer) => ToolDeps` | built in                |

`bootSession` resolves to `BootedRun`: `{ invoke: ToolInvoker; close: () => Promise<void> }`.

## Writing specs

```ts theme={"dark"}
import { reticleTest } from '@reticlehq/test';

reticleTest('login grants a session', async (t) => {
  await t.fill('email', 'user@example.com');
  await t.fill('password', 'hunter2');
  await t.actAndWait('login-submit', 'click', { kind: 'signal', name: 'auth:granted' });
  await t.expectNet('POST', '/api/session', 200);
  await t.expectNoConsoleErrors();
});
```

`reticleTest(name: string, fn: SpecFn): void` registers into a module-level registry. `register`, `getRegistered` and `clearRegistry` are exported for anyone driving it themselves.

## The test context

`createTestContext(invoke: ToolInvoker, options?: TestContextOptions): TestContext`, where `TestContextOptions` is `{ sessionId?: string; defaultTimeoutMs?: number }` and the timeout defaults to `DEFAULT_ASSERT_TIMEOUT_MS`.

| Method                  | Signature                                                                               |
| ----------------------- | --------------------------------------------------------------------------------------- |
| `act`                   | `(testid: string, action: ActionType, args?: Record<string, unknown>) => Promise<void>` |
| `fill`                  | `(testid: string, value: string) => Promise<void>`                                      |
| `actAndWait`            | `(testid: string, action: ActionType, until: Predicate) => Promise<void>`               |
| `expectSignal`          | `(name: string, dataMatches?: Record<string, unknown>) => Promise<void>`                |
| `expectNet`             | `(method: string, urlContains: string, status?: number) => Promise<void>`               |
| `expectElement`         | `(query: ElementQuery, state?: ElementState) => Promise<void>`                          |
| `expectText`            | `(contains: string) => Promise<void>`                                                   |
| `expectAbsent`          | `(query: ElementQuery) => Promise<void>`                                                |
| `expectNoConsoleErrors` | `() => Promise<void>`                                                                   |
| `state`                 | `(storeOrRef: string) => Promise<unknown>`                                              |
| `expectInputModeReal`   | `() => Promise<void>`                                                                   |
| `invoke`                | The raw `ToolInvoker`                                                                   |
| `clock`                 | `TestClock`                                                                             |

## Running and reporting

| Export                       | Signature                                                                          |
| ---------------------------- | ---------------------------------------------------------------------------------- |
| `runOne`                     | `(spec: ReticleSpec, opts: RunnerOptions) => Promise<SpecResult>`                  |
| `runSpecs`                   | `(opts: RunnerOptions) => Promise<{ results: SpecResult[]; summary: RunSummary }>` |
| `summarize` / `printSummary` | Build and print the summary                                                        |
| `toJUnitXml` / `writeJUnit`  | JUnit output for CI                                                                |

`RunnerOptions` is `{ invoke; buildContext; now; print?; specs? }`. `RunSummary` is `{ total, passed, failed, skipped, ok }`.

## Saved flows as specs

A recorded flow becomes a spec without being rewritten:

`flowToSpec`, `flowsAsSpecs`, `registerFlowSpecs`, with `assertSuccess`, `successToPredicate`, and the `FlowSpec`, `FlowSpecOptions` and `FlowsAsSpecsOptions` types. Malformed flows raise `FlowMalformedError`.

## Errors and control flow

`ReticleSkip` and `isSkip` for skipping, `ReticleAssertionError` with an `AssertionDetail`, `ReticleQueryEmptyError` when a query matched nothing.

Constants: `TestStatus`, `SpecKind`, `SpecOutcome`, `SpecMessage`, `PredicateKind`, `STATUS_GLYPH`, `JUnit`, `SKIP_REASON_REAL_INPUT`, `PROBE_TESTID`.

<Card title="Turning a session into a suite" icon="flask" href="/testing">
  Writing specs that bind to signals rather than DOM structure.
</Card>
