Appearance
Contract tests
A contract test verifies that an actor obeys what it advertises. Unit tests check that handlers do the right thing when called; contract tests check that the bus-level shape matches static accepts / emits / $introspect. Both are necessary; they catch different bugs.
Why contract tests exist
LLM agents and other actors discover what an actor does by reading $introspect. If your actor emits 'foo.changed' but only declares 'foo.updated' in static emits, agents pick up the wrong subscription topic and the system silently misbehaves. A contract test catches the drift before it ships.
What a contract test asserts
Five things, in order of value:
$introspectreturns the declared contract. Mount the actor, request$introspect, assert the response matches the static fields.- Every op in
static acceptshas a real handler. No declared op should silently no-op. - Every op the actor
emits in practice is declared instatic emits. Catches typos. - Every field has a
description. Required by the Actor Communication Contract. - Error specs and output schemas (when declared) match real responses. Required for
$introspect { depth: 'full' }consumers.
Asserting $introspect
ts
import { test } from 'node:test';
import assert from 'node:assert/strict';
import {
MatrixRuntime, InMemoryTransport, InMemoryBroker, RequestReply,
} from '@open-matrix/core';
import { Echo } from '../src/echo-actor';
test('Echo $introspect matches static accepts/emits', async () => {
const broker = new InMemoryBroker();
const transport = new InMemoryTransport(broker);
const runtime = new MatrixRuntime({ transport });
await runtime.create(Echo, 'echo');
const intro = await RequestReply.request<{
accepts: Record<string, { description: string }>;
emits: Record<string, { description: string }>;
}>(transport, 'echo', '$introspect', {});
// Every static accept op shows up in $introspect.
for (const op of Object.keys(Echo.accepts)) {
assert.ok(intro.accepts[op], `expected ${op} in accepts`);
assert.ok(intro.accepts[op].description, `${op} missing description`);
}
// No extra ops snuck in.
for (const op of Object.keys(intro.accepts)) {
assert.ok(Echo.accepts[op] || op.startsWith('$'), `unexpected op ${op}`);
}
await runtime.shutdown();
});Asserting "every accept has a handler"
ts
import { toHandlerCase } from '@open-matrix/core';
test('Echo declares no orphan accepts', () => {
const actor = new Echo();
for (const op of Object.keys(Echo.accepts)) {
if (op.startsWith('$')) continue; // system op, base class supplies handler
const handlerName = `on${toHandlerCase(op)}`;
assert.equal(typeof (actor as any)[handlerName], 'function',
`${Echo.name}.accepts['${op}'] declared but ${handlerName} not implemented`);
}
});toHandlerCase is the same function the framework uses for dispatch (projects/matrix-3/packages/core/src/engine/utils/NameUtils.ts). Reusing it ensures the test sees what the framework would see.
Asserting "every emit op is declared"
This is harder to check statically. The pattern is dynamic: drive the actor through a representative scenario, collect every emit, assert each one is in static emits.
ts
test('Echo emits only declared ops', async () => {
const broker = new InMemoryBroker();
const transport = new InMemoryTransport(broker);
const runtime = new MatrixRuntime({ transport });
await runtime.create(Echo, 'echo');
const seen = new Set<string>();
transport.subscribe('echo.$events', (payload) => {
const env = payload as MxEnvelope;
if (env?.op && !env.op.startsWith('$') && !env.op.startsWith('lifecycle.')) {
seen.add(env.op);
}
});
// Drive the actor through every code path that emits.
await RequestReply.request(transport, 'echo', 'echo.say', { msg: 'hi' });
// ... add more drivers to cover all emit branches ...
for (const op of seen) {
assert.ok(Echo.emits[op], `${op} emitted but not declared in static emits`);
}
await runtime.shutdown();
});Filter out system events ($stateChanged, $propertyChanged) and the framework-emitted lifecycle.available/lifecycle.removed — they are emitted by the base class, not your code.
Asserting "every field has a description"
The Actor Communication Contract requires it (root CLAUDE.md § "THE RULES" #3). A simple recursive check:
ts
function assertAllFieldsHaveDescriptions(spec: Record<string, any>, path: string[] = []) {
for (const [key, value] of Object.entries(spec)) {
const here = [...path, key];
if (key === 'description') {
assert.ok(typeof value === 'string' && value.length > 0,
`description at ${here.join('.')} must be a non-empty string`);
} else if (value && typeof value === 'object') {
assertAllFieldsHaveDescriptions(value, here);
}
}
}
test('Echo accepts schemas have descriptions everywhere', () => {
for (const [op, spec] of Object.entries(Echo.accepts)) {
assertAllFieldsHaveDescriptions(spec as any, [op]);
}
});Repeat for emits, streams, state, subscribes, blackboard.
Asserting "ops not in accepts are dropped"
The framework silently drops ops not in accepts ∪ system ops ∪ framework ops (MatrixActor.ts:2916-2917). Assert that the actor doesn't accidentally implement a handler for an undeclared op:
ts
test('Echo does not respond to undeclared ops', async () => {
// ... mount actor ...
let responded = false;
await RequestReply.request(transport, 'echo', 'echo.unknown', {}, { timeoutMs: 200 })
.then(() => { responded = true; })
.catch(() => {});
assert.equal(responded, false, 'echo responded to undeclared op');
});The request should time out (no reply) — that's the desired behaviour.
Pattern: shared contract-test suite
When multiple actors implement the same contract (e.g., several inference providers all implement IInferenceClient), factor the contract assertions into a shared suite that any implementation can plug into:
ts
// tests/contract/inference-client.contract.ts
export function inferenceClientContract<T extends MatrixActor>(
ActorClass: new () => T,
setup: () => Promise<{ runtime: MatrixRuntime; transport: ITransportAdapter }>,
) {
test(`${ActorClass.name} satisfies inference contract — $introspect shape`, async () => { ... });
test(`${ActorClass.name} satisfies inference contract — request shape`, async () => { ... });
}Then each implementation file imports and runs the suite:
ts
import { inferenceClientContract } from '../contract/inference-client.contract';
inferenceClientContract(AnthropicProvider, () => makeRuntime());
inferenceClientContract(OpenAIProvider, () => makeRuntime());This is how the inference-driver packages in projects/omega-drivers/ test their providers against a single contract.
Where contract tests live
For a single-package contract: in the package's tests/. For a contract that spans multiple packages: in projects/matrix-3/tests/ under a folder named for the contract (tests/contract/, tests/inference/).
See also
- Actor ops —
$introspect, op admittance. - Define an actor —
static accepts/emits. - Actor Communication Contract.
Source:
projects/matrix-3/packages/core/src/core/MatrixActor.ts:840-842, 2916-2917($introspect, op admittance),WORKSTREAMS/core-and-packaging/ACTOR-COMMUNICATION-CONTRACT.md(the rule that field descriptions are required).