Operations and emulated APIs

Use privileged controls to arrange or inspect one world without replacing the provider-shaped application interaction under test.

Every plugin exposes provider-shaped routes for the application and privileged operations for local control. Both reach one service world, but they provide different evidence.

Start with the claim

This checked test arranges Grace through an operation, then proves the application-facing users.list route sees that exact state:

test/owned-runtime.test.ts
import { createTestRuntime } from "localhost2137/testing";
import { expect, it } from "vitest";
import config from "../localhost.config.js";

it("arranges a user with an operation and reads it through Slack-shaped HTTP", async () => {
	const runtime = await createTestRuntime({
		config,
		port: 0,
		storage: "temporary",
	});

	try {
		const instance = await runtime.createInstance({ seed: true });
		try {
			const grace = await instance.slack.createUser({ name: "Grace" });
			const response = await fetch(new URL("users.list", instance.slack.connection.apiUrl), {
				headers: {
					authorization: `Bearer ${instance.slack.connection.botToken}`,
				},
			});

			expect(response.status).toBe(200);
			await expect(response.json()).resolves.toMatchObject({
				members: [
					{ id: "U000000", name: "localhost2137-bot" },
					{ id: grace.id, name: "Grace" },
					{ id: "U_ADA", name: "Ada" },
				],
				ok: true,
			});
		} finally {
			await instance.destroy();
		}
	} finally {
		await runtime.close();
	}
});
pnpm exec vitest run test/owned-runtime.test.ts

The operation is honest setup because the claim is about reading an existing user through the Slack-shaped route. The test proves route selection, bearer authentication, and response shape over operation-created state; its direct test-side fetch does not prove an application adapter. Merely asserting the result of createUser would skip all provider HTTP evidence.

Change the claim and the right boundary can change. A plugin semantic test may intentionally claim that createUser validates and persists one user; there, the operation itself is the subject.

Assign one job to each interaction

Scenario jobUsual boundaryEvidence produced
Arrange users, catalog entries, accounts, or failuresTyped operation or generated CLIThe plugin accepted privileged setup into this world.
Trigger an external actor, event, or time transitionOperation or runtime clockThe local actor or runtime capability ran.
Exercise application integrationApplication SDK, HTTP client, or callback handlerThe application crossed the documented provider-shaped surface.
Inspect emulator stateInspection operation or supported provider readThe selected world reached the observed state.
Observe application behaviorApplication output, database, queue, or public effectThe application produced its claimed result.

Operations weaken evidence only when they replace the application interaction named by the claim. They are valid for setup, triggers, inspection, and direct operation-contract tests.

Triggering a callback is not handling it

The Slack ping-pong example assigns four boundaries deliberately:

Code in the checked exampleRole
instance.slack.createUser, createChannel, addUserToChannelArrange a workspace through control operations.
instance.slack.sendMessage({ text: "ping" })Represent an external member and trigger the plugin's signed event.
Bolt app.message(/^ping$/, ...) and say("pong")Receive provider-shaped HTTP and reply through the application's normal SDK.
instance.slack.listMessages(...)Inspect the shared workspace after tracked delivery settles.

Calling the Bolt handler directly could be a valid handler unit test, but would skip HTTP parsing and signature validation. Writing pong through another operation would skip the outbound SDK request. Callbacks and webhooks contains the complete checked loop.

Keep privileged controls out of the provider API

Provider APIs often cannot create arbitrary surrounding state, force a payment outcome, or move 30 days forward. Adding invented provider routes for those jobs makes application traffic less compatible. Operations expose local-only control explicitly through one plugin-owned Zod schema.

The same operation is available through:

pnpm exec localhost describe slack --instance dev --json
pnpm exec localhost exec slack create-user --instance dev --name Grace --json

Typed tests call instance.slack.createUser({ name: "Grace" }); the CLI generates flags from the same installed schema. Operation success proves that control surface, not provider compatibility.

Let one domain behavior own the state

Provider routes and operations should be adapters over the plugin's same domain behavior and persistence. Separate implementations can accept different rules while appearing to share a database. Provider response mapping belongs at the route boundary; CLI and control error mapping belong at the operation boundary.

That separation lets semantic tests compare the resulting state without pretending the interfaces are interchangeable. The plugin still needs distinct evidence for:

  • provider request encoding, authentication, response and error shapes;
  • operation schemas, privileged outcomes, and useful inspection;
  • state invariants shared by both paths.

Move the boundary when the claim moves

The recurring-billing example creates a product and price through operations, then creates a customer and subscription and lists invoices through the official Stripe SDK. That proves the SDK path the application uses. A lower-level plugin test about renewal transactions can inspect invoices through an operation without weakening its different claim.

State does not decide the evidence; the falsifiable claim does. Continue with Integration testing for ownership, Using plugins for the installed control inventory, or Provider compatibility for what provider-shaped success actually establishes.