testing/ws_round_trip.ts

In-process test helpers for WebSocket JSON-RPC round-trips.

Drives register_action_ws without an HTTP server. Consumers supply specs + handlers; the harness constructs real WSContext instances backed by test-owned send/close pairs, fakes the authenticated Hono context (request_context, credential type, session id, api token id), and exposes a connect() factory returning a MockWsClient per connection.

Three layers are exported:

- Primitives (create_fake_ws, create_fake_hono_context, create_stub_upgrade, MinimalActionEnvironment, dispatch_ws_message) — used by fuz_app's own dispatcher tests and by consumers wiring tight one-off tests. - Harness (create_ws_test_harness, MockWsClient, keeper_identity) — the high-level driver. Give it specs + handlers, get back {transport, connect()}. connect() is async and resolves after on_socket_open completes, so broadcasts sent immediately after await harness.connect() reach the client. - Round-trip helpersis_notification / is_notification_with / is_response_for predicates, JSON-RPC wire-frame types (JsonrpcNotificationFrame, JsonrpcSuccessResponseFrame, JsonrpcErrorResponseFrame — distinct from the runtime Zod types in http/jsonrpc.ts so tests can narrow params / result), and build_broadcast_api for wiring a typed broadcast API against the harness's transport. Used by consumer round-trip test suites to replace ~100 lines of verbatim-identical glue.

Hono's wire upgrade is skipped — the Node test runtime has no @hono/node-ws adapter — but the full dispatch path is exercised (per-action auth, input validation, ctx.notify back to the originating socket, broadcast via BackendWebsocketTransport, and close-on-revoke).

Declarations
#

21 declarations

view source

build_broadcast_api
#

testing/ws_round_trip.ts view source

<TApi extends object>(options: { harness: WsTestHarness; specs: readonly ({ method: string; initiator: "frontend" | "backend" | "both"; side_effects: boolean; input: ZodType<unknown, unknown, $ZodTypeInternals<unknown, unknown>>; ... 7 more ...; rate_limit?: "both" | ... 2 more ... | undefined; } | { ...; } | { ...; })[]; }): TApi

Wire a typed broadcast API against the harness's transport, matching how a consumer's real backend composes the stack. Returns the typed API so tests can call .tx_run_created(...) / .workspace_changed(...) etc. directly.

const harness = create_ws_test_harness<BaseHandlerContext>({specs, handlers}); const broadcast = build_broadcast_api<MyBackendActionsApi>({ harness, specs: my_broadcast_action_specs, }); const client = await harness.connect(keeper_identity()); await broadcast.tx_run_created({run_id: '...', ...}); await client.wait_for(is_notification('tx_run_created'));

options

type { harness: WsTestHarness; specs: readonly ({ method: string; initiator: "frontend" | "backend" | "both"; side_effects: boolean; input: ZodType<unknown, unknown, $ZodTypeInternals<unknown, unknown>>; ... 7 more ...; rate_limit?: "both" | ... 2 more ... | undefined; } | { ...; } | { ...; })[]; }

returns

TApi

create_fake_hono_context
#

testing/ws_round_trip.ts view source

(opts: FakeHonoContextOptions): Context<any, any, {}>

Build a fake Hono Context exposing the auth keys the dispatcher reads via c.get(...). Only .get() is populated — no other Hono context surface is simulated.

opts

returns

Context<any, any, {}>

create_fake_ws
#

testing/ws_round_trip.ts view source

(): FakeWs

Build a real WSContext backed by in-memory send/close capture. Parsing of outgoing frames is left to the caller — sends holds the raw strings as the dispatcher wrote them.

returns

FakeWs

create_stub_upgrade
#

testing/ws_round_trip.ts view source

(): StubUpgrade

Build a fake upgradeWebSocket that captures the createEvents callback. The returned middleware is inert — tests drive createEvents directly.

returns

StubUpgrade

create_ws_test_harness
#

testing/ws_round_trip.ts view source

<TCtx extends BaseHandlerContext>(options: CreateWsTestHarnessOptions<TCtx>): WsTestHarness

Create a WebSocket test harness for the given specs + handlers.

Registers against a throwaway Hono app with a fake upgradeWebSocket; the captured events factory is invoked per connect() with a synthesized Hono context carrying the requested auth identity. Returned clients drive the real onOpen/onMessage/onClose path against a real WSContext.

options

type CreateWsTestHarnessOptions<TCtx>

returns

WsTestHarness

CreateWsTestHarnessOptions
#

testing/ws_round_trip.ts view source

CreateWsTestHarnessOptions<TCtx>

generics

TCtx

actions

The actions registered on this endpoint — matches the shape register_action_ws accepts. Each entry is a {spec, handler?} tuple; shared fuz_app primitives (like heartbeat_action) can be spread in alongside consumer-specific actions.

type ReadonlyArray<Action<TCtx>>

extend_context

type RegisterActionWsOptions<TCtx>['extend_context']

transport

Pass a pre-created transport to share with a broadcast API.

heartbeat

Threaded through to register_action_ws. Defaults to false in tests — fake timers + receive-silence detection need explicit opt-in and per- test tuning to avoid spurious closes.

type RegisterActionWsOptions<TCtx>['heartbeat']

log

Optional logger. Defaults to a silent [ws-test] logger.

type Logger

on_socket_open

Threaded straight through to register_action_ws.

type RegisterActionWsOptions<TCtx>['on_socket_open']

on_socket_close

Threaded straight through to register_action_ws.

type RegisterActionWsOptions<TCtx>['on_socket_close']

dispatch_ws_message
#

testing/ws_round_trip.ts view source

(on_message: (evt: MessageEvent<WSMessageReceive>, ws: WSContext<unknown>) => void, event: MessageEvent<any>, ws: WSContext<unknown>): Promise<...>

Hono types WSEvents.onMessage as () => void | Promise<void>. Awaits only the Promise branch so tests observe full dispatch (auth, validation, handler, send).

on_message

type (evt: MessageEvent<WSMessageReceive>, ws: WSContext<unknown>) => void

event

type MessageEvent<any>

ws

type WSContext<unknown>

returns

Promise<void>

FakeHonoContextOptions
#

FakeWs
#

testing/ws_round_trip.ts view source

FakeWs

A WSContext paired with capture arrays. Use sends to assert on outgoing frames; use closes to assert on revocation / close.

ws

type WSContext

sends

type Array<string>

closes

type Array<{code?: number; reason?: string}>

is_notification
#

testing/ws_round_trip.ts view source

(method: string): (msg: unknown) => boolean

Predicate matching a JSON-RPC notification with the given method name.

method

type string

returns

(msg: unknown) => boolean

is_notification_with
#

testing/ws_round_trip.ts view source

<P>(method: string, match: (params: P) => boolean): (msg: unknown) => msg is JsonrpcNotificationFrame<P>

Type-guard combinator: match a notification whose typed params satisfies match. Collapses the common test pattern of casting msg to JsonrpcNotificationFrame<P> in every predicate body.

const match_roster_for = (id: Uuid) => is_notification_with<RosterChangedParams>( WORLD_METHODS.roster_changed, (params) => params.character_id === id && !params.removed, ); const roster = await client.wait_for(match_roster_for(char_id));

method

type string

match

type (params: P) => boolean

returns

(msg: unknown) => msg is JsonrpcNotificationFrame<P>

is_response_for
#

testing/ws_round_trip.ts view source

(id: string | number): (msg: unknown) => boolean

Predicate matching a JSON-RPC response frame (success or error) for the given request id.

id

type string | number

returns

(msg: unknown) => boolean

JsonrpcErrorResponseFrame
#

testing/ws_round_trip.ts view source

JsonrpcErrorResponseFrame<D>

generics

D

default unknown

jsonrpc

type typeof JSONRPC_VERSION

id

type number | string

error

type {code: number; message: string; data?: D}

JsonrpcNotificationFrame
#

JsonrpcSuccessResponseFrame
#

keeper_identity
#

MinimalActionEnvironment
#

testing/ws_round_trip.ts view source

Minimal ActionEventEnvironment for tests that instantiate an ActionPeer without pulling in the full runtime. Pre-loads a spec map from the supplied list.

inheritance

executor

type 'frontend' | 'backend'

constructor

type new (specs: readonly ({ method: string; initiator: "frontend" | "backend" | "both"; side_effects: boolean; input: ZodType<unknown, unknown, $ZodTypeInternals<unknown, unknown>>; output: ZodType<unknown, unknown, $ZodTypeInternals<unknown, unknown>>; ... 6 more ...; rate_limit?: "both" | ... 2 more ... | undefined; } | { ...; } | { ...; })[]): MinimalActionEnvironment

specs
type readonly ({ method: string; initiator: "frontend" | "backend" | "both"; side_effects: boolean; input: ZodType<unknown, unknown, $ZodTypeInternals<unknown, unknown>>; output: ZodType<unknown, unknown, $ZodTypeInternals<unknown, unknown>>; ... 6 more ...; rate_limit?: "both" | ... 2 more ... | undefined; } | { ...; } ...

lookup_action_handler

type (): undefined

returns undefined

lookup_action_spec

type (method: string): { method: string; initiator: "frontend" | "backend" | "both"; side_effects: boolean; input: ZodType<unknown, unknown, $ZodTypeInternals<unknown, unknown>>; ... 7 more ...; rate_limit?: "both" | ... 2 more ... | undefined; } | { ...; } | { ...; } | undefined

method
type string
returns { method: string; initiator: "frontend" | "backend" | "both"; side_effects: boolean; input: ZodType<unknown, unknown, $ZodTypeInternals<unknown, unknown>>; output: ZodType<unknown, unknown, $ZodTypeInternals<unknown, unknown>>; ... 6 more ...; rate_limit?: "both" | ... 2 more ... | undefined; } | { ...; } | { ...; }...

MockWsClient
#

testing/ws_round_trip.ts view source

MockWsClient

A mock WS client: send requests, inspect/await incoming messages.

send

Send a JSON-RPC message (request or notification) to the server.

type (message: unknown) => Promise<void>

request

Send a JSON-RPC request and await its response. Resolves with the result; throws with a useful message (code, text, and any data payload) on an error frame — without this, asserting on result.foo for a failed request throws Cannot read property 'foo' of undefined, which hides the real cause. Use send + wait_for(is_response_for(id)) directly when you need to assert on the error frame itself.

type <R = unknown>( id: number | string, method: string, params: unknown, timeout_ms?: number, ) => Promise<R>

close

Close the connection, firing onClose. Returns a promise that resolves once on_socket_close (and the transport's own cleanup) have completed — tests that assert on post-close state should await.

type (code?: number, reason?: string) => Promise<void>

messages

Every message the server has sent, in arrival order.

type ReadonlyArray<unknown>
readonly

wait_for

Wait until a message satisfies predicate. Matches are checked against already-received messages first, then new arrivals until the timeout (defaults to 1000ms).

When predicate is a type guard (e.g. is_notification_with<P>), the result is narrowed automatically and callers don't need to spell <JsonrpcNotificationFrame<P>> on the call site.

type { <T>(predicate: (msg: unknown) => msg is T, timeout_ms?: number): Promise<T>; // eslint-disable-next-line @typescript-eslint/unified-signatures <T = unknown>(predicate: (msg: unknown) => boolean, timeout_ms?: number): Promise<T>; }

StubUpgrade
#

WsConnectIdentity
#

testing/ws_round_trip.ts view source

WsConnectIdentity

Auth identity for a mock connection.

account_id

Account id for the connection. Defaults to a fresh uuid per call.

type Uuid

credential_type

Credential type. Defaults to 'session'. Keeper actions require 'daemon_token'.

session_id

Session id (any string). Defaults to a fresh uuid. Hashed by the dispatcher.

type string

api_token_id

Api token id; set for bearer connections, null otherwise.

type string | null

roles

Roles to grant via active permits. Pass [ROLE_KEEPER] for keeper actions.

type Array<string>

WsTestHarness
#

testing/ws_round_trip.ts view source

WsTestHarness

A harness instance — transport handle + connection factory.

transport

connect

Open a mock connection. Resolves after on_socket_open (and the transport's register_ws) completes, so broadcasts issued immediately after the await reach the connection. Earlier revisions returned synchronously and required a settle_open() microtask drain — no longer necessary.

type (identity?: WsConnectIdentity) => Promise<MockWsClient>

Depends on
#