description: "Endpoint-named mock for Typert Remote traffic: a table of unary answers and stream scripts, live stream control, a log, and the Connection carrier face, for test authors booting a real browser client without a Host."
English | 中文
dsh-remote-mock lets tests configure Host responses through mock.remote.<namespace>.<method> using native Vitest mock methods. The same functions answer direct calls and real Connection traffic; reusable tables supply default responses, and explicitly declared streams support test-driven frames and cancellation. Missing responses fail the call and are reported again by assertNoUnmatched() at teardown. The package runs without a business Host in Node or a browser page, imports no DOM, React, or Node modules, and is consumed from devDependencies only.
Use it when a spec boots real client plugins that talk to ctx.remote and wants to script the Host side by endpoint name: whole-client jsdom specs through __DSH_TRANSPORT__, and unit specs that call dispatch / open directly. Endpoints are the Gateway's wire names (session/page, settings/describe); args is the caller's positional argument list with a trailing AbortSignal removed; a value is whatever the test registers and is answered unchanged. The only declaration is whether an endpoint is unary (unary) or a stream (stream).
mock.remote exposes every namespace and method without a method list or a domain-specific helper. Each accessed endpoint uses a cached native Vitest mock; @vitest/spy.fn is the implementation behind vi.fn and also works in a browser page without the Vitest runner. The same mock answers direct calls and Connection traffic, so return overrides and call assertions observe the functions the client actually invokes:
const mock = RemoteMock.create().load(remoteDefaultResponses)
mock.remote.settings.describe.mockResolvedValue(ok({
writable: true, hasDocument: false, namespaces: [],
}))
mock.remote.settings.mutate.mockResolvedValueOnce(ok(updatedNamespace))
// After the client writes:
expect(mock.remote.settings.mutate).toHaveBeenCalledWith('locale', operations, revision)
Use mockResolvedValue for a persistent response, mockResolvedValueOnce or mockReturnValueOnce for queued responses, and mockImplementation for argument-dependent behavior. Native queued responses run in registration order, then the mock's current implementation answers. Its initial implementation reads the registered default. mockClear() preserves responses and queues; mockReset() clears overrides and restores that initial implementation, which reads the latest default. Missing defaults still fail after queued responses are exhausted, including direct calls, which can throw synchronously.
Only explicit stream() or table stream declarations select stream methods; everything else uses unary mocks. Accessing a method does not fabricate a successful business result. Declare stream mode before retaining a method reference: each endpoint/mode owns its own mock. Namespace and method then probes and symbol reads are inert.
MockedRemote uses Vitest's deep mock transformation over the entire generated TypertRemoteNamespaceMap. A non-empty map preserves its namespace and method names, parameters, results, and native spy types. An empty map makes only this test proxy any, allowing arbitrary namespaces and methods; it does not augment or weaken production Remote declarations. No copied method signatures, optional generated-module suppressions, or compiler-wide flags are needed. Before handing off Remote/mock changes, run pnpm run typecheck to generate and check the real Client types; missing, stale, or partial declarations require rebuilding first. Unbuilt test success or any inference is not strict type evidence.
load(table) installs reusable unary values or handlers, stream scripts, and scriptless streams declarations. unary(endpoint, value) and unary(endpoint, fn) register individual defaults; handlers receive the caller's positional arguments and use existing request types. Each endpoint holds only its latest default, including an explicit undefined; updating defaults does not discard native overrides. ok(value) builds { ok: true, value }; failures use { ok: false, error: { code, message, details } }. Keep stateful handlers, promises, and native queues local to each test:
const initial = { writable: true, hasDocument: false, namespaces: [] }
const mock = RemoteMock.create().load({
unary: { 'settings/describe': ok(initial) },
})
mock.remote.settings.describe.mockResolvedValueOnce(ok({ ...initial, hasDocument: true }))
A stream script is a function of the open args and a StreamHandle (push, end, fail(error), signal); the stream stays open after the script returns until the handle ends or fails it. frames(items) builds a script that yields the items and ends; openStream(initial) one that yields them and stays open. mock.streams controls streams the client currently holds open, filtered by the args they were opened with; opened(endpoint, count) resolves once the endpoint has been opened that many times, and drained(endpoint) once the consumer of every matching stream has pulled everything pushed so far, an open stream's consumer waiting for more and a settled stream's queue empty (pulled from the queue; that equals processed only for a consumer that handles items inside its read loop):
mock.stream('session/follow', openStream([snapshotFrame]))
await mock.streams.opened('session/follow', 1)
mock.streams.push('session/follow', eventFrame, ([request]) => (request as { sessionId: string }).sessionId === SID)
mock.streams.fail('session/follow', new Error('gone'))
await mock.streams.drained('session/follow')
A failed stream rejects the consumer's next read with the given Error. Consumer cancellation (the opening signal or an early iterator return()) aborts StreamHandle.signal, ends the iteration without throwing, and logs the stream as cancelled.
mock.rpc is the ClientConnectionRpc face: install it as globalThis.__DSH_TRANSPORT__ = { rpc: mock.rpc } and the production connection plugin uses it in place of the HTTP caller, so every Remote call reaches dispatch and every stream open with no envelopes in between. Payloads carry { args } as the whole-client proxies send them (an array) or as the Gateway's own endpoints send them (one object, delivered as one positional arg); a call whose signal aborts rejects with the abort reason. RemoteMock.create() registers one stream, $events, that answers the Gateway client's opening with { type: 'ready', clientId, host: { home } } (host from RemoteMockOptions.host, default /home/mock) and stays open, which is what lets the assembled client reach connected; a spec overrides or fails it like any other stream.
mock.log.calls(endpoint?) lists unary calls through dispatch or rpc.call (args, seq, a live state of pending / answered / failed, and the answer or thrown error as result), streams(endpoint?) lists scripted opens with their live state and pushed count, requests(endpoint?) lists the first positional arg of calls and opens in order (without an endpoint, excluding the Gateway's own $-prefixed endpoints), and unmatched() lists requests that found no rule. Native .mock.calls additionally includes direct proxy calls; carrier stream mocks receive their final cancellation signal. assertNoUnmatched() reports misses at teardown. modeOf(endpoint) reports explicit registrations; endpoints() also includes accessed proxy methods so assembly can provide their namespaces.
dispatch rejects and open throws remote-mock: no rule for <endpoint>; registered: …, and the log records the miss; register the endpoint.{ args: unknown[] | object } — rpc.call rejects and rpc.open throws a TypeError; the whole-client proxies send the array form and the Gateway's own endpoints the object form, so a hand-written call is at fault.None, as this package is browser-side test infrastructure; nothing here reaches a model request.
None; this package neither assembles nor sends a provider request.
rpc serves a client in the same realm through __DSH_TRANSPORT__.rpc; no HTTP or WebSocket carrier for browser-lane specs is provided.{ ok, value } or { ok: false, error }); the mock passes it through unchanged and does not check those fields.requests, opened, drained, and push / end / fail; the caller also owns cancellation. Native call assertions still work. Use a registered stream script when a scenario needs those controls.