Appearance
Transport testing
Three distinct things you might want to test:
- An actor on top of a transport — see Actor testing.
- A transport itself (e.g., asserting
semanticToNatsproduces the right wire subject). - Cross-process behaviour over real NATS.
This page covers items 2 and 3.
Stubbing INatsLikeConnection for unit-testing transport logic
Both NatsTransport and createBrowserNatsTransport ultimately receive an INatsLikeConnection. You can implement that interface against a Map-backed broker to test the transport without touching real NATS:
ts
// tests/unit/transport/nats-transport.spec.ts
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { NatsTransport, type INatsLikeConnection, type INatsLikeMsg } from '@open-matrix/core';
function makeFakeConnection() {
const subjects = new Map<string, Set<(err: Error | null, msg: INatsLikeMsg) => void>>();
const published: Array<{ subject: string; data: Uint8Array | undefined }> = [];
const conn: INatsLikeConnection & { published: typeof published } = {
published,
publish(subject, data) {
published.push({ subject, data });
const handlers = subjects.get(subject);
if (handlers) {
const msg: INatsLikeMsg = { subject, data: data ?? new Uint8Array() };
for (const h of handlers) h(null, msg);
}
},
subscribe(subject, opts) {
let set = subjects.get(subject);
if (!set) { set = new Set(); subjects.set(subject, set); }
set.add(opts.callback);
return { unsubscribe() { set!.delete(opts.callback); } };
},
isClosed() { return false; },
async drain() {},
async close() {},
};
return conn;
}
test('NatsTransport prefixes inbox subjects with root', () => {
const conn = makeFakeConnection();
const t = new NatsTransport(conn, 'COM.EXAMPLE');
t.publish('system.llm/$inbox', { hello: 'world' });
assert.ok(conn.published[0].subject === 'COM.EXAMPLE.system.llm.$inbox');
});This is the right level for testing the wire-translation logic (semanticToNats/natsToSemantic) and the JSON encoding path. No external process; runs in milliseconds.
Using getWireTopicInfo for assertion
When asserting actor-level wire behaviour, transport.getWireTopicInfo(topic) gives you the resolved wire subject without having to spy on every publish:
ts
const info = transport.getWireTopicInfo('chat.conversation/$inbox');
assert.equal(info.wireTopic, 'COM.EXAMPLE.chat.conversation.$inbox');
assert.equal(info.root, 'COM.EXAMPLE');Available on both NatsTransport and InMemoryTransport (the in-memory variant returns the topic unchanged).
Real-NATS integration tests with @matrix/test-utils
@matrix/test-utils exports createNatsHarness and startNatsServer for tests that need a real NATS server in a child process. The implementation is at projects/matrix-3/packages/test-utils/src/nats-server.ts and :daemon-harness.ts:296-307.
ts
import { test } from 'node:test';
import { createNatsHarness, isNatsServerAvailable } from '@matrix/test-utils';
import { connect } from '@nats-io/nats-core';
test('two transports talk over real NATS', async (t) => {
if (!isNatsServerAvailable()) {
t.skip('nats-server binary missing on this system');
return;
}
const harness = await createNatsHarness();
try {
const c1 = await connect({ servers: `nats://127.0.0.1:${harness.port}` });
const c2 = await connect({ servers: `nats://127.0.0.1:${harness.port}` });
// build NatsTransports against c1, c2
// run assertions
await c1.drain(); await c2.drain();
} finally {
await harness.stop();
}
});isNatsServerAvailable() returns false when the nats-server binary cannot be found; gracefully skip the test in environments that don't have it (CI, dev machines without NATS installed). The harness allocates ports via getAvailablePorts(2) and supplies both a client port and a WebSocket port.
Daemon harness for cross-process behaviour
Step further up: createDaemonHarness() (daemon-harness.ts:54-272) brings up a full Host process — bundled NATS, gateway, runtime supervisor — into a temp directory, lets you install packages, and tears it down at the end of the test.
ts
import { test } from 'node:test';
import { createDaemonHarness } from '@matrix/test-utils';
test('host serves matrix-web', async () => {
const harness = await createDaemonHarness('host-test', 'demo');
try {
harness.installWorkspacePackage('@open-matrix/matrix-web');
await harness.start({ webapps: ['matrix-web'] });
const ok = await harness.healthcheck(5000);
if (!ok) throw new Error('host never became ready');
} finally {
await harness.teardown();
}
});This crosses the line from "testing the transport" to "testing a deployment" — but the same test-utils package is the entry point for both. See Runtime smoke tests for more.
Writing a transport-level fake
If you need a transport implementation that does something the SDK transports don't (record-and-replay, fuzz injection, ordering-bug reproduction), implement ITransportAdapter directly:
ts
import type { ITransportAdapter, IWireTopicInfo, TransportHandler } from '@open-matrix/core';
class RecordingTransport implements ITransportAdapter {
public readonly recorded: Array<{ kind: 'pub'|'sub'|'unsub'; topic: string; payload?: unknown }> = [];
private subs = new Map<string, Set<TransportHandler>>();
get topicPrefix() { return ''; }
get root() { return ''; }
getUnderlyingClient() { return undefined; }
async disconnect() { /* no-op */ }
getWireTopicInfo(topic: string): IWireTopicInfo {
return { appTopic: topic, wireTopic: topic, prefix: '' };
}
publish(topic: string, payload?: unknown): void {
this.recorded.push({ kind: 'pub', topic, payload });
const handlers = this.subs.get(topic);
if (handlers) for (const h of handlers) h(payload, { topic, ts: Date.now() });
}
subscribe(topic: string, handler: TransportHandler): () => void {
this.recorded.push({ kind: 'sub', topic });
let set = this.subs.get(topic);
if (!set) { set = new Set(); this.subs.set(topic, set); }
set.add(handler);
return () => { this.recorded.push({ kind: 'unsub', topic }); set!.delete(handler); };
}
}Plug the fake into new MatrixRuntime({ transport }) and assert against recorded. The pattern is the same as the test-level stubs in tests/unit/core/unit-actor-invoke.spec.ts:17-52.
See also
- In-memory transport — when the in-memory implementation is enough.
- Actor testing — testing actors over a stub transport.
- Runtime smoke tests — full-Host integration tests.
Source:
projects/matrix-3/packages/test-utils/src/daemon-harness.ts:54-307(daemon + nats harness),projects/matrix-3/packages/test-utils/src/nats-server.ts(startNatsServer),projects/matrix-3/tests/unit/core/unit-actor-invoke.spec.ts:17-52(stub transport pattern).