PAIPAI

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.

APIPurpose
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.
TestChatUIBare 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:

ResultPurpose
call.ViewReact component backed by the live action, execution, and ToolData state.
call.execute()Run the tool once and submit its schema-validated output.
call.actionCanonical typed in-memory output action for manual renderer flows.
call.statusPersisted call state: pending, output, failed, or cancelled.
call.executionStatusExecutor state: idle, running, completed, or failed.
call.output / call.errorCurrent validated outcome.
call.dataWritesEvery 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 }), and outputAvailable({ input, output }) require complete input and parse it through the browser schema.
  • outputError({ input, error }) and cancelled({ input }) accept optional retained input, including incomplete or invalid values. Their native error remains visible; renderer input is undefined when 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:

ResultPurpose
h.ProviderReact provider that mounts Pai.Provider and the selected AI.ThreadProvider.
h.driverTest-side driver for seeding state, queuing responses, commands, files, events, and inspecting requests.
h.clientReal AgentClient facade backed by the mock transport.
h.threadThread handle for the mounted thread. Treat as an escape hatch; prefer interacting through the UI.
h.threadIdId of the thread mounted by the harness.
h[Symbol.asyncDispose]()Close watchers and dispose mock resources.

On this page