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.
pnpm add -D @agentic-ui/testingAll examples below use vitest, but nothing is vitest-specific.
1. Call a capability like a function — callCapability
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:
// 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: false → result.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:
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:
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:
inputSchemais a schema the validator can actually enforce: roottype: 'object'(LLM tool APIs require it), knowntypekeywords, patterns that compile, keyword operands of the right type, no unsatisfiablerequired+additionalProperties: false.descriptionis non-empty and looks like English — it is documentation for the model.riskis 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 anexamplesarray).
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:
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.