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 { exchangeActorCredential, loadActorConstructor, resolveActorRunProvider, resolvePackageRunSpec, } 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('resolves API-key, cloud, and Space from an explicit home without Host state', () => { const home = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-actor-run-provider-')); try { fs.mkdirSync(path.join(home, 'credentials'), { recursive: true }); fs.writeFileSync( path.join(home, 'credentials', 'matrix-api-key.json'), `${JSON.stringify({ apiKey: 'hc_home_key', cloud: 'https://dev.hivecast.ai/', authorityRoot: 'richard-santomauro-nimbletec-com', })}\n`, 'utf8', ); const resolved = resolveActorRunProvider({ home }); assert.equal(resolved.key, 'hc_home_key'); assert.equal(resolved.cloud, 'https://dev.hivecast.ai'); assert.equal(resolved.space, 'richard-santomauro-nimbletec-com'); assert.ok(resolved.checked.includes('/credentials/matrix-api-key.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 provider options over environment and home configuration', () => { const home = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-actor-run-provider-precedence-')); const previous = { MATRIX_API_KEY: process.env.MATRIX_API_KEY, HIVECAST_API_KEY: process.env.HIVECAST_API_KEY, MATRIX_CLOUD: process.env.MATRIX_CLOUD, HIVECAST_CLOUD: process.env.HIVECAST_CLOUD, MATRIX_SPACE: process.env.MATRIX_SPACE, HIVECAST_SPACE: process.env.HIVECAST_SPACE, }; try { fs.mkdirSync(path.join(home, 'credentials'), { recursive: true }); fs.writeFileSync( path.join(home, 'credentials', 'matrix-api-key.json'), `${JSON.stringify({ apiKey: 'hc_home_key', cloud: 'https://home.hivecast.ai', space: 'home-space', })}\n`, 'utf8', ); process.env.MATRIX_API_KEY = 'hc_env_key'; process.env.MATRIX_CLOUD = 'https://env.hivecast.ai'; process.env.MATRIX_SPACE = 'env-space'; const resolved = resolveActorRunProvider({ home, key: 'hc_explicit_key', cloud: 'https://explicit.hivecast.ai/', space: 'explicit-space', }); assert.equal(resolved.key, 'hc_explicit_key'); assert.equal(resolved.cloud, 'https://explicit.hivecast.ai'); assert.equal(resolved.space, 'explicit-space'); } finally { restoreEnv(previous); fs.rmSync(home, { recursive: true, force: true }); } }); it('reports missing key, cloud, and Space before contacting the cloud', () => { const home = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-actor-run-provider-missing-')); const previous = { MATRIX_API_KEY: process.env.MATRIX_API_KEY, HIVECAST_API_KEY: process.env.HIVECAST_API_KEY, MATRIX_CLOUD: process.env.MATRIX_CLOUD, HIVECAST_CLOUD: process.env.HIVECAST_CLOUD, MATRIX_SPACE: process.env.MATRIX_SPACE, HIVECAST_SPACE: process.env.HIVECAST_SPACE, }; try { delete process.env.MATRIX_API_KEY; delete process.env.HIVECAST_API_KEY; delete process.env.MATRIX_CLOUD; delete process.env.HIVECAST_CLOUD; delete process.env.MATRIX_SPACE; delete process.env.HIVECAST_SPACE; assert.throws( () => resolveActorRunProvider({ home }), /MATRIX_API_KEY_REQUIRED/u, ); assert.throws( () => resolveActorRunProvider({ home, key: 'hc_key' }), /MATRIX_CLOUD_REQUIRED/u, ); assert.throws( () => resolveActorRunProvider({ home, key: 'hc_key', cloud: 'https://dev.hivecast.ai' }), /MATRIX_SPACE_REQUIRED/u, ); } finally { restoreEnv(previous); fs.rmSync(home, { recursive: true, force: true }); } }); it('exchanges through /api/install/actor-credential without putting the key in the URL', async () => { const requests: Array<{ readonly url: string; readonly init: Record }> = []; const provider = { key: 'hc_actor_key', cloud: 'https://dev.hivecast.ai', space: 'richard-santomauro-nimbletec-com', checked: [], }; const fetchImpl = async (url: URL, init: RequestInit): Promise => { requests.push({ url: url.toString(), init: init as Record }); return new Response(JSON.stringify({ ok: true, space: { authorityRoot: 'richard-santomauro-nimbletec-com' }, transport: { natsUrl: 'nats://dev.hivecast.ai:4422' }, mount: 'apps.ping', effectiveMount: 'richard-santomauro-nimbletec-com.apps.ping', credentials: { jwt: 'jwt', seed: 'SU' }, source: 'standalone-actor', credentialPipeline: 'api-key-exchange', }), { status: 200, headers: { 'content-type': 'application/json' }, }); }; const exchanged = await exchangeActorCredential({ provider, mount: 'apps.ping', actorId: 'PingActor', packageId: '@acme/ping', runtimeId: 'standalone-actor:ping', accepts: { ping: {} }, emits: {}, subscribes: {}, ttlSeconds: 90, fetchImpl, }); assert.equal(exchanged.ok, true); assert.equal(requests.length, 1); assert.equal(requests[0]?.url, 'https://dev.hivecast.ai/api/install/actor-credential'); assert.equal(JSON.stringify(requests[0]).includes('hc_actor_key'), true); assert.equal(requests[0]?.url.includes('hc_actor_key'), false); assert.equal((requests[0]?.init.headers as Record).authorization, 'Bearer hc_actor_key'); const body = JSON.parse(String(requests[0]?.init.body)) as Record; assert.equal(body.space, 'richard-santomauro-nimbletec-com'); assert.equal(body.mount, 'apps.ping'); assert.equal(body.actorId, 'PingActor'); assert.deepEqual(body.accepts, { ping: {} }); assert.equal(body.ttlSeconds, 90); }); 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): void { for (const [key, value] of Object.entries(previous)) { if (value === undefined) { delete process.env[key]; } else { process.env[key] = value; } } }