279 lines
9.8 KiB
TypeScript
279 lines
9.8 KiB
TypeScript
import { describe, it } from 'node:test';
|
|
import { strict as assert } from 'node:assert';
|
|
import * as fs from 'node:fs';
|
|
import * as os from 'node:os';
|
|
import * as path from 'node:path';
|
|
|
|
import {
|
|
loadActorConstructor,
|
|
resolveActorRunBroker,
|
|
resolvePackageRunSpec,
|
|
validateActorHandlers,
|
|
} from '../../src/commands/actor-run.js';
|
|
|
|
describe('matrix actor run', () => {
|
|
it('loads a named actor export from an entry module', async () => {
|
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-actor-run-entry-'));
|
|
try {
|
|
const entry = path.join(dir, 'actors.mjs');
|
|
fs.writeFileSync(entry, [
|
|
'export class NamedActor {}',
|
|
'export class OtherActor {}',
|
|
'',
|
|
].join('\n'), 'utf8');
|
|
|
|
const actor = await loadActorConstructor(entry, 'NamedActor');
|
|
|
|
assert.equal(actor.name, 'NamedActor');
|
|
} finally {
|
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
it('loads a default actor export from an entry module', async () => {
|
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-actor-run-default-entry-'));
|
|
try {
|
|
const entry = path.join(dir, 'actor.mjs');
|
|
fs.writeFileSync(entry, [
|
|
'export default class DefaultActor {}',
|
|
'',
|
|
].join('\n'), 'utf8');
|
|
|
|
const actor = await loadActorConstructor(entry, 'default');
|
|
|
|
assert.equal(actor.name, 'DefaultActor');
|
|
} finally {
|
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
it('reports available actor exports when the requested export is missing', async () => {
|
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-actor-run-missing-entry-'));
|
|
try {
|
|
const entry = path.join(dir, 'actor.mjs');
|
|
fs.writeFileSync(entry, [
|
|
'export class PresentActor {}',
|
|
'',
|
|
].join('\n'), 'utf8');
|
|
|
|
await assert.rejects(
|
|
() => loadActorConstructor(entry, 'MissingActor'),
|
|
/No actor class found as export "MissingActor".*PresentActor/u,
|
|
);
|
|
} finally {
|
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
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', () => {
|
|
const home = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-actor-run-broker-'));
|
|
try {
|
|
fs.mkdirSync(path.join(home, 'credentials'), { recursive: true });
|
|
fs.writeFileSync(
|
|
path.join(home, 'credentials', 'matrix-broker.json'),
|
|
`${JSON.stringify({
|
|
natsUrl: 'nats://broker.example:4222',
|
|
root: 'acme-dev',
|
|
jwt: 'jwt-home',
|
|
seed: 'seed-home',
|
|
})}\n`,
|
|
'utf8',
|
|
);
|
|
|
|
const resolved = resolveActorRunBroker({ home });
|
|
assert.equal(resolved.natsUrl, 'nats://broker.example:4222');
|
|
assert.equal(resolved.root, 'acme-dev');
|
|
assert.equal(resolved.jwt, 'jwt-home');
|
|
assert.equal(resolved.seed, 'seed-home');
|
|
assert.ok(resolved.checked.includes(`${home}/credentials/matrix-broker.json`));
|
|
assert.equal(fs.existsSync(path.join(home, 'host.json')), false);
|
|
assert.equal(fs.existsSync(path.join(home, 'runtimes')), false);
|
|
assert.equal(fs.existsSync(path.join(home, 'package-records')), false);
|
|
} finally {
|
|
fs.rmSync(home, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
it('prefers explicit broker options over environment and home configuration', () => {
|
|
const home = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-actor-run-broker-precedence-'));
|
|
const previous = {
|
|
MATRIX_NATS_URL: process.env.MATRIX_NATS_URL,
|
|
NATS_URL: process.env.NATS_URL,
|
|
MATRIX_ROOT: process.env.MATRIX_ROOT,
|
|
MATRIX_SPACE: process.env.MATRIX_SPACE,
|
|
MATRIX_NATS_JWT: process.env.MATRIX_NATS_JWT,
|
|
MATRIX_NATS_SEED: process.env.MATRIX_NATS_SEED,
|
|
};
|
|
try {
|
|
fs.mkdirSync(path.join(home, 'credentials'), { recursive: true });
|
|
fs.writeFileSync(
|
|
path.join(home, 'credentials', 'matrix-broker.json'),
|
|
`${JSON.stringify({
|
|
natsUrl: 'nats://home.example:4222',
|
|
root: 'home-root',
|
|
jwt: 'home-jwt',
|
|
seed: 'home-seed',
|
|
})}\n`,
|
|
'utf8',
|
|
);
|
|
process.env.MATRIX_NATS_URL = 'nats://env.example:4222';
|
|
process.env.MATRIX_ROOT = 'env-root';
|
|
process.env.MATRIX_NATS_JWT = 'env-jwt';
|
|
process.env.MATRIX_NATS_SEED = 'env-seed';
|
|
|
|
const resolved = resolveActorRunBroker({
|
|
home,
|
|
natsUrl: 'nats://explicit.example:4222',
|
|
root: 'explicit-root',
|
|
jwt: 'explicit-jwt',
|
|
seed: 'explicit-seed',
|
|
});
|
|
|
|
assert.equal(resolved.natsUrl, 'nats://explicit.example:4222');
|
|
assert.equal(resolved.root, 'explicit-root');
|
|
assert.equal(resolved.jwt, 'explicit-jwt');
|
|
assert.equal(resolved.seed, 'explicit-seed');
|
|
} finally {
|
|
restoreEnv(previous);
|
|
fs.rmSync(home, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
it('uses Space as a root alias for direct broker execution', () => {
|
|
const resolved = resolveActorRunBroker({
|
|
natsUrl: 'nats://127.0.0.1:4222',
|
|
space: 'space-root',
|
|
});
|
|
|
|
assert.equal(resolved.natsUrl, 'nats://127.0.0.1:4222');
|
|
assert.equal(resolved.root, 'space-root');
|
|
assert.equal(resolved.jwt, undefined);
|
|
assert.equal(resolved.seed, undefined);
|
|
});
|
|
|
|
it('reports missing broker URL/root and half credentials before connecting', () => {
|
|
const home = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-actor-run-broker-missing-'));
|
|
const previous = {
|
|
MATRIX_NATS_URL: process.env.MATRIX_NATS_URL,
|
|
NATS_URL: process.env.NATS_URL,
|
|
MATRIX_ROOT: process.env.MATRIX_ROOT,
|
|
MATRIX_SPACE: process.env.MATRIX_SPACE,
|
|
MATRIX_NATS_JWT: process.env.MATRIX_NATS_JWT,
|
|
MATRIX_NATS_SEED: process.env.MATRIX_NATS_SEED,
|
|
NATS_JWT: process.env.NATS_JWT,
|
|
NATS_SEED: process.env.NATS_SEED,
|
|
};
|
|
try {
|
|
delete process.env.MATRIX_NATS_URL;
|
|
delete process.env.NATS_URL;
|
|
delete process.env.MATRIX_ROOT;
|
|
delete process.env.MATRIX_SPACE;
|
|
delete process.env.MATRIX_NATS_JWT;
|
|
delete process.env.MATRIX_NATS_SEED;
|
|
delete process.env.NATS_JWT;
|
|
delete process.env.NATS_SEED;
|
|
|
|
assert.throws(
|
|
() => resolveActorRunBroker({ home }),
|
|
/MATRIX_NATS_URL_REQUIRED/u,
|
|
);
|
|
assert.throws(
|
|
() => resolveActorRunBroker({ home, natsUrl: 'nats://127.0.0.1:4222' }),
|
|
/MATRIX_ROOT_REQUIRED/u,
|
|
);
|
|
assert.throws(
|
|
() => resolveActorRunBroker({ home, natsUrl: 'nats://127.0.0.1:4222', root: 'demo', jwt: 'jwt-only' }),
|
|
/MATRIX_NATS_JWT_SEED_PAIR_REQUIRED/u,
|
|
);
|
|
assert.throws(
|
|
() => resolveActorRunBroker({ home, natsUrl: 'nats://127.0.0.1:4222', root: 'demo', seed: 'seed-only' }),
|
|
/MATRIX_NATS_JWT_SEED_PAIR_REQUIRED/u,
|
|
);
|
|
} finally {
|
|
restoreEnv(previous);
|
|
fs.rmSync(home, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
it('resolves direct package run metadata from matrix.json runtime.factory without Host state', () => {
|
|
const packageDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-package-run-spec-'));
|
|
try {
|
|
fs.mkdirSync(path.join(packageDir, 'src'), { recursive: true });
|
|
fs.writeFileSync(
|
|
path.join(packageDir, 'package.json'),
|
|
`${JSON.stringify({ name: '@example/direct-run', version: '0.1.0', type: 'module' }, null, 2)}\n`,
|
|
'utf8',
|
|
);
|
|
fs.writeFileSync(
|
|
path.join(packageDir, 'matrix.json'),
|
|
`${JSON.stringify({
|
|
name: '@example/direct-run',
|
|
version: '0.1.0',
|
|
runtime: {
|
|
language: 'javascript',
|
|
entry: 'src/index.mjs',
|
|
factory: {
|
|
kind: 'factory',
|
|
export: 'createDirectRunActor',
|
|
instance: {
|
|
id: 'RUNTIME-HOST-DIRECT-RUN',
|
|
class: 'service',
|
|
rootKind: 'component',
|
|
componentType: 'DirectRunActor',
|
|
mount: 'actor-runner-smoke',
|
|
autoStart: true,
|
|
},
|
|
},
|
|
},
|
|
components: [{
|
|
type: 'DirectRunActor',
|
|
mount: 'actor-runner-smoke',
|
|
accepts: ['ping', 'getStatus'],
|
|
}],
|
|
}, null, 2)}\n`,
|
|
'utf8',
|
|
);
|
|
fs.writeFileSync(path.join(packageDir, 'src', 'index.mjs'), 'export function createDirectRunActor() {}\n', 'utf8');
|
|
|
|
const spec = resolvePackageRunSpec(packageDir, { mount: 'actor-runner-smoke' });
|
|
|
|
assert.equal(spec.packageName, '@example/direct-run');
|
|
assert.equal(spec.manifestPath, path.join(packageDir, 'matrix.json'));
|
|
assert.equal(spec.exportName, 'createDirectRunActor');
|
|
assert.equal(spec.mount, 'actor-runner-smoke');
|
|
assert.equal(spec.actorId, 'DirectRunActor');
|
|
assert.equal(spec.packageId, '@example/direct-run');
|
|
assert.equal(spec.accepts.ping && typeof spec.accepts.ping, 'object');
|
|
assert.equal(spec.accepts.getStatus && typeof spec.accepts.getStatus, 'object');
|
|
assert.equal(fs.existsSync(path.join(packageDir, 'host.json')), false);
|
|
assert.equal(fs.existsSync(path.join(packageDir, 'runtimes')), false);
|
|
assert.equal(fs.existsSync(path.join(packageDir, 'package-records')), false);
|
|
} finally {
|
|
fs.rmSync(packageDir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
});
|
|
|
|
function restoreEnv(previous: Record<string, string | undefined>): void {
|
|
for (const [key, value] of Object.entries(previous)) {
|
|
if (value === undefined) {
|
|
delete process.env[key];
|
|
} else {
|
|
process.env[key] = value;
|
|
}
|
|
}
|
|
}
|