import { afterEach, 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'; describe('FP-30 mx lifecycle commands', () => { const tempDirs: string[] = []; function createLocalPackage(rootDir: string): { packageDir: string; packageName: string } { const packageName = '@acme/weather-actor'; const packageDir = path.join(rootDir, 'weather-actor'); fs.mkdirSync(path.join(packageDir, 'src'), { recursive: true }); fs.writeFileSync( path.join(packageDir, 'matrix.json'), JSON.stringify( { name: packageName, version: '1.0.0', runtime: { language: 'typescript', entry: 'src/index.ts' }, components: [ { type: 'WeatherActor', export: 'WeatherActor', mount: 'weather.actor', autoStart: true, }, ], permissions: { fsPolicy: 'none' }, }, null, 2 ), 'utf8' ); fs.writeFileSync(path.join(packageDir, 'package.json'), JSON.stringify({ name: packageName, version: '1.0.0' }, null, 2)); fs.writeFileSync(path.join(packageDir, 'src', 'index.ts'), 'export class WeatherActor {}\n', 'utf8'); return { packageDir, packageName }; } async function loadMxCli(): Promise<{ createProgram: () => import('commander').Command }> { const mxCliUrl = new URL('../../src/index.ts', import.meta.url).href; return await import(`${mxCliUrl}?test=${Date.now()}-${Math.random().toString(16).slice(2)}`); } async function runMxCli(argv: string[]): Promise { const logs: string[] = []; const originalLog = console.log; const originalExit = process.exit; const { createProgram } = await loadMxCli(); console.log = (...args: unknown[]) => { logs.push(args.map((arg) => String(arg)).join(' ')); }; (process as unknown as { exit: (code?: number) => never }).exit = ((code?: number) => { throw new Error(`process.exit(${code ?? 0})`); }) as never; try { const program = createProgram(); await program.parseAsync(argv); return logs; } finally { console.log = originalLog; (process as unknown as { exit: typeof process.exit }).exit = originalExit; } } function parseLastJson(logs: string[]): unknown { for (let i = logs.length - 1; i >= 0; i--) { const value = logs[i].trim(); if (!value) continue; try { return JSON.parse(value); } catch { // Continue scanning log lines until JSON is found. } } throw new Error(`No JSON payload found in logs:\n${logs.join('\n')}`); } afterEach(() => { while (tempDirs.length > 0) { const dir = tempDirs.pop(); if (dir) { fs.rmSync(dir, { recursive: true, force: true }); } } }); it('installs, lists, inspects, validates, and uninstalls local packages', async () => { const root = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-fp30-lifecycle-')); tempDirs.push(root); const cwd = path.join(root, 'workspace'); const packagesDir = path.join(cwd, '.matrix', 'packages'); fs.mkdirSync(cwd, { recursive: true }); const { packageDir, packageName } = createLocalPackage(root); await runMxCli([ 'node', 'mx', 'install', packageDir, '--packages-dir', packagesDir, '--json', ]); const installedPath = path.join(packagesDir, 'node_modules', '@acme', 'weather-actor'); assert.equal(fs.existsSync(installedPath), true); const listedAfterInstall = parseLastJson(await runMxCli([ 'node', 'mx', 'list', '--packages-dir', packagesDir, '--json', ])) as string[]; assert.equal(Array.isArray(listedAfterInstall), true); assert.equal(listedAfterInstall.includes(packageName), true); const infoPayload = parseLastJson(await runMxCli([ 'node', 'mx', 'info', packageName, '--packages-dir', packagesDir, '--json', ])) as { packageName: string; installPath: string }; assert.equal(infoPayload.packageName, packageName); assert.equal(path.resolve(infoPayload.installPath), path.resolve(installedPath)); const validatePayload = parseLastJson(await runMxCli([ 'node', 'mx', 'validate', path.join(packageDir, 'matrix.json'), '--json', ])) as { packageName: string; valid: boolean }; assert.equal(validatePayload.packageName, packageName); assert.equal(validatePayload.valid, true); await runMxCli([ 'node', 'mx', 'uninstall', packageName, '--packages-dir', packagesDir, '--json', ]); assert.equal(fs.existsSync(installedPath), false); const listedAfterUninstall = parseLastJson(await runMxCli([ 'node', 'mx', 'list', '--packages-dir', packagesDir, '--json', ])) as string[]; assert.equal(Array.isArray(listedAfterUninstall), true); assert.equal(listedAfterUninstall.includes(packageName), false); }); });