React Testing API Reference
The public helpers exposed by @pai/react-test-utils.
createReactTestUtils({ Pai, AI }) returns all @pai/test-utils helpers plus React
helpers.
| API | Purpose |
|---|---|
t.toolProps(toolName).inputStreaming(...) | Props while a partial tool input is streaming. |
t.toolProps(toolName).inputAvailable(...) | Props once the complete input is available. |
t.toolProps(toolName).running(...) | Props for a running tool. |
t.toolProps(toolName).waiting(...) | Props for a waiting tool with a pending action. |
t.toolProps(toolName).outputAvailable(...) | Props for a tool with completed output. |
t.toolProps(toolName).outputError(...) | Props for a tool with an output error. |
t.toolProps(toolName).cancelled(...) | Props for a cancelled tool. |
toolSpecProps<TSpec>(toolName) | Props for a generated tool spec, independent of an agent binding. |
createClientToolHarness(definition) | Execute or render one named client-defined tool without providers or transport. |
createClientToolHarness(name, config) | Test a name-less clientTools map config under its registration name. |
t.toolRenderers({...}) | Preserve typed static renderer maps. |
t.threadState(...) | Build a client-visible thread state. |
t.run({ messages: [t.input(...), t.context(...), t.output(...)] }) | Build an ordered run with relation-aware messages. |
t.tool.inputStreaming(...) / inputAvailable(...) / outputAvailable(...) / outputError(...) | Build native AI SDK tool parts for client harness messages. |
t.threadSummary(...) | Build a thread list summary. |
t.createClientHarness(...) | Create real PAI React providers backed by a controlled mock client. |
TestChatUI | Bare composer and native-message transcript for exercising harness send/message/tool-renderer wiring. |
t.run(...) accepts the agent contract's typed metadata. It may be
omitted when {} is a valid client-readable output; when the schema output has required
fields (including fields produced by defaults), the fixture requires the
complete canonical value so test state cannot violate the client contract.
For a run whose accepted inputs are transcript-hidden, pass their non-empty
inputMessageIds separately and place only visible context/output in messages;
messages: [] represents an admitted empty envelope. Thread activity is derived
from state.runs; pending actions are sidecars resolved from an exact
{ message, part } pair. Neither can be overridden independently.
Client Tool Harness
createClientToolHarness() accepts the same definition registered by
useClientTool() or <ClientTool />. Its input, output, action, and ToolData
types are inferred from the definition.
const tool = createClientToolHarness(readPage);
await tool.execute({ selector: "main" });
const call = tool.call({ selector: "main" });
render(<call.View />);
await call.execute();call() starts with a deterministic pending output action. Its renderer
receives an input-available native part and a waiting run status.
Mount call.View before calling
call.execute() to assert initial UI or hold asynchronous execution at a
progress state. Repeated call.execute() calls return the same promise and run
the executor once.
The call exposes:
| Result | Purpose |
|---|---|
call.View | React component backed by the live action, execution, and ToolData state. |
call.execute() | Run the tool once and submit its schema-validated output. |
call.action | Canonical typed in-memory output action for manual renderer flows. |
call.status | Persisted call state: pending, output, failed, or cancelled. |
call.executionStatus | Executor state: idle, running, completed, or failed. |
call.output / call.error | Current validated outcome. |
call.dataWrites | Every ToolData write with channel, id, value, timestamp, and transient flag. |
call.abort(reason?) | Cancel a pending call and fence out late execution results. |
For a name-less config created for a clientTools map, pass the registration
name separately:
const tool = createClientToolHarness("readPage", readPageConfig);Static client-tool renderer fixtures follow the production input lifecycle:
tool.render.inputStreaming({ input })accepts raw partial input without applying schema defaults or normalizations.inputAvailable({ input }),running({ input }), andoutputAvailable({ input, output })require complete input and parse it through the browser schema.outputError({ input, error })andcancelled({ input })accept optional retained input, including incomplete or invalid values. Their native error remains visible; renderer input isundefinedwhen it does not validate.
Use tool.render.inputStreaming(...), .inputAvailable(...), .running(...),
.waiting(...), .outputAvailable(...), .outputError(...), or
.cancelled(...) for a hook-safe static renderer fixture when the state does
not need to be reached through execution.
Tool Prop Builders
t.toolProps(toolName) binds one contract tool and returns state-specific prop
builders for that tool. Prefer binding once near the top of a renderer test:
const compareProposals = t.toolProps("compareProposals");
const completed = compareProposals.outputAvailable({ input, output });
const failed = compareProposals.outputError({
input,
error: "No submitted proposals",
});This keeps each fixture tied to one tool name and avoids repeating the name for every state.
For a shared renderer typed from generated <name>ToolSpecs, use the package
level toolSpecProps() helper. Its string argument is the registration name to
show in the fixture; the generated spec supplies the stable definition-id type
and all payload types. The helper does not set toolId at runtime because the
spec is a type-only input:
import { toolSpecProps } from "@pai/react-test-utils";
import type { PaiToolSpecs } from "../../generated/pai";
const lookupCustomer =
toolSpecProps<PaiToolSpecs["crm.lookup-customer.v1"]>("findCustomer");
const completed = lookupCustomer.outputAvailable({
input: { customerId: "customer-1" },
output: { name: "Example Customer" },
});Here "crm.lookup-customer.v1" selects the stable definition spec, while
"findCustomer" is the registration name exposed in this fixture. This keeps
definition-scoped renderer tests independent of whichever agents alias or
serve that spec.
Waiting fixtures separate the model-provided tool input from the pending action input:
const tool = approveAward.waiting({
input: { jobBriefId: "job-telemetry-platform" },
action: {
name: "approval",
input: { message: "Approve Northstar?" },
},
});The tool name is already bound by t.toolProps("approveAward"), so
action.name is the suspend/action name. The generated pending action still has
the full public name, such as approveAward.approval.
For a client-executed spec (hasExecute: false), use action.name: "output"
to build its implicit output action. The action input is the tool input, and
onSubmit receives the tool output:
const upload = toolSpecProps<UploadToolSpec>("uploadReceipt").waiting({
input: { fileName: "receipt.pdf" },
action: {
name: "output",
input: { fileName: "receipt.pdf" },
onSubmit: (output) => saveUpload(output.url),
},
});This implicit action omits action.ref.suspendName, just like the runtime. If
a backend-executed tool explicitly declares a suspend named output, add
declaredSuspend: true beside action.name so the fixture preserves
action.ref.suspendName === "output".
Suspension History
Tool renderers may need prior suspensions, not only the current pending action.
Wizard-style tools, retries, approval audits, and multi-step human input flows
can all render previous suspend/resume records.
The tool-prop fixture API supports suspensions on every display-state helper:
inputStreaming, inputAvailable, running, waiting, outputAvailable,
outputError, and cancelled.
Each entry has the public PaiSuspensionView shape:
const tool = collectRequirements.outputAvailable({
input: { jobBriefId: "job-telemetry-platform" },
output: { requirementsId: "requirements-1" },
suspensions: [
{
actionId: "action-ask-timeline",
name: "askTimeline",
input: { question: "When should it launch?" },
suspendedAt: "2026-08-21T01:00:00.000Z",
state: "submitted",
resume: { timelineWeeks: 16 },
resolvedAt: "2026-08-21T01:01:00.000Z",
},
],
});This is not limited to waiting(...). A tool can suspend, resume into more
work, suspend again, then complete, fail, or be cancelled. Native client-harness
tool-part fixtures also accept suspensions, but they only expose the AI SDK
states input-streaming, input-available, output-available, and
output-error; running, waiting, and cancelled are derived renderer
display states built with t.toolProps(...).
Client Harness
t.createClientHarness(...) returns a provider and test-side driver controls. It does
not call React Testing Library's render() and does not own your app-level test
providers.
When the agent contract has required client-readable run-metadata fields, pass
initialMetadata. The option becomes required at compile time because
the mock transport cannot execute the agent's server-side metadata schema to
derive defaults for dynamically created runs.
await using h = t.createClientHarness({
threadId: "thread-1",
initialMetadata: { rating: null }, // when required by the contract
seed,
});
await h.driver.responses.queue((r) => r.assistant([r.text("Done.")]));
render(<ChatSurface />, {
wrapper: ({ children }) => (
<ThemeProvider>
<h.Provider>{children}</h.Provider>
</ThemeProvider>
),
});Queue r.error(error) when the next accepted run should fail and expose a
recoverable chat.state.error for retry/regenerate tests:
await h.driver.responses.queue((r) => r.error(new Error("Provider failed")));When a test only needs generic send/message/tool rendering, render TestChatUI
inside the harness provider:
import { TestChatUI } from "@pai/react-test-utils";
await using h = t.createClientHarness({ toolRenderers });
render(<TestChatUI />, { wrapper: h.Provider });TestChatUI is intentionally bare and unstyled. It is a stand-in for testing
PAI wiring, not a substitute for transcript coverage of your application UI.
The harness shape is:
| Result | Purpose |
|---|---|
h.Provider | React provider that mounts Pai.Provider and the selected AI.ThreadProvider. |
h.driver | Test-side driver for seeding state, queuing responses, commands, files, events, and inspecting requests. |
h.client | Real AgentClient facade backed by the mock transport. |
h.thread | Thread handle for the mounted thread. Treat as an escape hatch; prefer interacting through the UI. |
h.threadId | Id of the thread mounted by the harness. |
h[Symbol.asyncDispose]() | Close watchers and dispose mock resources. |