Validation, defaults, serialization, the typed method tree: everything a send does before the network runs for real under the stub transport, so tests exercise your schema without any HTTP. The compiler is part of the test surface too: wrong payloads, unknown events, and missing fields fail at build time.
The stub transport#
stubTransport is exported from the runtime subpath and plugs into the transport option:
import { createRepostClient } from "@repost/client";
import { stubTransport } from "@repost/client/runtime";
const repost = createRepostClient({
apiKey: "test",
transport: stubTransport,
});
const result = await repost.webhooks.user.created({
customerId: "customer_test",
data: { id: "user_test", email: "[email protected]" },
});
console.log(result.id); // "msg_stub"The returned result echoes the envelope's type, customerId, and timestamp with the fixed id msg_stub. Credentials still resolve before the stub runs, so tests exercise the production configuration path; any non-empty key satisfies it.
stubTransport is imported from @repost/client/runtime, not from the package root.
Asserting on the full request#
Use the native testing helper to assert on the serialized request and idempotency key. It implements the same one-attempt Transport interface as the production transport:
import { ScriptedStubTransport } from "@repost/client/testing";
const transport = new ScriptedStubTransport().enqueueResponse(
202,
JSON.stringify({
id: "msg_test",
type: "user.created",
customerId: "customer_test",
timestamp: "2026-01-01T00:00:00.000Z",
}),
{ "content-type": "application/json" },
);
const repost = createRepostClient({
apiKey: "test",
transport,
generators: { now: () => "2026-01-01T00:00:00.000Z" },
});
await repost.webhooks.user.created({
customerId: "customer_test",
data: { id: "user_test", email: "[email protected]" },
});
expect(JSON.parse(transport.requests[0].bodyText)).toMatchObject({
type: "user.created",
customerId: "customer_test",
});
expect(transport.requests[0].idempotencyKey).toBeTruthy();bodyText, bodyBytes, headers, attempt number, timeout, and idempotency key are captured from the actual runtime attempt.
Deterministic values#
Pin the injectable generators to freeze timestamps and generated ids:
const repost = createRepostClient({
apiKey: "test",
transport: stubTransport,
generators: {
now: () => "2026-01-01T00:00:00.000Z",
uuid: () => "00000000-0000-4000-8000-000000000000",
},
});With now pinned, the envelope timestamp and every @default(now()) field are fixed.
Testing retry behavior#
Queue failures and responses on ScriptedStubTransport, then use deterministicClientOptions() or ManualClock from @repost/client/testing to advance retry delays without sleeping. Set maxAttempts, retryBaseDelayMs, and attemptTimeoutMs on the client; retries never live inside the transport.