fix(cli): validate actor handlers before broker connect

This commit is contained in:
Ubuntu 2026-06-08 18:42:59 +00:00
parent 42b286e4f7
commit f475f156d0
6 changed files with 47 additions and 2 deletions

View File

@ -129,6 +129,10 @@ npx matrix broker stop
Expected result: the actor run command prints JSON with `ok: true` and the Expected result: the actor run command prints JSON with `ok: true` and the
`echo` result from `GreeterActor`. `echo` result from `GreeterActor`.
Handler names are part of the Matrix actor contract: `static accepts = { echo:
{} }` maps to `onEcho(payload)`. Do not implement `echo(payload)` directly; the
CLI rejects that shape before it connects to the broker.
If you need the HiveCast browser proof and `pnpm doctor:hivecast` reports If you need the HiveCast browser proof and `pnpm doctor:hivecast` reports
missing Chromium: missing Chromium:

View File

@ -25,6 +25,10 @@ matrix config set registry https://registry.npmjs.org/
matrix registry doctor matrix registry doctor
``` ```
Actor handlers use Matrix handler naming: an accepted op named `echo` is
implemented as `onEcho(payload)`, not `echo(payload)`. `matrix actor run`
validates this before connecting to the broker.
Direct runner credentials are resolved from explicit flags, environment Direct runner credentials are resolved from explicit flags, environment
variables, package-local `.matrix`, and `~/.matrix`: variables, package-local `.matrix`, and `~/.matrix`:

View File

@ -1,6 +1,6 @@
{ {
"name": "@open-matrix/cli", "name": "@open-matrix/cli",
"version": "0.1.4", "version": "0.1.5",
"description": "Matrix package-author CLI.", "description": "Matrix package-author CLI.",
"type": "module", "type": "module",
"bin": { "bin": {

View File

@ -4,6 +4,7 @@ import * as path from 'node:path';
import { pathToFileURL } from 'node:url'; import { pathToFileURL } from 'node:url';
import type { MatrixActor } from '@open-matrix/core/core/MatrixActor.js'; import type { MatrixActor } from '@open-matrix/core/core/MatrixActor.js';
import { toHandlerCase } from '@open-matrix/core/engine/utils/NameUtils';
import { RequestReply } from '@open-matrix/core/engine/remoting/RequestReply.js'; import { RequestReply } from '@open-matrix/core/engine/remoting/RequestReply.js';
import { MatrixRuntime } from '@open-matrix/core/runtime/MatrixRuntime.js'; import { MatrixRuntime } from '@open-matrix/core/runtime/MatrixRuntime.js';
import { NatsTransport } from '@open-matrix/core/transport/NatsTransport.js'; import { NatsTransport } from '@open-matrix/core/transport/NatsTransport.js';
@ -77,6 +78,7 @@ export async function actorRunCommand(
} }
const exportName = options.export?.trim() || 'default'; const exportName = options.export?.trim() || 'default';
const ActorClass = await loadActorConstructor(entryPath, exportName); const ActorClass = await loadActorConstructor(entryPath, exportName);
validateActorHandlers(ActorClass);
const mount = options.mount?.trim() || inferMountFromEntry(entryPath); const mount = options.mount?.trim() || inferMountFromEntry(entryPath);
if (!mount) { if (!mount) {
throw new Error('matrix actor run requires --mount <mount> when it cannot infer a mount from the entry file'); throw new Error('matrix actor run requires --mount <mount> when it cannot infer a mount from the entry file');
@ -358,6 +360,28 @@ export async function loadActorConstructor(entryPath: string, exportName: string
throw new Error(`No actor class found as export "${exportName}". Available exports: ${available.join(', ') || '(none)'}`); throw new Error(`No actor class found as export "${exportName}". Available exports: ${available.join(', ') || '(none)'}`);
} }
export function validateActorHandlers(ActorClass: ActorConstructor): void {
const accepts = ActorClass.accepts;
if (!accepts || typeof accepts !== 'object' || Array.isArray(accepts)) {
return;
}
const prototype = ActorClass.prototype as Record<string, unknown>;
for (const op of Object.keys(accepts)) {
const handlerName = `on${toHandlerCase(op)}`;
if (typeof prototype[handlerName] === 'function') {
continue;
}
const directHandler = prototype[op];
const directHint = typeof directHandler === 'function'
? ` Found ${op}(payload); Matrix actor handlers use ${handlerName}(payload) naming.`
: '';
throw new Error(
`MATRIX_ACTOR_HANDLER_MISSING: ${ActorClass.name} declares static accepts.${op} but does not define ${handlerName}(payload).${directHint}`,
);
}
}
async function loadPackageBootstrap(entryPath: string, exportName: string): Promise<PackageBootstrapFunction> { async function loadPackageBootstrap(entryPath: string, exportName: string): Promise<PackageBootstrapFunction> {
const moduleUrl = `${pathToFileURL(entryPath).href}?matrix_package_run=${Date.now()}-${Math.random().toString(16).slice(2)}`; const moduleUrl = `${pathToFileURL(entryPath).href}?matrix_package_run=${Date.now()}-${Math.random().toString(16).slice(2)}`;
const mod = await import(moduleUrl) as Record<string, unknown>; const mod = await import(moduleUrl) as Record<string, unknown>;

View File

@ -48,7 +48,7 @@ export function createProgram(): Command {
program program
.name('matrix') .name('matrix')
.description('Matrix SDK CLI for package authors') .description('Matrix SDK CLI for package authors')
.version('0.1.4'); .version('0.1.5');
const broker = program const broker = program
.command('broker') .command('broker')

View File

@ -8,6 +8,7 @@ import {
loadActorConstructor, loadActorConstructor,
resolveActorRunBroker, resolveActorRunBroker,
resolvePackageRunSpec, resolvePackageRunSpec,
validateActorHandlers,
} from '../../src/commands/actor-run.js'; } from '../../src/commands/actor-run.js';
describe('matrix actor run', () => { describe('matrix actor run', () => {
@ -64,6 +65,18 @@ describe('matrix actor run', () => {
} }
}); });
it('rejects accepts ops without onXxx handlers before broker connection', async () => {
class BadActor {
static accepts = { echo: { description: 'Echo a message' } };
echo(): void {}
}
assert.throws(
() => validateActorHandlers(BadActor as any),
/MATRIX_ACTOR_HANDLER_MISSING: BadActor declares static accepts\.echo but does not define onEcho\(payload\).*Found echo\(payload\)/u,
);
});
it('resolves broker URL, root, JWT, and seed from an explicit home without Host state', () => { it('resolves broker URL, root, JWT, and seed from an explicit home without Host state', () => {
const home = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-actor-run-broker-')); const home = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-actor-run-broker-'));
try { try {