Testing¶
The testing strategy starts at the behavior users depend on.
High-level cassette tests run a real Agent, a real provider adapter, and a
real tool. They verify request JSON, response decoding, message history, usage
accounting, and termination. Small unit tests cover errors and helpers that are
difficult to reach clearly through a provider conversation.
Local checks¶
The command enforces zig fmt --check, uses zig build check as the Zig
compiler-backed lint/type-check gate, runs the full suite, and builds the
downstream consumer projects.
zig build audit-cassettes runs independently and is also part of both
zig build check and zig build test. It requires every YAML fixture to have
a typed manifest entry and every manifest path to exist. It rejects duplicate
live paths or IDs, request headers, sensitive header/value shapes, real Bedrock
regions or Azure resource hosts, unredacted multipart bodies, and unstable
parse/stringify normalization.
tests/consumers/agent/ resolves ZigAI as a path dependency and runs the
public agent and evaluation APIs. tests/consumers/providers/ imports the
unified module together with every standalone compatibility package. CI runs
both consumers through ./scripts/check on Linux and macOS.
./scripts/test-cli adds a deterministic process-level test. It starts a local
HTTP fixture and sends all three binaries through the real standard-library
network transport, including authentication, provider decoding, streaming,
manifest loading, external tool execution, and the follow-up model request.
When OPENAI_API_KEY and ANTHROPIC_API_KEY are available, run the opt-in live
smoke tests with ./scripts/test-live. It makes one minimal request to both;
when GEMINI_API_KEY is also present, it checks Google too. Live requests
complement cassettes; they never replace deterministic tests.
Provider cassettes live in tests/cassettes/. Their codec, recorder, replay
transport, and body filters live in tests/support/; none are exported by the
library or compiled into the command-line clients.
tests/fixtures/pydantic_ai/ contains cross-language message golden files.
Regenerate the pinned v2 fixture with the official upstream adapter:
uv run --with pydantic-ai-slim==2.31.0 \
python scripts/generate-pydantic-ai-messages-fixture.py \
tests/fixtures/pydantic_ai/messages-v2.31.0.json
The generator checks the installed package version before writing the fixture.
tests/cassettes/models/ contains real tool-loop recordings for eight models
from each first-party provider. The matrix spans multiple model generations and
size tiers. Every recording is defined once in the typed
tests/support/cassette_manifest.zig manifest. Recording and replay share its
stable ID, provider, model, scenario, fixture path, execution route, and
credential requirements. Its source field distinguishes live recordings from
deterministic failure injections.
tests/cassettes/buffered/ contains a separate minimal text response for the
same 24-model first-party matrix. These fixtures isolate ordinary request
encoding, response decoding, usage, and terminal-state handling from tools.
tests/cassettes/streamed/text/ and tests/cassettes/streamed/tools/ contain
48 real streams for that matrix. Text fixtures require decoded deltas, usage,
one final result, and exact pong output. Tool fixtures additionally require
one completed function call, one local result, final text containing the
returned temperature, and complete two-request replay.
tests/cassettes/providers/ and tests/cassettes/compatible/ cover the eleven
configured Chat Completions providers. Every provider has a real buffered and
streamed-text response plus a complete function-tool loop. Scenario models may
differ when a provider's text model does not expose tools; for example,
Together text uses GPT-OSS while its tool row uses the serverless Qwen model
from Together's function-calling example.
tests/cassettes/structured/ and tests/cassettes/thinking/ contain one real
recording per first-party capability profile. Structured fixtures validate the
native OpenAI text.format, Anthropic output_config.format, and Google
responseJsonSchema request shapes through a provider-neutral typed result.
Thinking fixtures validate each provider's high-effort wire control and
normalized reasoning usage. Anthropic additionally proves streamed thinking
parts, deltas, opaque signatures, and durable message history; OpenAI and
Google keep provider-hidden reasoning out of neutral content parts.
tests/cassettes/errors/ contains deterministic OpenAI, Anthropic, and Google
failure sequences. Each one covers a 429 recovery with retry hints and rate
metadata, retry exhaustion after two 503 responses, malformed-success
classification, and a non-retryable 400 observed through strict message, code,
and body bounds. These fixtures use provider-realistic wire envelopes but are
not live recordings: manufacturing upstream failures would be unsafe and
nondeterministic. One shared Chat Completions contract fixture runs through all
eleven named compatible providers, proving their provider identities without
duplicating identical protocol YAML.
The same directory also contains one deterministic failure contract for each specialized native route: Bedrock Converse, Azure Responses, Mistral Conversations, and Cohere v2 Chat. Their provider-specific request and success shapes exercise rate-limit recovery, server-error exhaustion, malformed success classification, and bounded error observation without live failures.
tests/cassettes/native/ contains real provider-native recordings: OpenAI web
search, Anthropic web search plus fetch, Google Search plus URL Context, a
complete Amazon Bedrock Converse function-tool loop, and an Azure OpenAI v1
Responses function-tool loop. It also contains a Mistral Conversations web
search with native execution entries, references, and connector-token usage,
plus a Cohere v2 strict function-tool loop with citations. These recordings
verify the native request shapes and responses. Replay also proves the neutral
evidence boundary: OpenAI URL citations, Anthropic server-tool calls and
results, Google grounding chunks and supports, and Mistral tool references are
retained as structured provider details alongside normalized usage counters.
tests/cassettes/specialized/ completes the ordinary success matrix for the
specialized native routes. It contains buffered Bedrock Converse; buffered and
streamed Azure Responses; buffered, streamed, and function-tool Mistral
Conversations; and buffered and streamed Cohere v2 Chat. The existing native
fixtures supply the Bedrock, Azure, and Cohere function-tool loops. Bedrock
streaming is excluded because its current adapter is buffered-only.
The specialized scenario applicability is deliberate:
| Native route | Buffered | Streamed text | Function tool | Retry/error |
|---|---|---|---|---|
| Bedrock Converse | Live | Excluded: adapter has no ConverseStream transport | Live | Deterministic |
| Azure Responses | Live | Live | Live | Deterministic |
| Mistral Conversations | Live | Live | Live | Deterministic |
| Cohere v2 Chat | Live | Live | Live | Deterministic |
OpenAI-compatible providers support all three success scenarios through the shared Chat Completions transport. Their failures are deterministic because manufacturing live rate limits and server failures is unsafe and unstable.
tests/cassettes/rich/ contains one real inline-image exchange for each
first-party provider. Replay checks the semantic answer (red) rather than
accepting any non-empty model output.
tests/cassettes/files/ contains a real lifecycle for each first-party
provider. OpenAI covers a downloadable fine-tuning input; Anthropic records the
safe upload, inspect, reuse, and delete path for a non-downloadable uploaded
file; Google covers resumable upload, inspect, reuse, and delete.
Record the complete matrix with real credentials:
Inspect the manifest without making a request or exposing credential values. The first command reports readiness for every recording; the second prints only recordings whose complete credential set is present:
Pass a stable manifest ID, provider, exact model, or scenario to record only part of it:
zig build record-cassettes -- anthropic
zig build record-cassettes -- gemini-3.5-flash
zig build record-cassettes -- openai/gpt-5-nano/native-tool
zig build record-cassettes -- function-tool
zig build record-cassettes -- first-party-buffered
zig build record-cassettes -- first-party-streaming
zig build record-cassettes -- first-party-capabilities
zig build record-cassettes -- streamed-text
zig build record-cassettes -- streamed-function-tool
zig build record-cassettes -- structured-output
zig build record-cassettes -- thinking
zig build record-cassettes -- provider-error
zig build record-cassettes -- native-tools
zig build record-cassettes -- native-google
zig build record-cassettes -- native-bedrock
zig build record-cassettes -- native-azure
zig build record-cassettes -- native-cohere
zig build record-cassettes -- specialized-success
zig build record-cassettes -- specialized-errors
zig build record-cassettes -- mistral
zig build record-cassettes -- rich-content
zig build record-cassettes -- rich-anthropic
zig build record-cassettes -- files
zig build record-cassettes -- files-google
Unknown and empty filters fail before network I/O. Credentials are resolved
only after selection, so recording one provider never requires unrelated keys.
Deterministic fixtures are reported as fixture by --list, omitted by
--list-runnable, and explicitly skipped by the recorder without resolving a
credential or making a request.
The recorder accepts OPENAI_API_KEY, ANTHROPIC_API_KEY, and either
GOOGLE_API_KEY or GEMINI_API_KEY. Native Bedrock recording additionally
uses AWS_BEARER_TOKEN_BEDROCK and AWS_DEFAULT_REGION; native Azure recording
uses AZURE_OPENAI_API_KEY and AZURE_OPENAI_ENDPOINT; native Mistral uses
MISTRAL_API_KEY; native Cohere uses CO_API_KEY. It replaces each cassette
atomically only after a successful agent tool loop. Authentication headers are
never copied; provider-specific URL filters replace the real Bedrock region and
Azure resource endpoint.
The files follow Cassetter v1 YAML. JSON bodies are nested YAML values, streamed responses use literal text blocks, and binary bodies are explicit. Headers are omitted by default. Review request and response content before committing a new cassette. Replay compares JSON request bodies structurally, so harmless object whitespace and key formatting introduced by YAML serialization do not cause a mismatch; non-JSON bodies remain byte-exact.
Focused deterministic codec fixtures also live in the typed manifest. This
lets the audit reject orphaned files without presenting synthetic contracts as
live recordings; the recorder reports them as fixture and never resolves
credentials for them.
File recordings use the same request filters during recording and replay. Multipart boundaries are normalized, uploaded bytes are replaced with an explicit redaction marker, and provider-issued upload URLs are mapped to stable fixture URLs. Response headers remain opt-in: an allowlist can retain a safe value or replace it, while sensitive header names are rejected even when a rule selects them. The live caller still receives the original provider header; only the recorded copy is sanitized.
Coverage policy¶
The gate is 100% line coverage for every executable Zig line under src/,
excluding Zig's standard library, generated files, two type-declaration lines,
and the timeout sleep expression that LLVM/kcov cannot trace inside Zig's
concurrent I/O task. Alias-only standalone provider modules have no executable
lines; the downstream consumer builds compile those instead. Coverage comes
primarily from public, high-level behavior; a line-only unit test should exist
only when a high-level scenario would obscure the behavior being tested.
Run ./scripts/coverage on Linux. It compiles with LLVM debug information,
runs unit, cassette, agent, real-transport, CLI-success, and CLI-error paths,
merges their reports, checks that every source file is represented, and fails
unless the final line rate is exactly 100%. CI runs the same command. macOS
kcov requires extra debugger signing, so coverage remains a Linux-only gate.
tests/mcp_conformance.zig keeps the MCP 2026-07-28 method and compatibility
inventories executable. The src/mcp.zig tests cover nested schemas, malformed
envelopes, allocation failures, stdio/SSE ordering, capability guards,
pagination cycles, and lossless extension JSON. The human-readable companion is
MCP conformance.
Official interoperability inputs are test assets, never production modules.
tests/mcp/upstreams.yaml pins the official conformance framework and reference
servers by commit. The matrix deliberately includes both stdio and Streamable
HTTP and is validated by the ordinary test suite before live jobs consume it.
Recorded wire evidence uses the Cassetter-style YAML codec in
tests/mcp/transcripts.zig. One interaction preserves a JSON-RPC request, the
ordered notifications observed while it was active, and its response. Replay
is transport-neutral; the transcript metadata records whether the evidence was
captured from stdio or HTTP.
zig build mcp-interop is the opt-in recorder. It expects an official server
checkout at the revision named in tests/mcp/upstreams.yaml and atomically
replaces the requested fixture only after discovery and the tool, resource,
and prompt inventories all succeed:
zig build mcp-interop -- stdio <server> <revision> <fixture.yaml> <command> [args...]
zig build mcp-interop -- http <server> <revision> <fixture.yaml> <endpoint>
The committed TypeScript todos recordings prove identical behavior over stdio and HTTP. The Python everything-server recording adds a second official SDK and a much broader schema surface. Ordinary CI replays all three without a network connection or upstream toolchain.
./scripts/mcp-interop typescript <checkout> and
./scripts/mcp-interop python <checkout> verify that a prepared checkout is at
the manifest pin, exercise its live server, and compare fresh transcripts with
the committed fixtures. .github/workflows/mcp-interop.yml runs those checks
only through workflow_dispatch; normal CI remains deterministic and
network-free beyond dependency installation.
The same manual workflow checks out the pinned official conformance framework
and runs its complete 2026-07-28 client requirements against
zigai-mcp-conformance-client. This adapter is test-only and deliberately
thin: it translates runner scenarios into public ZigAI calls instead of adding
conformance hooks to the library. tests/mcp/conformance-baseline.yaml names
every scenario that is not supported yet. The runner fails for a new
regression, an unlisted failure, or a stale baseline entry that has started to
pass, so the file doubles as an executable interoperability backlog.
Benchmarks¶
Production benchmarks live under benchmarks/, separate from runtime source
and correctness tests. Their checked-in baseline format is strict and
versioned. Workload names must be unique and sorted, and checksums make a
semantic workload change fail until the baseline is reviewed deliberately.
Thresholds use integer basis points. A null threshold publishes timing data
without gating CI; adding a number is the explicit review action that enables
a regression check. The comparator rejects missing, extra, reordered, or
checksum-changed workloads and emits stable JSON with pass or fail.
Run the complete suite in the safety-checked release mode used for published baselines:
The command prints stable JSON measurements for seven sorted workloads:
- provider-neutral history processing plus serialization and parsing;
- MCP 2026-07-28 request validation, dispatch, and response encoding;
- a complete agent turn with four parallel local tool calls;
- OpenAI Responses request decoding;
- OpenAI Responses request encoding;
- reflected schema generation plus schema and output validation; and
- OpenAI streaming request encoding, SSE event decoding, event delivery, and final response accumulation.
Every workload runs warmup iterations followed by 21 timed samples of 32 operations. The median nanoseconds per operation is reported. Each operation also hashes its semantic result; the runner rejects a changing checksum and keeps it observable so optimized builds cannot discard the work.
The harness and a two-run determinism smoke test for every workload run in the
ordinary zig build test gate. Timed measurements remain in the dedicated
benchmark step and are not mixed into line coverage.
Checked-in baselines live in benchmarks/baselines/ for Linux x86_64 and
macOS ARM64. Compare locally on a matching platform and write a report with:
zig build benchmark -Doptimize=ReleaseSafe -- \
check benchmarks/baselines/macos-aarch64.json benchmark-report.json
The isolated CI matrix publishes benchmark-report.json for both platforms.
It always rejects Zig-version, optimization-mode, target, workload-set, and
checksum drift. Timing entries remain unreviewed and cannot fail CI until a
maintainer adds max_regression_basis_points to that platform's baseline.
Fuzzing¶
ZigAI fuzzes every untrusted parser family: history and deferred state, JSON Schema output, JSON/YAML agent specifications, buffered and streaming provider responses, Cassetter YAML, MCP JSON-RPC and SSE framing, CLI tool manifests, and HTTP retry metadata.
Run a bounded local campaign with an iteration limit:
Use a larger suffix for longer runs, for example 10K or 1M. Each target
also carries a small valid corpus, so the ordinary test suite executes stable
parser smoke cases without fuzz instrumentation. Inputs are capped at 16 KiB;
the production parsers apply their stricter documented limits after that.
The pinned Zig 0.16.0 test runner requires error-return tracing to be disabled in fuzz mode. The script applies that workaround only to the fuzz compilation; normal checks retain the default tracing behavior. CI runs a 1,000-iteration bounded campaign on macOS for every push and pull request. Zig 0.16.0 currently emits an empty fuzzer coverage record on Linux before the targets start, while the same targets run normally on macOS; Linux still runs the corpus smoke tests and the 100% line-coverage gate.
Stress and allocation failures¶
The bounded stress suite exercises the public API under sustained load:
- 128-step tool loops and 32-call parallel tool batches;
- cancellation racing an active model request, with work drained before return;
- repeated provider connection failures followed by successful retries;
- 1,024-message histories and 2,048 streaming deltas;
- every allocation failure in an agent tool loop;
- 512 HTTP client init/deinit cycles; and
- 128 real HTTP requests through one client while the fixture closes every connection, forcing connection recovery.
Run both safety-oriented build modes locally:
Run only one mode while iterating:
The suite is deliberately bounded and deterministic, so it is suitable for every push. CI runs it in both modes on Linux and macOS. Longer fuzz campaigns and live-provider tests remain opt-in because their duration or external state is not deterministic.
Pricing snapshots¶
data/genai_prices_v2.json is pinned to a pydantic/genai-prices release commit
and checksum. tools/pricing_snapshot.py turns it into
src/pricing_snapshot.zig; do not edit the generated Zig file.
To update it, change the release metadata and snapshot date in the generator, then run:
The snapshot applies date-based prices as of its date. Recurring time-of-day discounts stay out of the standard table; use a custom table for those. Tests only read the checked-in data and never call a live pricing or billing API.
Model compatibility snapshot¶
data/model_catalog.json is the only hand-edited model snapshot source.
src/model_catalog_snapshot.zig is generated and must never be edited by hand.
For each update:
- Verify IDs, aliases, limits, lifecycle, and capabilities against the linked primary provider page. Record only capabilities supported by both the model and ZigAI's adapter.
- Keep providers, model IDs, aliases, and enum lists in deterministic order;
advance
updated_atwithout changingversionunless the source schema changes. - Run
zig build update-model-catalogand review both the JSON source and generated Zig diff. - Run
zig build check-model-catalog,zig build test, and exact coverage.
The normal zig build check target runs the drift checker. It parses the
source with unknown fields rejected, validates the provider-neutral catalog,
and compares generated bytes, so CI cannot accept a stale or hand-edited
snapshot.
Provider extension settings¶
Provider request tests inspect every portable setting at the wire boundary. Extra-body fixtures must be small inline JSON objects without credentials. Tests cover provider-tag mismatches, malformed or non-object JSON, attempts to shadow adapter-owned fields, reserved headers, and CR/LF injection.
Agentic maintenance¶
Four GitHub Agentic Workflows watch CI, documentation, provider APIs, and test quality. Their agent jobs are read-only. The only repository write they can request is one file-scoped pull request for maintainer review.
Recompile workflow lock files after changing their frontmatter: