Appearance
Actor testing
Headless actor tests run under node:test with tsx. They build a transport (real or stubbed), instantiate a MatrixRuntime (or MockRuntime), mount the actor, and assert on what flowed across the bus. This page covers the four idioms used across the matrix-work-harness tree.
Idiom 1: real MatrixRuntime with InMemoryTransport
The simplest setup. No NATS process, no daemon, no harness. Everything runs in-process.
ts
// tests/unit/my-package/my-actor.spec.ts
import { test } from 'node:test';
import assert from 'node:assert/strict';
import {
MatrixActor,
MatrixRuntime,
InMemoryTransport,
InMemoryBroker,
RequestReply,
} from '@open-matrix/core';
class MyActor extends MatrixActor {
static accepts = {
'my.echo': {
description: 'Echo input back',
schema: { msg: { type: 'string', description: 'Input' } },
returns: { msg: { type: 'string', description: 'Echoed' } },
},
};
async onMyEcho(payload: { msg: string }) {
return { msg: payload.msg };
}
}
test('MyActor echoes', async () => {
const broker = new InMemoryBroker();
const transport = new InMemoryTransport(broker, { name: 'test' });
const runtime = new MatrixRuntime({ transport });
await runtime.create(MyActor, 'my-actor');
const result = await RequestReply.request<{ msg: string }>(
transport, 'my-actor', 'my.echo', { msg: 'hi' }, { timeoutMs: 1000 },
);
assert.equal(result.msg, 'hi');
await runtime.shutdown();
});This is the same code path production uses, just with the in-process broker instead of NATS. It exercises real dispatch, real history bookkeeping, real lifecycle. Use it for anything you can.
Idiom 2: stub transport for tighter control
When you need to inspect the exact envelopes published or simulate transport-level behaviour, build a stub transport. The pattern in tests/unit/core/unit-actor-invoke.spec.ts:17-52 is canonical:
ts
function createInvokeTransport() {
const published: Array<{ topic: string; payload: unknown }> = [];
const invocations: Array<{ rootPath: string; op: string; data: unknown }> = [];
const subs = new Map<string, Function[]>();
return {
published,
invocations,
publish(topic: string, payload: unknown) {
published.push({ topic, payload });
const handlers = subs.get(topic) || [];
for (const h of handlers) h(payload);
},
subscribe(topic: string, handler: Function) {
if (!subs.has(topic)) subs.set(topic, []);
subs.get(topic)!.push(handler);
return () => { /* unsubscribe */ };
},
async invoke(rootPath: string, op: string, data?: unknown) {
invocations.push({ rootPath, op, data });
return { ok: true };
},
};
}After driving the actor, assert against published and invocations:
ts
test('actor emits market.tick on update', async () => {
// ... wire transport, runtime, actor ...
await actor.handleUpdate({ symbol: 'TSLA', price: 245.5 });
const tick = transport.published.find(p => p.topic.endsWith('.$events'));
assert.ok(tick, 'no $events emission seen');
const env = tick!.payload as MxEnvelope;
assert.equal(env.op, 'market.tick');
});Idiom 3: MockRuntime for surgical actor unit tests
MockRuntime (src/core/MockRuntime.ts) is a minimal IMatrixRuntime implementation that does not allocate transport machinery. It is exported from the root barrel:
ts
import { MockRuntime, MatrixActor } from '@open-matrix/core';
const runtime = new MockRuntime();Use MockRuntime when you only care about this one actor's behaviour and want zero overhead from the runtime. It satisfies the IMatrixRuntime interface enough for MatrixActor.initialize() to run, but it does not provide a real transport. You usually pair it with a stub transport.
MockRuntime is also what CheatDetector integrates with for catching actor implementation mistakes during tests (instances calling private methods, escaping the bus, etc.).
Idiom 4: @matrix/test-utils for integration tests
When you need a real NATS server or a real matrixd/Host process, drop down to @matrix/test-utils (projects/matrix-3/packages/test-utils/src/index.ts):
ts
import {
createNatsHarness, createDaemonHarness, isNatsServerAvailable,
} from '@matrix/test-utils';
import { test } from 'node:test';
test('cross-actor invoke over real NATS', async (t) => {
if (!isNatsServerAvailable()) {
t.skip('nats-server binary missing');
return;
}
const nats = await createNatsHarness();
// connect actors against nats.port / nats.wsPort ...
await nats.stop();
});createDaemonHarness() is documented under the Testing — Runtime smoke tests page; treat it as the integration-level fixture.
Asserting on the bus
@matrix/test-utils ships an InMemoryBus that is not the same as InMemoryTransport. It is a lighter pub/sub for tests that want to observe non-Matrix events:
ts
import { InMemoryBus, wrapBus } from '@matrix/test-utils';
const bus = new InMemoryBus();
const off = bus.on('foo', (payload, meta) => { /* ... */ });
bus.emit('foo', { x: 1 });
off();For actor tests, the assertion helpers in @matrix/test-utils/src/assertions.ts give you typed expectations:
ts
import {
assertActorMounted,
assertMessagePublished,
assertMessageNotPublished,
assertHttpResponse,
assertWithinBudget,
} from '@matrix/test-utils';Each helper documents what it checks; their source is short and worth reading once.
Picking the right idiom
| Situation | Use |
|---|---|
| Pure handler logic, no bus traffic to inspect | Idiom 3 (MockRuntime) |
| Single actor + caller, in-process | Idiom 1 (InMemoryTransport) |
| Need to assert exact wire traffic | Idiom 2 (stub transport) |
| Cross-actor or cross-process behaviour | Idiom 4 (test-utils + real NATS or daemon) |
| Browser shell + headless actor + real backend | Playwright + a --serve Host (out of scope here) |
Tip: Production tests in
projects/matrix-3/tests/mostly use Idiom 1. Reach for Idiom 4 only when you actually need to test wire-level behaviour or cross-process effects.
See also
- Unit tests — patterns for testing single ops.
- Contract tests — verifying
accepts/emitsagainst a spec. - Runtime smoke tests — daemon/host-level tests.
- In-memory transport — the transport you build on.
Source:
projects/matrix-3/packages/core/src/core/MockRuntime.ts,projects/matrix-3/tests/unit/core/MatrixActor.spec.ts,projects/matrix-3/tests/unit/core/unit-actor-invoke.spec.ts:17-52(stub transport pattern),projects/matrix-3/packages/test-utils/src/index.ts.