Skip to content

Transport testing

Three distinct things you might want to test:

  1. An actor on top of a transport — see Actor testing.
  2. A transport itself (e.g., asserting semanticToNats produces the right wire subject).
  3. 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

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).