172 lines
8.6 KiB
TypeScript
172 lines
8.6 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 { 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 });
|
||
|
|
}
|
||
|
|
});
|
||
|
|
});
|