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 { spawnSync } from 'node:child_process'; import { fileURLToPath } from 'node:url'; import { actorCommand } from '../../src/commands/actor.js'; import { installOpenMatrixCoreStub } from '../support/installCoreStub.ts'; import { loadMatrixServiceManifest } from '../../src/utils/service-manifest.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const repoRoot = path.resolve(__dirname, '../../../../'); describe('mx actor command', () => { it('creates a compilable actor scaffold package', async () => { const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-actor-')); const outputDir = path.join(tmpRoot, 'my-weather-service'); try { const result = await actorCommand('my-weather-service', { cwd: tmpRoot, outputDir, dev: true, }); assert.equal(result.rootDir, outputDir); assert.equal(result.workspacePath, path.join(tmpRoot, '.matrix', 'workspace.json')); assert.equal(fs.existsSync(path.join(outputDir, 'matrix.json')), true); assert.equal(fs.existsSync(path.join(outputDir, 'src', `${result.className}.ts`)), true); assert.equal(fs.existsSync(path.join(outputDir, 'src', 'index.ts')), true); assert.equal(fs.existsSync(path.join(outputDir, 'examples', 'get-status.mxq')), true); assert.equal(fs.existsSync(path.join(outputDir, 'skills', 'my-weather-service.skill.md')), true); assert.equal(fs.existsSync(path.join(outputDir, 'tests', 'my-weather-service.spec.ts')), true); assert.equal(fs.existsSync(path.join(outputDir, 'package.json')), true, 'package.json should be generated'); assert.equal(fs.existsSync(path.join(outputDir, 'tsconfig.json')), true, 'tsconfig.json should be generated'); assert.equal(fs.existsSync(path.join(outputDir, 'matrix.service.json')), false, 'standalone factory is declared in matrix.json'); assert.equal( fs.existsSync(path.join(outputDir, 'src', `create-standalone-${result.className}.ts`)), true, 'standalone service factory should be generated', ); const pkg = JSON.parse(fs.readFileSync(path.join(outputDir, 'package.json'), 'utf8')); assert.equal(pkg.type, 'module', 'package.json should have type: module'); assert.ok(Array.isArray(pkg.files), 'package.json should have files array'); assert.equal(pkg.files.includes('matrix.service.json'), false, 'package files should not ship a second service manifest'); assert.equal(pkg.scripts?.build, 'tsc -p tsconfig.json'); assert.equal(pkg.scripts?.['build:watch'], 'tsc -w -p tsconfig.json --preserveWatchOutput'); assert.equal(pkg.scripts?.dev, 'tsc -w -p tsconfig.json --preserveWatchOutput'); assert.equal(pkg.scripts?.test, 'node --import tsx --test tests/**/*.spec.ts'); assert.equal(pkg.dependencies?.['@open-matrix/core'], '*', 'actor service package should declare runtime SDK dependency'); assert.equal(pkg.devDependencies?.['@open-matrix/core'], undefined, 'actor service package should not hide runtime SDK in devDependencies'); const tsconfig = JSON.parse(fs.readFileSync(path.join(outputDir, 'tsconfig.json'), 'utf8')); assert.ok(tsconfig.compilerOptions, 'tsconfig.json should have compilerOptions'); assert.equal(tsconfig.compilerOptions.target, 'ES2022'); assert.equal(tsconfig.compilerOptions.incremental, true); assert.equal(tsconfig.compilerOptions.tsBuildInfoFile, '.tsbuildinfo'); assert.equal(tsconfig.compilerOptions.sourceMap, true); const manifest = JSON.parse(fs.readFileSync(path.join(outputDir, 'matrix.json'), 'utf8')) as { name: string; runtime?: { factory?: { kind?: string; export?: string; instance?: { rootKind?: string; componentType?: string; autoStart?: boolean; }; }; }; components: Array<{ mount?: string; type?: string }>; }; assert.equal(manifest.name, '@matrix/my-weather-service'); assert.equal(manifest.components[0]?.mount, 'my-weather-service'); assert.equal(manifest.components[0]?.type, result.className); assert.equal(manifest.runtime?.factory?.kind, 'factory'); assert.equal(manifest.runtime?.factory?.export, `createStandalone${result.className}`); assert.equal(manifest.runtime?.factory?.instance?.rootKind, 'component'); assert.equal(manifest.runtime?.factory?.instance?.componentType, result.className); assert.equal(manifest.runtime?.factory?.instance?.autoStart, true); // Install a minimal runtime stub so the scaffolded actor can resolve // the published package import shape without depending on a built workspace copy. installOpenMatrixCoreStub(outputDir); const compile = spawnSync( process.execPath, [ path.resolve(repoRoot, 'node_modules/tsx/dist/cli.mjs'), path.join(outputDir, 'src', `${result.className}.ts`), ], { cwd: outputDir, encoding: 'utf8' } ); assert.equal( compile.status, 0, `expected generated actor to compile, stderr: ${compile.stderr || '(none)'}` ); const build = spawnSync( process.execPath, [ path.resolve(repoRoot, 'node_modules/typescript/bin/tsc'), '-p', path.join(outputDir, 'tsconfig.json'), ], { cwd: outputDir, encoding: 'utf8' }, ); assert.equal(build.status, 0, `expected generated package to build, stderr: ${build.stderr || '(none)'}`); const loadedService = loadMatrixServiceManifest(outputDir); assert.equal(loadedService.manifestPath, path.join(outputDir, 'matrix.json')); assert.equal(loadedService.exportName, `createStandalone${result.className}`); assert.equal(loadedService.entryPath, path.join(outputDir, 'dist', 'index.js')); assert.equal(loadedService.serviceInstance.rootKind, 'component'); assert.equal(loadedService.serviceInstance.mount, 'my-weather-service'); const workspace = JSON.parse(fs.readFileSync(result.workspacePath!, 'utf8')); assert.equal(workspace.home, '.matrix/home'); assert.deepEqual(workspace.default, ['system', 'host-control', 'my-weather-service']); assert.deepEqual(workspace.services?.system, { package: '@open-matrix/system', store: 'system', }); assert.deepEqual(workspace.services?.['host-control'], { package: '@open-matrix/host-control', store: 'system', }); assert.deepEqual(workspace.services?.['my-weather-service'], { path: './my-weather-service', }); } finally { fs.rmSync(tmpRoot, { recursive: true, force: true }); } }); it('adds the source checkout system service before the actor in dev workspaces', async () => { const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-actor-system-dev-')); const systemDir = path.join(tmpRoot, 'projects', 'matrix-3', 'packages', 'system'); const hostControlDir = path.join(tmpRoot, 'projects', 'matrix-3', 'packages', 'host-control'); try { fs.mkdirSync(systemDir, { recursive: true }); fs.writeFileSync(path.join(systemDir, 'package.json'), JSON.stringify({ name: '@open-matrix/system' }), 'utf8'); fs.writeFileSync(path.join(systemDir, 'matrix.json'), JSON.stringify({ name: '@open-matrix/system' }), 'utf8'); fs.mkdirSync(hostControlDir, { recursive: true }); fs.writeFileSync(path.join(hostControlDir, 'package.json'), JSON.stringify({ name: '@open-matrix/host-control' }), 'utf8'); fs.writeFileSync(path.join(hostControlDir, 'matrix.json'), JSON.stringify({ name: '@open-matrix/host-control' }), 'utf8'); const result = await actorCommand('dev-loop-demo', { cwd: tmpRoot, outputDir: path.join(tmpRoot, 'dev-loop-demo'), dev: true, }); const workspace = JSON.parse(fs.readFileSync(result.workspacePath!, 'utf8')); assert.equal(workspace.home, '.matrix/home'); assert.deepEqual(workspace.default, ['system', 'host-control', 'dev-loop-demo']); assert.deepEqual(workspace.services?.system, { path: './projects/matrix-3/packages/system' }); assert.deepEqual(workspace.services?.['host-control'], { path: './projects/matrix-3/packages/host-control' }); assert.deepEqual(workspace.services?.['dev-loop-demo'], { path: './dev-loop-demo' }); } finally { fs.rmSync(tmpRoot, { recursive: true, force: true }); } }); });