Skip to content

Test your capability

@agentic-ui/testing lets you test a capability as a plain function — no LLM, no Vue, no transport. Every call still runs the full registry pipeline (resolve → stale check → validate → permission → confirmation → execute), so what passes here behaves the same in the app.

bash
pnpm add -D @agentic-ui/testing

All examples below use vitest, but nothing is vitest-specific.

1. Call a capability like a function — callCapability

ts
import { describe, expect, it } from 'vitest';
import { callCapability } from '@agentic-ui/testing';
import type { AgentCapability } from '@agentic-ui/core';

const greet: AgentCapability<{ name: string }> = {
  name: 'demo.greet',
  description: 'Greet a person by name.',
  inputSchema: {
    type: 'object',
    properties: { name: { type: 'string', minLength: 1 } },
    required: ['name'],
    additionalProperties: false,
  },
  risk: 'read',
  execute: ({ name }) => ({ message: `Hello, ${name}!` }),
};

it('greets', async () => {
  const result = await callCapability(greet, { name: 'An' });
  expect(result.status).toBe('ok');
  expect(result.structuredContent).toEqual({ message: 'Hello, An!' });
});

it('rejects bad input BEFORE execute runs', async () => {
  const result = await callCapability(greet, { name: '' });
  expect(result.status).toBe('invalid'); // schema said minLength 1
});

callCapability registers the definition into a throwaway registry and returns the ToolResult. It never throws for pipeline outcomes — assert on result.status (ok | invalid | denied | unconfirmed | unavailable | ambiguous | stale | error).

2. Test mutating tools — confirmation stubs

A capability with risk: 'mutating' | 'destructive' normally blocks on a human approval. In tests, stub the answer:

ts
// approve → the tool runs
const ok = await callCapability(saveCap, {}, { confirm: 'approve' });
expect(ok.status).toBe('ok');

// deny → the tool NEVER executes
const denied = await callCapability(saveCap, {}, { confirm: 'deny' });
expect(denied.status).toBe('unconfirmed');

// or script it — `confirm` accepts a full ConfirmationHook
await callCapability(saveCap, {}, {
  confirm: async (req) => (req.digest === expected ? { approved: true } : { approved: false }),
});

There is a matching permission stub for the permission hook (permission: falseresult.status === 'denied', or pass a full PermissionHook).

3. Snapshot the approval digest — capabilityDigest

Approval is bound to sha256(canonicalJson({ name, targetId, input })). To assert your approval UI shows the right digest — or that the pipeline asks with the payload you expect — compute it with the same algorithm and observe the request:

ts
import { callCapability, capabilityDigest } from '@agentic-ui/testing';
import type { ConfirmationRequest } from '@agentic-ui/core';

it('asks approval for exactly this payload', async () => {
  const seen: ConfirmationRequest[] = [];
  await callCapability(saveCap, {}, {
    confirm: 'approve',
    onConfirmation: (req) => seen.push(req),
  });

  expect(seen[0].digest).toBe(await capabilityDigest(saveCap, {}));
});

capabilityDigest(cap, input) uses the capability's own target.id (pass a third argument to override). The digest is stable — canonical JSON sorts keys — so it is safe to store in a snapshot file. computeDigest / canonicalJson / sha256hex are re-exported for lower-level needs.

4. Lint the definition itself — expectCapabilityContract

Catches contract bugs the happy-path tests miss — run it over every capability you ship:

ts
import { expectCapabilityContract } from '@agentic-ui/testing';

it('honours the capability contract', () => {
  expectCapabilityContract(greet, {
    examples: [{ name: 'An' }], // each example must PASS the inputSchema
  });
});

It throws one error listing every violation:

  • inputSchema is a schema the validator can actually enforce: root type: 'object' (LLM tool APIs require it), known type keywords, patterns that compile, keyword operands of the right type, no unsatisfiable required + additionalProperties: false.
  • description is non-empty and looks like English — it is documentation for the model.
  • risk is declared (read | mutating | destructive) — it drives the confirmation policy, never leave it implicit.
  • every example input passes inputSchema (examples can also live on the definition as an examples array).

5. Multi-instance / lifecycle scenarios — createTestRegistry

For anything beyond a single call (two targets, suspend/dispose, stale revisions), build a registry and drive it directly. callCapability accepts { registry } to reuse it:

ts
import { callCapability, createTestRegistry } from '@agentic-ui/testing';

it('routes to the addressed instance', async () => {
  const registry = createTestRegistry({ clock: () => 42 }); // deterministic clock
  registry.register({ ...cap, target: { id: 'page' } });
  registry.register({ ...cap, target: { id: 'drawer' } });

  const result = await callCapability(cap, input, { registry, target: 'drawer' });
  expect(result.status).toBe('ok');
});

Real-world example

The playground tests its five form capabilities (form.describe / read_values / set_values / validate / save) with exactly these helpers — see playground/test/form-capabilities.test.ts.