409 lines
13 KiB
TypeScript
Raw Permalink Normal View History

import { describe, it } from 'node:test';
import { strict as assert } from 'node:assert';
import * as fs from 'node:fs';
import * as net from 'node:net';
import * as os from 'node:os';
import * as path from 'node:path';
import { spawn } from 'node:child_process';
import { resolveNatsBinary } from '../../src/utils/runner-transport.js';
import { assertExitCode, parseLastJson, runMxCli } from '../helpers/cli-harness.js';
interface IWebSocketLike {
addEventListener(
type: 'open' | 'error' | 'close',
listener: (event: unknown) => void,
options?: { once?: boolean },
): void;
close(): void;
}
async function getAvailablePorts(count: number): Promise<number[]> {
const servers = await Promise.all(
Array.from({ length: count }, () => new Promise<net.Server>((resolve, reject) => {
const server = net.createServer();
server.once('error', reject);
server.listen(0, '127.0.0.1', () => resolve(server));
})),
);
const ports = servers.map((server) => {
const address = server.address();
assert.ok(address && typeof address === 'object');
return address.port;
});
await Promise.all(servers.map((server) => new Promise<void>((resolve, reject) => {
server.close((error) => (error ? reject(error) : resolve()));
})));
return ports;
}
async function createRunInPlaceFixture(rootDir: string): Promise<string> {
const packageDir = path.join(rootDir, 'sample-run-package');
const runtimeDir = path.join(packageDir, 'dist', 'runtime');
fs.mkdirSync(runtimeDir, { recursive: true });
fs.mkdirSync(path.join(packageDir, '.matrix'), { recursive: true });
const [natsPort, wsPort] = await getAvailablePorts(2);
fs.writeFileSync(
path.join(packageDir, 'package.json'),
JSON.stringify({
name: '@acme/sample-run-package',
version: '1.0.0',
type: 'module',
main: './dist/runtime/index.js',
}, null, 2),
'utf8',
);
fs.writeFileSync(
path.join(packageDir, 'matrix.json'),
JSON.stringify({
name: '@acme/sample-run-package',
version: '1.0.0',
runtime: {
language: 'typescript',
entry: './dist/runtime/index.js',
factory: {
kind: 'factory',
export: 'createSampleRunService',
bootstrap: {
overrides: {
mode: 'run-in-place',
},
},
instance: {
packageName: '@acme/sample-run-package',
packageRevision: '1.0.0',
class: 'service',
rootKind: 'package-root',
mount: 'sample-run.local',
autoStart: true,
},
},
},
components: [
{
type: 'SampleRunService',
export: 'SampleRunService',
mount: 'sample-run',
autoStart: true,
},
],
permissions: {
fsPolicy: 'none',
},
}, null, 2),
'utf8',
);
fs.writeFileSync(
path.join(packageDir, '.matrix', 'dev.environment.json'),
JSON.stringify({
name: 'dev',
runtime: {
root: 'COM.TEST.RUNNER',
runtimeId: 'sample-run-dev',
},
nats: {
mode: 'embedded',
port: natsPort,
wsPort,
binaryPath: resolveNatsBinary(process.cwd()),
},
}, null, 2),
'utf8',
);
fs.writeFileSync(
path.join(runtimeDir, 'index.js'),
`export async function createSampleRunService(overrides = {}, _services, _configResolution, bootstrapContext) {
return {
ok: true,
overrides,
bootContext: bootstrapContext,
runtime: {
async shutdown() {
return undefined;
}
}
};
}
`,
'utf8',
);
return packageDir;
}
async function createServeFixture(rootDir: string): Promise<{
packageDir: string;
httpPort: number;
}> {
const packageDir = path.join(rootDir, 'sample-serve-package');
const distDir = path.join(packageDir, 'dist');
fs.mkdirSync(path.join(packageDir, '.matrix'), { recursive: true });
fs.mkdirSync(distDir, { recursive: true });
fs.mkdirSync(path.join(packageDir, 'branding'), { recursive: true });
const [httpPort, natsPort, wsPort] = await getAvailablePorts(3);
fs.writeFileSync(
path.join(packageDir, 'package.json'),
JSON.stringify({
name: '@acme/sample-serve-package',
version: '1.0.0',
type: 'module',
}, null, 2),
'utf8',
);
fs.writeFileSync(
path.join(packageDir, 'matrix.json'),
JSON.stringify({
name: '@acme/sample-serve-package',
version: '1.0.0',
webapp: {
distDir: 'dist',
entry: 'index.html',
appName: 'sample-serve',
},
permissions: {
fsPolicy: 'none',
},
}, null, 2),
'utf8',
);
fs.writeFileSync(
path.join(packageDir, '.matrix', 'dev.environment.json'),
JSON.stringify({
name: 'dev',
runtime: {
root: 'COM.TEST.SERVE',
runtimeId: 'sample-serve-dev',
},
nats: {
mode: 'embedded',
port: natsPort,
wsPort,
binaryPath: resolveNatsBinary(process.cwd()),
},
http: {
enabled: true,
port: httpPort,
},
}, null, 2),
'utf8',
);
fs.writeFileSync(
path.join(distDir, 'index.html'),
'<!DOCTYPE html><html><head><title>Sample Serve</title><link rel="manifest" href="/manifest.json"></head><body><div id="app">Sample Serve Fixture</div></body></html>\n',
'utf8',
);
fs.writeFileSync(
path.join(distDir, 'app.js'),
'console.log("sample-serve");\n',
'utf8',
);
fs.writeFileSync(
path.join(packageDir, 'branding', 'manifest.json'),
JSON.stringify({
name: 'Sample Serve',
short_name: 'Sample',
start_url: '/',
display: 'standalone',
icons: [{ src: '/branding/favicon.svg', sizes: '16x16', type: 'image/svg+xml' }],
}, null, 2),
'utf8',
);
fs.writeFileSync(
path.join(packageDir, 'branding', 'favicon.svg'),
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><rect width="16" height="16" fill="#111827"/><path d="M4 11h8v2H4zM5 3h6v2H5zm1 4h4v2H6z" fill="#f8fafc"/></svg>\n',
'utf8',
);
return { packageDir, httpPort };
}
async function waitForHttpReady(
url: string,
child: ReturnType<typeof spawn>,
timeoutMs = 15_000,
): Promise<void> {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
if (child.exitCode !== null) {
throw new Error(`mx run exited early with code ${child.exitCode}`);
}
try {
const response = await fetch(url);
if (response.ok) {
return;
}
} catch {
// Retry until the deadline expires.
}
await new Promise((resolve) => setTimeout(resolve, 100));
}
throw new Error(`Timed out waiting for ${url}`);
}
describe('mx run package front door', () => {
it('runs a package from the current folder via matrix.json runtime.factory and package-local environment', async () => {
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-run-package-'));
const packageDir = await createRunInPlaceFixture(tempRoot);
assert.equal(fs.existsSync(path.join(packageDir, '.matrix', 'host.status.json')), false);
const result = await runMxCli(
['node', 'mx', 'run', '.', '--env', 'dev', '--check', '--json'],
{ cwd: packageDir },
);
assertExitCode(result, 0);
const payload = parseLastJson(result.logs) as Record<string, unknown>;
assert.equal(payload.packageName, '@acme/sample-run-package');
assert.equal(payload.packageDir, packageDir);
assert.equal(payload.environmentName, 'dev');
assert.equal(payload.root, 'COM.TEST.RUNNER');
assert.equal(payload.mount, 'sample-run.local');
assert.equal(payload.entryPath, path.join(packageDir, 'dist', 'runtime', 'index.js'));
assert.equal(payload.runnerTransport?.mode, 'embedded');
assert.equal(payload.runnerTransport?.root, 'COM.TEST.RUNNER');
assert.match(String(payload.runnerTransport?.url), /^nats:\/\/127\.0\.0\.1:\d+$/);
assert.match(String(payload.runnerTransport?.wsUrl), /^ws:\/\/127\.0\.0\.1:\d+$/);
});
it('fails clearly when a directory target does not declare a runtime factory', async () => {
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-run-missing-service-'));
const packageDir = path.join(tempRoot, 'missing-service');
fs.mkdirSync(packageDir, { recursive: true });
const result = await runMxCli(
['node', 'mx', 'run', '.'],
{ cwd: packageDir },
);
assertExitCode(result, 1);
assert.match(
result.errors.join('\n'),
/does not declare a matrix\.json runtime\.factory/,
);
});
it('runs a webapp package as a Matrix asset runtime without a package HTTP server', async () => {
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-run-webapp-assets-'));
const { packageDir } = await createServeFixture(tempRoot);
const result = await runMxCli(
['node', 'mx', 'run', '.', '--env', 'dev', '--check', '--json'],
{ cwd: packageDir },
);
assertExitCode(result, 0);
const payload = parseLastJson(result.logs) as Record<string, unknown>;
assert.equal(payload.packageName, '@acme/sample-serve-package');
assert.equal(payload.mode, 'webapp-assets');
assert.equal(payload.assetMount, 'system.runtimes.sample-serve-dev.http');
assert.equal(payload.baseUrl, undefined);
assert.equal(payload.runnerTransport?.mode, 'embedded');
});
it('publishes a served webapp HTTP origin in run metadata', async () => {
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-run-webapp-origin-'));
const { packageDir, httpPort } = await createServeFixture(tempRoot);
const result = await runMxCli(
['node', 'mx', 'run', '.', '--env', 'dev', '--serve', '--check', '--json'],
{ cwd: packageDir },
);
assertExitCode(result, 0);
const payload = parseLastJson(result.logs) as Record<string, unknown>;
assert.equal(payload.packageName, '@acme/sample-serve-package');
assert.equal(payload.mode, 'standalone-webapp');
assert.equal(payload.assetMount, 'system.runtimes.sample-serve-dev.http');
assert.equal(payload.baseUrl, `http://127.0.0.1:${httpPort}`);
assert.equal(payload.httpPort, httpPort);
assert.equal(payload.runnerTransport?.mode, 'embedded');
});
it('serves built webapp assets with same-origin bootstrap and nats-ws routes', async () => {
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-run-serve-'));
const { packageDir, httpPort } = await createServeFixture(tempRoot);
const mxCliDir = path.resolve(import.meta.dirname, '../..');
const cliEntry = path.resolve(import.meta.dirname, '../../src/index.ts');
const tsxLoader = path.resolve(import.meta.dirname, '../../../../node_modules/tsx/dist/loader.mjs');
const child = spawn(
process.execPath,
['--import', tsxLoader, cliEntry, 'run', packageDir, '--env', 'dev', '--serve'],
{
cwd: mxCliDir,
stdio: ['ignore', 'pipe', 'pipe'],
},
);
const stdout: string[] = [];
const stderr: string[] = [];
child.stdout.on('data', (chunk: Buffer) => {
stdout.push(chunk.toString('utf8'));
});
child.stderr.on('data', (chunk: Buffer) => {
stderr.push(chunk.toString('utf8'));
});
try {
const baseUrl = `http://127.0.0.1:${httpPort}`;
await waitForHttpReady(`${baseUrl}/healthz`, child);
const html = await fetch(`${baseUrl}/`);
assert.equal(html.status, 200);
assert.match(await html.text(), /Sample Serve Fixture/);
const bootstrap = await fetch(`${baseUrl}/api/bootstrap`);
assert.equal(bootstrap.status, 200);
const payload = await bootstrap.json() as {
root?: string;
transportPlan?: { wsPath?: string; authMode?: string; sessionMode?: string };
};
assert.equal(payload.root, 'COM.TEST.SERVE');
assert.equal(payload.transportPlan?.wsPath, '/nats-ws');
assert.equal(payload.transportPlan?.authMode, 'anonymous');
assert.equal(payload.transportPlan?.sessionMode, 'local-client');
const manifest = await fetch(`${baseUrl}/manifest.json`);
assert.equal(manifest.status, 200);
const branding = await fetch(`${baseUrl}/branding/favicon.svg`);
assert.equal(branding.status, 200);
const WebSocketCtor = (globalThis as typeof globalThis & {
WebSocket?: new (url: string) => IWebSocketLike;
}).WebSocket;
assert.ok(WebSocketCtor, 'Node runtime must expose WebSocket for same-origin proxy proof');
const ws = new WebSocketCtor(`ws://127.0.0.1:${httpPort}/nats-ws`);
await new Promise<void>((resolve, reject) => {
ws.addEventListener('open', () => {
ws.close();
}, { once: true });
ws.addEventListener('error', (event) => {
reject(new Error(`WebSocket upgrade failed: ${String(event)}`));
}, { once: true });
ws.addEventListener('close', () => {
resolve();
}, { once: true });
});
} finally {
child.kill('SIGTERM');
await new Promise<void>((resolve) => {
child.once('exit', () => resolve());
});
}
assert.match(stdout.join(''), /serving @acme\/sample-serve-package/);
assert.doesNotMatch(stderr.join(''), /Run target not found|does not declare|Error:/);
});
});