Testing WebMCP actions
Test the execution contract before testing whether a model chooses the right tool. Deterministic tests catch the expensive failures—unauthorized work, duplicate effects, and false success—without a browser or model in the loop.
Call the guarded handler directly
ts
import { describe, expect, it, vi } from "vitest";
import { guard } from "signett";
import {
MemoryIdempotencyStore,
MemoryOperationJournal,
} from "signett/testing";
const invocation = () => ({ signal: new AbortController().signal });
it("does not execute for a viewer", async () => {
const cancel = vi.fn(async () => ({ state: "cancelled" as const }));
const execute = guard(cancel, {
context: () => ({ role: "viewer" }),
authorize: ({ context }) => context.role === "owner",
});
await expect(execute({ orderId: "A" }, invocation())).rejects.toMatchObject({
code: "authorization_denied",
});
expect(cancel).not.toHaveBeenCalled();
});Prove duplicate behavior
ts
it("performs one effect for repeated input", async () => {
const store = new MemoryIdempotencyStore();
const journal = new MemoryOperationJournal();
const cancel = vi.fn(async () => ({ state: "cancelled" as const }));
const execute = guard(cancel, {
idempotency: {
key: ({ input }) => input.orderId,
store,
},
journal: { store: journal },
});
await execute({ orderId: "A" }, invocation());
await execute({ orderId: "A" }, invocation());
expect(cancel).toHaveBeenCalledOnce();
});Run the same concurrency contract against a production adapter:
ts
import { checkIdempotencyStore } from "signett/testing";
await checkIdempotencyStore(() => new PostgresIdempotencyStore(pool));The kit uses fresh keys on every run and checks fresh claims, live equal-key waiting, abandoned in-flight state, completion, explicit release, distinct-key parallelism, and pre-aborted calls. For a remote database, pass { concurrencyTimeoutMs: 5_000 } if two independent operations need more than the default second to start. See recipes/postgres-idempotency.ts for a copyable PostgreSQL adapter and its transaction-duration tradeoff.
Test these invariants
For every consequential tool:
- The wrong principal cannot reach the handler.
- Context and policy failures fail closed.
- Identical concurrent keys cause one durable effect.
- Different keys are not serialized globally.
- Verification runs after execution and replay.
- An unverified outcome is never returned as success.
- Cancellation before execution does no work.
- Application errors retain their original identity.
- Telemetry failure cannot change business behavior.
- Sensitive inputs and outputs do not enter lifecycle events.
Then test native integration
After deterministic tests pass, verify the complete path in each supported browser agent:
- the tool is discoverable only in the intended page state;
- the description causes correct tool selection;
- agent arguments satisfy application validation;
- visible page state and returned state agree;
- registration cleanup removes stale tools;
- unsupported browsers retain a usable human interface.
Model-driven tests complement invariant tests. They do not replace them.
For the complete browser path, install @signett/eval and run signett agent. It opens a fresh headless Chrome profile, discovers the page's exact WebMCP inventory, lets a tool-capable model work on a prompt, and records bounded Evidence:
sh
npm install --save-dev @signett/eval
npx signett agent \
--url http://127.0.0.1:3000 \
--prompt "Find the prepared order and report its status." \
--endpoint https://provider.example/v1/chat/completions \
--model tool-capable-model \
--output .artifacts/order-status.jsonAn ad-hoc run checks the interface contract. For consequential work, define a saved suite with reset, snapshot, and authoritative grade hooks. Run deterministic checks on every pull request and repeated real-model Trials on a schedule. The headless-agent codelab shows the complete setup; the CLI reference lists its budgets, expectations, and lifecycle hooks.
Run Signett's complete local suite with:
sh
npm run validate