/** * Developer Lifecycle — Scaffold & Validate Tests * * Tests the scaffold → validate → install pipeline WITHOUT requiring * the daemon runtime. These tests verify file generation, content * correctness, and manifest validation. * * For full daemon integration (discover → serve), see: * tests/integration/daemon/e2e-developer-lifecycle.spec.ts */ 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'; import { actorCommand } from '../../src/commands/actor.js'; import { webappCommand } from '../../src/commands/webapp.js'; import { validateCommand } from '../../src/commands/validate.js'; import { dependencyInstallNpmrcLines, installCommand } from '../../src/commands/install.js'; describe('developer lifecycle: scaffold + validate + install', () => { const tempDirs: string[] = []; afterEach(() => { while (tempDirs.length > 0) { const dir = tempDirs.pop(); if (dir) { fs.rmSync(dir, { recursive: true, force: true }); } } }); // --------------------------------------------------------------------------- // Actor scaffold // --------------------------------------------------------------------------- describe('actor scaffold', () => { it('routes all Matrix runtime dependency scopes to the configured registry', () => { const lines = dependencyInstallNpmrcLines({ registry: 'http://127.0.0.1:3000/api/packages/open-matrix/npm/', token: 'token-runtime-install', }); assert.ok(lines.includes('@open-matrix:registry=http://127.0.0.1:3000/api/packages/open-matrix/npm/')); assert.ok(lines.includes('@matrix:registry=http://127.0.0.1:3000/api/packages/open-matrix/npm/')); assert.ok(lines.includes('@omega:registry=http://127.0.0.1:3000/api/packages/open-matrix/npm/')); assert.ok(lines.includes('always-auth=true')); assert.ok(lines.includes('//127.0.0.1:3000/api/packages/open-matrix/npm/:_authToken=token-runtime-install')); }); it('generates all expected files with correct content', async () => { const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-lifecycle-actor-')); tempDirs.push(tempRoot); const actorDir = path.join(tempRoot, 'my-counter'); const result = await actorCommand('my-counter', { outputDir: actorDir }); // Return value shape assert.equal(result.rootDir, actorDir); assert.ok(result.className, 'className should be set'); assert.equal(result.mount, 'my-counter'); // All expected files const expectedFiles = [ '.npmrc', 'matrix.json', 'package.json', 'tsconfig.json', path.join('src', `${result.className}.ts`), path.join('src', `create-standalone-${result.className}.ts`), path.join('src', 'index.ts'), path.join('examples', 'get-status.mxq'), path.join('skills', 'my-counter.skill.md'), path.join('tests', 'my-counter.spec.ts'), ]; for (const f of expectedFiles) { assert.ok(fs.existsSync(path.join(actorDir, f)), `Missing: ${f}`); } // matrix.json const manifest = JSON.parse(fs.readFileSync(path.join(actorDir, 'matrix.json'), 'utf8')); assert.equal(manifest.name, '@matrix/my-counter'); assert.ok(Array.isArray(manifest.components)); assert.equal(manifest.components[0]?.mount, 'my-counter'); assert.equal(manifest.components[0]?.type, result.className); assert.equal(manifest.runtime?.factory?.export, `createStandalone${result.className}`); assert.equal(manifest.runtime?.factory?.instance?.componentType, result.className); assert.equal(manifest.runtime?.factory?.instance?.autoStart, true); // package.json const pkg = JSON.parse(fs.readFileSync(path.join(actorDir, 'package.json'), 'utf8')); assert.equal(pkg.type, 'module'); assert.equal(pkg.name, '@matrix/my-counter'); 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.ok(pkg.dependencies?.['@open-matrix/core']); assert.equal(pkg.devDependencies?.['@open-matrix/core'], undefined); const npmrc = fs.readFileSync(path.join(actorDir, '.npmrc'), 'utf8'); assert.ok(npmrc.includes('registry=https://registry.npmjs.org/')); assert.ok(npmrc.includes('@open-matrix:registry=https://registry.hivecast.ai/api/packages/open-matrix/npm/')); assert.ok(npmrc.includes('@matrix:registry=https://registry.hivecast.ai/api/packages/open-matrix/npm/')); assert.ok(npmrc.includes('@omega:registry=https://registry.hivecast.ai/api/packages/open-matrix/npm/')); // tsconfig.json const tsconfig = JSON.parse(fs.readFileSync(path.join(actorDir, 'tsconfig.json'), 'utf8')); assert.equal(tsconfig.compilerOptions.target, 'ES2022'); assert.equal(tsconfig.compilerOptions.module, 'NodeNext'); assert.equal(tsconfig.compilerOptions.declaration, true); assert.equal(tsconfig.compilerOptions.incremental, true); assert.equal(tsconfig.compilerOptions.tsBuildInfoFile, '.tsbuildinfo'); assert.equal(tsconfig.compilerOptions.sourceMap, true); assert.deepEqual(tsconfig.include, ['src']); // Actor source imports @open-matrix/core (not @open-matrix/core) const actorSrc = fs.readFileSync(path.join(actorDir, 'src', `${result.className}.ts`), 'utf8'); assert.ok(actorSrc.includes('@open-matrix/core'), 'should import from @open-matrix/core'); assert.ok(!actorSrc.includes("'@open-matrix/core'"), 'should NOT import from @open-matrix/core'); }); it('validates successfully after scaffolding', async () => { const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-lifecycle-actor-val-')); tempDirs.push(tempRoot); const actorDir = path.join(tempRoot, 'test-actor'); await actorCommand('test-actor', { outputDir: actorDir }); const validation = await validateCommand(actorDir); assert.equal(validation.valid, true, `Diagnostics: ${JSON.stringify(validation.diagnostics)}`); assert.equal(validation.packageName, '@matrix/test-actor'); }); it('installs into a packages directory', async () => { const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-lifecycle-actor-inst-')); tempDirs.push(tempRoot); const actorDir = path.join(tempRoot, 'my-service'); await actorCommand('my-service', { outputDir: actorDir }); const packagesRoot = path.join(tempRoot, '.matrix', 'packages'); fs.mkdirSync(path.join(packagesRoot, 'node_modules'), { recursive: true }); await installCommand(actorDir, { cwd: tempRoot, packagesDir: packagesRoot, force: true, }); // Verify installed const installedManifest = path.join(packagesRoot, 'node_modules', '@matrix', 'my-service', 'matrix.json'); assert.ok(fs.existsSync(installedManifest), 'Installed package should have matrix.json'); const manifest = JSON.parse(fs.readFileSync(installedManifest, 'utf8')); assert.equal(manifest.name, '@matrix/my-service'); }); }); // --------------------------------------------------------------------------- // Webapp scaffold // --------------------------------------------------------------------------- describe('webapp scaffold', () => { it('generates all expected files with correct content', async () => { const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-lifecycle-webapp-')); tempDirs.push(tempRoot); const appDir = path.join(tempRoot, 'my-dashboard'); const result = await webappCommand('my-dashboard', { outputDir: appDir }); // Return value shape assert.equal(result.rootDir, appDir); assert.ok(result.appClassName); assert.ok(result.shellClassName); assert.ok(result.tagName); assert.equal(result.appName, 'my-dashboard'); // All expected webapp files const expectedFiles = [ '.npmrc', 'matrix.json', 'package.json', 'tsconfig.json', 'vite.config.ts', 'index.html', path.join('src', 'index.ts'), path.join('src', 'init.ts'), path.join('src', `${result.appClassName}.ts`), path.join('src', 'shell', `${result.shellClassName}.ts`), path.join('src', 'template', 'app.template.ts'), path.join('src', 'styles', 'app.styles.ts'), path.join('tests', 'my-dashboard.spec.ts'), path.join('examples', 'introspect.mxq'), ]; for (const f of expectedFiles) { assert.ok(fs.existsSync(path.join(appDir, f)), `Missing webapp file: ${f}`); } // matrix.json — webapp section (scoped name: @open-matrix/) const manifest = JSON.parse(fs.readFileSync(path.join(appDir, 'matrix.json'), 'utf8')); assert.equal(manifest.name, '@open-matrix/my-dashboard'); assert.ok(manifest.webapp, 'should have webapp section'); assert.ok(manifest.webapp.distDir); assert.equal(manifest.webapp.appName, 'my-dashboard'); // package.json — webapp deps const pkg = JSON.parse(fs.readFileSync(path.join(appDir, 'package.json'), 'utf8')); assert.equal(pkg.type, 'module'); assert.ok(pkg.scripts?.dev, 'should have vite dev script'); assert.ok(pkg.scripts?.build, 'should have vite build script'); assert.ok(pkg.devDependencies?.vite); assert.ok(pkg.devDependencies?.['@open-matrix/core']); assert.ok(pkg.devDependencies?.['@open-matrix/federation']); const npmrc = fs.readFileSync(path.join(appDir, '.npmrc'), 'utf8'); assert.ok(npmrc.includes('@open-matrix:registry=https://registry.hivecast.ai/api/packages/open-matrix/npm/')); // tsconfig.json — DOM libs, noEmit const tsconfig = JSON.parse(fs.readFileSync(path.join(appDir, 'tsconfig.json'), 'utf8')); assert.ok(tsconfig.compilerOptions.lib?.includes('DOM')); assert.equal(tsconfig.compilerOptions.noEmit, true); assert.equal(tsconfig.compilerOptions.module, 'ESNext'); assert.equal(tsconfig.compilerOptions.moduleResolution, 'Bundler'); // vite.config.ts — base path const viteConfig = fs.readFileSync(path.join(appDir, 'vite.config.ts'), 'utf8'); assert.ok(viteConfig.includes('/apps/my-dashboard/'), 'vite base should point to /apps/my-dashboard/'); // index.html — custom element tag const indexHtml = fs.readFileSync(path.join(appDir, 'index.html'), 'utf8'); assert.ok(indexHtml.includes(result.tagName), `HTML should contain custom element <${result.tagName}>`); // Shell imports federation const shellSrc = fs.readFileSync( path.join(appDir, 'src', 'shell', `${result.shellClassName}.ts`), 'utf8', ); assert.ok(shellSrc.includes('@open-matrix/core'), 'shell should import from @open-matrix/core'); }); it('validates successfully after scaffolding', async () => { const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-lifecycle-webapp-val-')); tempDirs.push(tempRoot); const appDir = path.join(tempRoot, 'test-app'); await webappCommand('test-app', { outputDir: appDir }); const validation = await validateCommand(appDir); assert.equal(validation.valid, true, `Diagnostics: ${JSON.stringify(validation.diagnostics)}`); }); it('installs into a packages directory', async () => { const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-lifecycle-webapp-inst-')); tempDirs.push(tempRoot); const appDir = path.join(tempRoot, 'my-app'); await webappCommand('my-app', { outputDir: appDir }); const packagesRoot = path.join(tempRoot, '.matrix', 'packages'); fs.mkdirSync(path.join(packagesRoot, 'node_modules'), { recursive: true }); await installCommand(appDir, { cwd: tempRoot, packagesDir: packagesRoot, force: true, }); // Scoped package: @open-matrix/my-app → node_modules/@open-matrix/my-app/ const installedManifest = path.join(packagesRoot, 'node_modules', '@open-matrix', 'my-app', 'matrix.json'); assert.ok(fs.existsSync(installedManifest), 'Installed webapp should have matrix.json'); const manifest = JSON.parse(fs.readFileSync(installedManifest, 'utf8')); assert.equal(manifest.name, '@open-matrix/my-app'); assert.ok(manifest.webapp, 'Installed manifest should preserve webapp section'); }); }); // --------------------------------------------------------------------------- // Validation // --------------------------------------------------------------------------- describe('manifest validation', () => { it('rejects manifest with missing required fields', async () => { const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-lifecycle-bad-')); tempDirs.push(tempRoot); const badDir = path.join(tempRoot, 'bad-package'); fs.mkdirSync(badDir, { recursive: true }); fs.writeFileSync( path.join(badDir, 'matrix.json'), JSON.stringify({ name: 'bad-package' }, null, 2), ); const validation = await validateCommand(badDir); assert.ok(validation.diagnostics.length > 0, 'Should have diagnostics for bad manifest'); }); }); });