Follow a checked operation through signed HTTP delivery, an application handler, an SDK reply, tracked completion, and state inspection.
Provider integrations run in both directions. A callback-capable plugin sends provider-shaped HTTP to the application's normal receiver; localhost2137 does not replace the handler with a function call.
The example application is an ordinary Slack Bolt receiver. Its handler replies through Bolt's normal Web API client:
import { App, LogLevel } from "@slack/bolt";
export interface PingPongBotOptions {
readonly apiUrl: string;
readonly botToken: string;
readonly port: number;
readonly signingSecret: string;
}
export interface PingPongBot {
start(): Promise<void>;
stop(): Promise<void>;
}
/** Builds an ordinary Bolt app wired only through localhost2137 connection metadata. */
export function buildPingPongBot(options: PingPongBotOptions): PingPongBot {
const app = new App({
clientOptions: { slackApiUrl: options.apiUrl },
endpoints: "/slack/events",
logLevel: LogLevel.ERROR,
signingSecret: options.signingSecret,
token: options.botToken,
});
app.message(/^ping$/, async ({ say }) => {
await say("pong");
});
let phase: "new" | "running" | "stopped" = "new";
return Object.freeze({
async start() {
if (phase !== "new") throw new Error("Ping-pong bot may be started exactly once.");
await app.start({ host: "127.0.0.1", port: options.port });
phase = "running";
},
async stop() {
if (phase === "stopped") return;
if (phase === "running") await app.stop();
phase = "stopped";
},
});
}The checked test starts that receiver before the trigger, sends ping as an external user, waits for
tracked delivery, then observes the SDK-written pong in the same world:
import { createServer } from "node:net";
import { slack } from "@localhost2137/slack";
import { defineConfig } from "localhost2137";
import { createTestRuntime } from "localhost2137/testing";
import { afterEach, describe, expect, it } from "vitest";
import { buildPingPongBot, type PingPongBot } from "../src/bot.js";
const runtimes: Array<Awaited<ReturnType<typeof createTestRuntime>>> = [];
const bots: PingPongBot[] = [];
afterEach(async () => {
await Promise.all(bots.splice(0).map((bot) => bot.stop()));
await Promise.all(runtimes.splice(0).map((runtime) => runtime.close()));
});
describe("official Slack Bolt ping-pong bot", () => {
it("receives ping and posts pong without Slack credentials or a workspace", async () => {
const botPort = await availablePort();
const config = defineConfig({
services: {
slack: slack({
config: {
botToken: "xoxb-local-ping-pong",
eventsUrl: `http://127.0.0.1:${botPort}/slack/events`,
signingSecret: "local-ping-pong-signing-secret",
workspaceName: "Ping Pong Local",
},
}),
},
});
const runtime = await createTestRuntime({ config, port: 0, storage: "temporary" });
runtimes.push(runtime);
const instance = await runtime.createInstance();
try {
const ada = await instance.slack.createUser({ name: "Ada" });
const channel = await instance.slack.createChannel({ name: "general" });
await instance.slack.addUserToChannel({ channel: channel.id, user: ada.id });
const bot = buildPingPongBot({
apiUrl: instance.slack.connection.apiUrl,
botToken: instance.slack.connection.botToken,
port: botPort,
signingSecret: instance.slack.connection.signingSecret,
});
bots.push(bot);
await bot.start();
await instance.slack.sendMessage({ channel: channel.id, from: ada.id, text: "ping" });
await instance.idle();
const messages = await instance.slack.listMessages({ channel: channel.id });
expect(messages.map(({ text }) => text)).toEqual(["pong", "ping"]);
expect(messages[0]).toMatchObject({ userId: "U000000" });
expect(messages[0]).not.toHaveProperty("eventId");
} finally {
await instance.destroy();
}
});
});
async function availablePort(): Promise<number> {
const server = createServer();
await new Promise<void>((resolve, reject) => {
server.once("error", reject);
server.listen({ host: "127.0.0.1", port: 0 }, resolve);
});
const address = server.address();
if (!address || typeof address === "string") {
server.close();
throw new Error("Bot port reservation did not return an address.");
}
await new Promise<void>((resolve, reject) =>
server.close((cause) => (cause ? reject(cause) : resolve())),
);
return address.port;
}pnpm --filter @localhost2137/example-slack-ping-bot testThe observable sequence is fixed by the code:
bot.start() makes the configured receiver ready before delivery is triggered.sendMessage represents Ada, persists ping, and schedules a signed Events API request.say("pong") through the Web API.instance.idle() waits for tracked plugin delivery to settle.listMessages proves the same workspace contains the user message and SDK reply.Calling the handler directly would skip HTTP parsing and signature validation. Writing pong with
an operation would skip the application's outbound SDK request. Either can serve a narrower test;
neither establishes this complete claim.
There is no universal callback URL. The Slack plugin uses eventsUrl; Stripe uses webhookUrl.
Both default to null, so installing them does not send callbacks. The receiver must be listening
before the action that schedules delivery, as await bot.start() shows above.
Config-based destinations belong to the runtime template, not an instance. A second instance gets different plugin state, clock, logs, routes, and connection values, but the same configured callback URL. Changing that URL requires a daemon restart.
App-facing connection values configure requests the application sends. Plugin mount config chooses where plugin-initiated requests go. The runtime control token belongs to neither path.
The runtime supplies tracked outbound fetch and task ownership, not a universal event protocol. Each plugin defines:
Those details are part of the plugin's compatibility surface. localhost2137 does not promise exactly-once delivery. Slack documents bounded attempts and later virtual-time deadlines; Stripe documents transactional event/outbox creation and stable redelivery identity. Neither policy is a runtime default for other plugins.
idle() establishesawait instance.idle() waits until currently registered plugin work and nested work registered by
it settles after a microtask turn. A tracked failure rejects instead of becoming an arbitrary sleep.
| Question | Answer from idle() |
|---|---|
| Did the currently tracked delivery attempt settle? | Yes. |
| Did tracked nested work settle? | Yes. |
| Did a retry scheduled for later virtual time run? | No; advance according to the plugin's documented policy. |
| Did application work continue after its HTTP response? | Unknown; use the application's completion boundary. |
| Will an external provider/network behave identically? | No. |
An HTTP receiver can acknowledge before its own queue, transaction, or job finishes. First establish
plugin-side settlement with idle(), then wait for the application's public effect when the claim
includes later work. Virtual time and asynchronous work separates current tracked
work from later deadlines.
At-least-once delivery can repeat a stable event after the receiver committed but its response was lost. Test deduplication only when the installed plugin documents that identity and redelivery rule. Inventing duplicate requests for a provider that does not promise them produces unrelated evidence.
Separate instance storage never proves callback routing. When an installed plugin does define one callback destination in mount config, all instances of that mount deliver to it. The runtime does not add an instance header, rewrite the URL, or teach a shared application database how to associate an event with a world.
Parallel callback scenarios therefore need one deliberate design:
Do not pass the runtime control token to the receiver as a routing workaround. Instances and isolation defines the shared-config boundary.
The checked loop establishes the installed plugin's documented signed request, the application's normal HTTP handler, its normal SDK reply, and the observed local effect. It does not establish external DNS, TLS, dashboard configuration, account permissions, or omitted Slack behavior.
Read Slack events and retries or Stripe webhook delivery and recovery before turning a local delivery into a provider claim. Runtime boundaries lists the external properties that remain.