fix(cli): validate actor handlers before broker connect
This commit is contained in:
parent
42b286e4f7
commit
f475f156d0
@ -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:
|
||||||
|
|
||||||
|
|||||||
@ -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`:
|
||||||
|
|
||||||
|
|||||||
@ -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": {
|
||||||
|
|||||||
@ -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>;
|
||||||
|
|||||||
@ -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')
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user