@arizeai/phoenix-client
Version:
A client for the Phoenix API
264 lines (218 loc) • 11.3 kB
text/mdx
---
title: "CI Eval Tests"
description: "Run dataset-backed Phoenix evaluations as Vitest or Jest tests"
---
`/phoenix-client/vitest` and `/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 `/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 /phoenix-client @arizeai/phoenix-evals vitest dotenv
# or, for jest:
npm install -D /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 |
|--------|---------|
| `/phoenix-client/vitest` | Vitest entrypoint (`describe`, `test`, `it`, `logOutput`, `logAnnotation`, `evaluate`) |
| `/phoenix-client/vitest/reporter` | Vitest reporter that prints a Phoenix-flavored summary at the end of the run |
| `/phoenix-client/jest` | Same API surface, wired to Jest globals |
| `/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 `/phoenix-client`
and `/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>