---
title: "CI Eval Tests"
description: "Run dataset-backed Phoenix evaluations as Vitest or Jest tests"
---

`@arizeai/phoenix-client/vitest` and `@arizeai/phoenix-client/jest` let
you write evaluations as ordinary Vitest or Jest tests that fit cleanly
into local development and CI. Each `describe()`
block becomes a Phoenix dataset and a new experiment; each `test()`
becomes a dataset example plus a recorded experiment run; the assertion
outcome is captured as a `pass` boolean annotation. Anything you log via
`logOutput()` / `logAnnotation()` / `evaluate()` lands on the run.
Suite-level `acceptanceCriteria` can fail CI on aggregate annotation metrics,
for example when average quality drops below `0.8`.

Tracing is provided by `@arizeai/phoenix-otel` (OpenInference). LLM and
agent calls instrumented with OpenInference appear as child spans of
each test's task span.

## Install

```bash
npm install -D @arizeai/phoenix-client @arizeai/phoenix-evals vitest dotenv
# or, for jest:
npm install -D @arizeai/phoenix-client @arizeai/phoenix-evals jest dotenv
```

## Minimal Example

```ts
import * as px from "@arizeai/phoenix-client/vitest";
import { expect } from "vitest";

px.describe("generate sql demo", () => {
  px.test(
    "generates select all",
    {
      input: { userQuery: "Get all users from the customers table" },
      expected: { sql: "SELECT * FROM customers;" },
    },
    async ({ input, expected }) => {
      const sql = await myApp(input.userQuery);
      px.logOutput({ sql });
      expect(sql).toEqual(expected?.sql);
    },
  );
});
```

## Docs And Source In `node_modules`

After install, a coding agent can inspect the installed package directly:

```text
node_modules/@arizeai/phoenix-client/docs/
node_modules/@arizeai/phoenix-client/src/
```

That gives the agent version-matched docs plus the exact implementation
that shipped with your project.

## Module Map

| Import | Purpose |
|--------|---------|
| `@arizeai/phoenix-client/vitest` | Vitest entrypoint (`describe`, `test`, `it`, `logOutput`, `logAnnotation`, `evaluate`) |
| `@arizeai/phoenix-client/vitest/reporter` | Vitest reporter that prints a Phoenix-flavored summary at the end of the run |
| `@arizeai/phoenix-client/jest` | Same API surface, wired to Jest globals |
| `@arizeai/phoenix-client/jest/reporter` | Jest reporter |

## Phoenix Terminology

The public API uses Phoenix terms end-to-end — what you read in this package
matches what shows up in the Phoenix UI and the REST API.

| Test field                  | Phoenix concept                          |
| --------------------------- | ---------------------------------------- |
| `input`                     | `Example.input`                          |
| `expected`                  | `Example.output` (reference)             |
| `metadata`                  | `Example.metadata`                       |
| `id`                        | `Example.id` (stable upsert id)          |
| `logOutput(value)`          | `ExperimentRun.output`                   |
| `Annotation.name`           | `ExperimentEvaluation.name`              |
| `Annotation.score`          | `ExperimentEvaluationResult.score`       |
| `Annotation.label`          | `ExperimentEvaluationResult.label`       |
| `Annotation.explanation`    | `ExperimentEvaluationResult.explanation` |
| `Annotation.annotatorKind`  | `annotator_kind` (`LLM` / `CODE` / `HUMAN`) |

## Configuration

The Vitest and Jest submodules reuse the standard `@arizeai/phoenix-client`
and `@arizeai/phoenix-otel` configuration, so setup goes through the
standard Phoenix env vars.

| Variable | Purpose |
| --- | --- |
| `PHOENIX_HOST` | Phoenix base URL |
| `PHOENIX_API_KEY` | Bearer token for Phoenix |
| `PHOENIX_CLIENT_HEADERS` | Optional JSON headers forwarded to the Phoenix client and tracer |
| `PHOENIX_TEST_TRACKING=false` | Disable sync to Phoenix for the current run (tracking is on by default) |
| `PHOENIX_TEST_REPETITIONS` | Default number of times to run each test |
| `PHOENIX_TEST_REPORTER=verbose` | Show every test row plus per-test `output:` detail (default is the compact view) |
| `PHOENIX_TEST_REPORTER_MAX_ROWS` | Max test rows shown per suite in compact mode (default `10`; failures are never hidden) |
| `PHOENIX_TEST_COLOR` | Force ANSI color on/off (otherwise auto: on for a TTY, off in CI / `NO_COLOR`) |

`describe()` also accepts `repetitions` and `dryRun` on its config object,
plus `acceptanceCriteria` for aggregate score thresholds. `test()` accepts
`repetitions` and `dryRun` on its params — see
[CI Eval Tests: Vitest](./ci-evals-vitest) /
[CI Eval Tests: Jest](./ci-evals-jest).

## Repetitions

Run a test (or a whole suite) more than once to measure non-determinism.
Each repetition is a separate experiment run against the same dataset
example, carrying its own `repetition_number`, so the Phoenix compare view
lines them up. Resolution order: per-test `repetitions` → suite
`repetitions` → `PHOENIX_TEST_REPETITIONS` → `1`.

## Dry-Run Mode

Dry-run executes test bodies (and tracing, when a tracer is attached) but
creates no dataset, experiment, run, or annotations in Phoenix. The
reporter still prints a local summary.

- **Whole process** — `PHOENIX_TEST_TRACKING=false`.
- **One suite** — `describe(name, fn, { dryRun: true })`.
- **One test** — `test(name, { input, dryRun: true }, fn)`; that case runs
  as an ordinary local test, with no dataset example and nothing uploaded,
  even when the rest of the suite syncs.

## Reporter Output

The Vitest and Jest reporters print a Phoenix-flavored summary at the end of a
run. By default the output is **compact** and scales to large suites:

- A **scoreboard** across all suites — passed count, the gated metric average,
  the acceptance verdict, and the experiment link, one row per suite.
- A per-suite **results table** showing only failures and evaluator *misses*
  (a run whose annotation score falls below its acceptance bar), with an
  `AGGREGATE` row over the whole suite. Passing rows are hidden behind a
  `… N passing rows hidden` footer — their full detail (input, output,
  annotations) is always written to the JSON artifacts (see
  `PHOENIX_TEST_REPORT_DIR`).

Set `PHOENIX_TEST_REPORTER=verbose` to expand every test row and restore the
per-test `output:` detail block. `PHOENIX_TEST_REPORTER_MAX_ROWS` caps the rows
shown per suite in compact mode (failures are never hidden).

## Acceptance Criteria

Acceptance criteria turn a suite into a CI gate. After every test runs, Phoenix
aggregates the annotation scores you logged and fails the suite if any criterion
misses its bar. Because they run *after* all tests, every case still executes
and the reporter prints the full scorecard before failing — you see every
regression in one run, not just the first.

Each criterion aggregates one annotation (by `annotationName`) with one
`metric`:

- **`average`** — gate on overall quality. The mean score across all runs must
  clear `threshold` (compared in `direction`). A few weak runs are tolerated as
  long as the mean holds.
- **`passRate`** — gate on consistency. Each run *passes* when its `passFn`
  predicate returns `true`, and the suite passes when the **fraction** of
  passing runs is at least `minPassRate` (e.g. `minPassRate: 0.9` ⇒ 90% must
  pass; `minPassRate: 1` ⇒ all of them).

`passFn` receives the run's annotation (`score`, `label`, `explanation`,
`metadata`, …) and returns a boolean, so it can express any pass rule — a score
bar, a score range, a label match, a metadata check, etc.

```ts
px.describe("text-to-sql", () => {
  // each test logs `token_f1` (0–1), `valid_sql` (boolean), and `latency_ms`
}, {
  acceptanceCriteria: [
    // overall quality: the mean token_f1 across the suite must be >= 0.8
    { annotationName: "token_f1", metric: "average", threshold: 0.8 },
    // consistency: at least 90% of runs must score >= 0.7 on token_f1
    {
      annotationName: "token_f1",
      metric: "passRate",
      passFn: (a) => typeof a.score === "number" && a.score >= 0.7,
      minPassRate: 0.9,
    },
    // hard floor: every run must produce valid SQL (boolean must be true)
    {
      annotationName: "valid_sql",
      metric: "passRate",
      passFn: (a) => a.score === true,
      minPassRate: 1,
    },
    // budget: lower is better, so the mean latency must stay <= 800ms
    {
      annotationName: "latency_ms",
      metric: "average",
      threshold: 800,
      direction: "minimize",
    },
  ],
});
```

### Direction

`direction` applies to the `average` metric only — `passRate` encodes its own
comparison inside `passFn`:

- `"maximize"` (default) — higher is better; the mean clears `threshold` when it
  is `>=` it.
- `"minimize"` — lower is better; the mean clears `threshold` when it is `<=`
  it. Use it for cost, latency, or error-rate annotations.

### Scoring details

- **`passFn` flexibility** — because `passFn` receives the whole annotation, a
  `passRate` criterion can gate on a score bar (`a.score >= 0.7`), a range
  (`a.score >= 0.5 && a.score <= 0.9`), a label (`a.label === "correct"`), or
  any combination. Booleans arrive as `a.score === true` / `false`.
- **Booleans in `average`** count as `1` (`true`) / `0` (`false`) when computing
  the mean.
- **Duplicate annotations** — if a run logs the same `annotationName` more than
  once, the last one counts.
- **Missing annotations** — an `average` criterion with no numeric/boolean
  scores, or a `passRate` criterion whose annotation was never logged, fails
  with a "no … found" reason rather than passing vacuously.
- **Skipped vs dry-run** — skipped tests are excluded from the aggregate;
  dry-run tests are included because they still execute locally.

### Reporter output

Criteria are evaluated once, after all tests finish. If any fail, the suite
throws a single aggregated error and the reporter prints an `Acceptance
Criteria` block listing each criterion's observed value, the bar it needed to
clear, and its sample count. The reported value is the **mean** for `average`,
and the **fraction of runs that passed** for `passRate` (so a fully-passing
`passRate` criterion reads `1.000`).

## Where To Start

- [CI Eval Tests: Vitest](./ci-evals-vitest) — config, reporter, and the full `describe` /
  `test` / `test.each` API as it surfaces in Vitest
- [CI Eval Tests: Jest](./ci-evals-jest) — the same API surface in a Jest project
- [CI Eval Test Annotations](./ci-evals-annotations) — `logAnnotation`, `evaluate`, and
  the `Annotation` shape

<section className="hidden" data-agent-context="source-map" aria-label="Source map">
  <h2>Source Map</h2>
  <ul>
    <li><code>src/vitest/index.ts</code></li>
    <li><code>src/vitest/reporter.ts</code></li>
    <li><code>src/jest/index.ts</code></li>
    <li><code>src/jest/reporter.ts</code></li>
    <li><code>src/testing/runner.ts</code></li>
    <li><code>src/testing/acceptance.ts</code></li>
    <li><code>src/testing/helpers.ts</code></li>
    <li><code>src/testing/phoenix-test-tracking.ts</code></li>
    <li><code>src/testing/state.ts</code></li>
    <li><code>src/testing/types.ts</code></li>
    <li><code>src/testing/reporter-format.ts</code></li>
  </ul>
</section>
