hivecast-sdk/scripts/prove-sdk-cli-uat.mjs

536 lines
19 KiB
JavaScript
Raw Normal View History

#!/usr/bin/env node
import { spawnSync, spawn } from 'node:child_process';
import { createServer } from 'node:http';
import { cp, mkdir, mkdtemp, readdir, readFile, rm, writeFile } from 'node:fs/promises';
import { existsSync } from 'node:fs';
import { tmpdir } from 'node:os';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
const sourceWorkspaceDir = path.resolve(scriptDir, '..');
let workspaceDir = sourceWorkspaceDir;
const packages = [
'@open-matrix/contracts',
'@open-matrix/federation',
'@open-matrix/omega-core',
'@open-matrix/core',
'@open-matrix/browser-host',
'@open-matrix/browser-kit',
'@open-matrix/hivecast-sdk',
'@open-matrix/cli',
];
function run(command, args, options = {}) {
const result = spawnSync(command, args, {
cwd: options.cwd ?? workspaceDir,
stdio: options.stdio ?? 'inherit',
env: { ...process.env, ...options.env },
encoding: 'utf8',
});
if (result.status !== 0) {
const details = options.stdio === 'pipe'
? `\nstdout:\n${result.stdout ?? ''}\nstderr:\n${result.stderr ?? ''}`
: '';
throw new Error(`${command} ${args.join(' ')} failed with exit ${result.status}${details}`);
}
return result;
}
function parseJsonFromOutput(output, label) {
const trimmed = output.trim();
if (!trimmed) throw new Error(`${label} produced no output`);
const start = trimmed.lastIndexOf('\n{');
const jsonText = start >= 0 ? trimmed.slice(start + 1) : trimmed;
try {
return JSON.parse(jsonText);
} catch (error) {
throw new Error(`${label} did not end with JSON: ${error.message}\n${output}`);
}
}
function runRuntimeHost(runtimeHostBin, args, options = {}) {
const command = runtimeHostBin.endsWith('.js') || runtimeHostBin.endsWith('.mjs')
? process.execPath
: runtimeHostBin;
const prefix = command === process.execPath ? [runtimeHostBin] : [];
return run(command, [...prefix, '--workspace-source', ...args], options);
}
async function findTarball(tarballDir, packageName) {
const packageDir = path.join(workspaceDir, 'packages', packageName.split('/').at(-1));
const packageJson = JSON.parse(await readFile(path.join(packageDir, 'package.json'), 'utf8'));
const expectedPrefix = packageJson.name.replace('@', '').replace('/', '-');
const entries = await readdir(tarballDir);
const tarball = entries.find((entry) => entry.startsWith(expectedPrefix) && entry.endsWith('.tgz'));
if (!tarball) throw new Error(`Packed tarball missing for ${packageName}`);
return path.join(tarballDir, tarball);
}
async function getFreePort() {
return new Promise((resolvePort, reject) => {
const server = createServer();
server.once('error', reject);
server.listen(0, '127.0.0.1', () => {
const address = server.address();
if (!address || typeof address === 'string') {
server.close(() => reject(new Error('Unable to allocate a local port')));
return;
}
const { port } = address;
server.close(() => resolvePort(port));
});
});
}
function envString(...names) {
for (const name of names) {
const value = process.env[name];
if (typeof value === 'string' && value.trim()) return value.trim();
}
return undefined;
}
function resolveLiveKeyConfig() {
const key = envString('MATRIX_API_KEY', 'HIVECAST_TEST_KEY', 'HIVECAST_API_KEY');
const cloud = envString('MATRIX_CLOUD', 'HIVECAST_TEST_CLOUD', 'HIVECAST_CLOUD');
const space = envString('MATRIX_SPACE', 'HIVECAST_TEST_SPACE', 'HIVECAST_SPACE');
const provided = [key, cloud, space].filter(Boolean).length;
if (provided > 0 && provided < 3) {
throw new Error('Live SDK CLI proof requires MATRIX_API_KEY/MATRIX_CLOUD/MATRIX_SPACE together');
}
if (!key || !cloud || !space) return null;
return { key, cloud: cloud.replace(/\/+$/g, ''), space };
}
async function validateLiveKeyConfig(config) {
const response = await fetch(`${config.cloud}/api/install/key-info`, {
method: 'POST',
headers: {
authorization: `Bearer ${config.key}`,
'content-type': 'application/json',
},
body: JSON.stringify({ space: config.space }),
});
const text = await response.text();
if (!response.ok) {
throw new Error(`Live SDK CLI key validation failed (${response.status}): ${text}`);
}
const body = text.trim() ? JSON.parse(text) : {};
if (body?.ok !== true) {
throw new Error(`Live SDK CLI key validation did not return ok:true: ${text}`);
}
return {
ok: true,
cloud: config.cloud,
space: config.space,
keyId: body.key?.id ?? body.id ?? null,
fingerprint: body.key?.fingerprint ?? body.fingerprint ?? null,
};
}
async function findOnlyTarball(dir, label) {
const entries = (await readdir(dir)).filter((entry) => entry.endsWith('.tgz'));
if (entries.length !== 1) {
throw new Error(`${label} expected exactly one packed tarball, found ${entries.length}: ${entries.join(', ')}`);
}
return path.join(dir, entries[0]);
}
function shouldCopySdkPath(relativePath) {
if (!relativePath) return true;
const parts = relativePath.split(path.sep);
if (parts.includes('node_modules')) return false;
if (parts.includes('dist')) return false;
if (parts.includes('.turbo')) return false;
const base = parts.at(-1) ?? '';
if (base === '.tsbuildinfo') return false;
if (base.endsWith('.tgz')) return false;
return true;
}
async function createProofWorkspace(proofRoot) {
if (process.env.MATRIX_SDK_CLI_UAT_IN_PLACE === '1') {
return {
workspaceDir: sourceWorkspaceDir,
copied: false,
};
}
const copiedWorkspace = path.join(proofRoot, 'hivecast-sdk-workspace');
await cp(sourceWorkspaceDir, copiedWorkspace, {
recursive: true,
dereference: false,
filter: (src) => shouldCopySdkPath(path.relative(sourceWorkspaceDir, src)),
});
return {
workspaceDir: copiedWorkspace,
copied: true,
};
}
function waitForRunReady(child) {
return new Promise((resolveReady, reject) => {
let stdout = '';
let stderr = '';
let settled = false;
const timer = setTimeout(() => {
if (settled) return;
settled = true;
reject(new Error(`matrix run did not become ready\nstdout:\n${stdout}\nstderr:\n${stderr}`));
}, 10_000);
child.stdout.setEncoding('utf8');
child.stderr.setEncoding('utf8');
child.stdout.on('data', (chunk) => {
stdout += chunk;
if (!stdout.includes('"ok": true') || !stdout.includes('"command": "run"')) return;
if (!settled) {
settled = true;
clearTimeout(timer);
resolveReady({ ok: true, command: 'run' });
}
});
child.stderr.on('data', (chunk) => {
stderr += chunk;
});
child.once('exit', (code) => {
if (settled) return;
settled = true;
clearTimeout(timer);
reject(new Error(`matrix run exited before ready (${code})\nstdout:\n${stdout}\nstderr:\n${stderr}`));
});
});
}
async function stopChild(child) {
if (child.exitCode !== null || child.signalCode !== null) return;
child.kill('SIGTERM');
await new Promise((resolveStop) => {
const timer = setTimeout(() => {
child.kill('SIGKILL');
resolveStop();
}, 3000);
child.once('exit', () => {
clearTimeout(timer);
resolveStop();
});
});
}
async function main() {
const proofRoot = await mkdtemp(path.join(tmpdir(), 'hivecast-sdk-cli-uat.'));
const proofWorkspace = await createProofWorkspace(proofRoot);
workspaceDir = proofWorkspace.workspaceDir;
const tarballDir = path.join(proofRoot, 'tarballs');
const packageTarballDir = path.join(proofRoot, 'package-tarballs');
const toolingDir = path.join(proofRoot, 'tooling');
const authorDir = path.join(proofRoot, 'author-package');
const packageConsumerDir = path.join(proofRoot, 'package-manager-consumer');
const port = await getFreePort();
const packagePort = await getFreePort();
const liveKeyConfig = resolveLiveKeyConfig();
let liveKeyValidation = null;
let keep = process.env.MATRIX_SDK_KEEP_CLI_UAT === '1';
let runner;
let packageRunner;
2026-06-07 23:11:26 +00:00
const runtimeHostBin = envString('MATRIX_SDK_RUNTIME_HOST_BIN');
const managedHostProofEnabled = Boolean(runtimeHostBin);
let runtimeHostStarted = false;
2026-06-07 23:11:26 +00:00
let managedHostProof = null;
try {
await mkdir(tarballDir, { recursive: true });
await mkdir(packageTarballDir, { recursive: true });
await mkdir(toolingDir, { recursive: true });
await mkdir(authorDir, { recursive: true });
await mkdir(packageConsumerDir, { recursive: true });
if (proofWorkspace.copied) {
run('pnpm', ['install', '--ignore-scripts']);
}
run('pnpm', ['run', 'clean']);
run('pnpm', ['run', 'build']);
for (const packageName of packages) {
run('pnpm', ['--filter', packageName, 'pack', '--pack-destination', tarballDir]);
}
const tarballs = Object.fromEntries(
await Promise.all(packages.map(async (packageName) => [packageName, await findTarball(tarballDir, packageName)]))
);
const tarballDependencies = Object.fromEntries(
Object.entries(tarballs).map(([packageName, tarballPath]) => [packageName, `file:${tarballPath}`])
);
const authorOverrides = Object.fromEntries(
[
'@open-matrix/contracts',
'@open-matrix/federation',
'@open-matrix/omega-core',
'@open-matrix/core',
].map((packageName) => [packageName, tarballDependencies[packageName]])
);
await writeFile(path.join(toolingDir, 'package.json'), JSON.stringify({
name: 'hivecast-sdk-cli-uat-tooling',
version: '0.0.0',
private: true,
type: 'module',
dependencies: {
'@open-matrix/cli': tarballDependencies['@open-matrix/cli'],
},
pnpm: {
overrides: tarballDependencies,
},
}, null, 2));
run('pnpm', ['install', '--ignore-scripts'], { cwd: toolingDir });
run('pnpm', ['exec', 'matrix', 'init', authorDir, '--name', 'fresh-sdk-author'], { cwd: toolingDir });
const authorPackagePath = path.join(authorDir, 'package.json');
const authorPackage = JSON.parse(await readFile(authorPackagePath, 'utf8'));
authorPackage.dependencies['@open-matrix/core'] = tarballDependencies['@open-matrix/core'];
authorPackage.pnpm = { overrides: authorOverrides };
await writeFile(authorPackagePath, `${JSON.stringify(authorPackage, null, 2)}\n`);
run('pnpm', ['exec', 'matrix', 'add', 'actor', 'greeter', '--dir', authorDir, '--mount', 'demo.greeter'], { cwd: toolingDir });
if (liveKeyConfig) {
liveKeyValidation = await validateLiveKeyConfig(liveKeyConfig);
}
2026-06-07 23:11:26 +00:00
const configureKey = liveKeyConfig?.key ?? 'matrix_sdk_test_key_redacted';
const configureCloud = liveKeyConfig?.cloud ?? 'https://example.invalid';
const configureSpace = liveKeyConfig?.space ?? 'fresh-sdk-space';
2026-06-07 23:11:26 +00:00
const configureArgs = [
'exec', 'matrix', 'configure',
'--dir', authorDir,
'--key', configureKey,
'--cloud', configureCloud,
'--space', configureSpace,
2026-06-07 23:11:26 +00:00
];
if (runtimeHostBin) {
configureArgs.push(
'--runtime-host-bin', runtimeHostBin,
'--runtime-host-routing', 'workspace-source',
);
}
run('pnpm', configureArgs, { cwd: toolingDir });
run('pnpm', ['install', '--ignore-scripts'], { cwd: authorDir });
run('pnpm', ['run', 'build'], { cwd: authorDir });
runner = spawn('pnpm', [
'exec', 'matrix', 'run',
'--dir', authorDir,
'--port', String(port),
], {
cwd: toolingDir,
stdio: ['ignore', 'pipe', 'pipe'],
env: process.env,
});
await waitForRunReady(runner);
const invoke = run('pnpm', [
'exec', 'matrix', 'invoke',
'demo.greeter',
'echo',
'{"message":"hello-from-sdk-cli"}',
'--dir', authorDir,
'--port', String(port),
], { cwd: toolingDir, stdio: 'pipe' });
const invoked = JSON.parse(invoke.stdout.trim());
if (invoked.ok !== true || invoked.result?.message !== 'hello-from-sdk-cli') {
throw new Error(`Unexpected matrix invoke output: ${invoke.stdout}`);
}
const [freshPackageJson, tsconfig, actorSource] = await Promise.all([
readFile(path.join(authorDir, 'package.json'), 'utf8'),
readFile(path.join(authorDir, 'tsconfig.json'), 'utf8'),
readFile(path.join(authorDir, 'src', 'GreeterActor.ts'), 'utf8'),
]);
const combinedAuthorFiles = `${freshPackageJson}\n${tsconfig}\n${actorSource}`;
const forbidden = [
'@matrix/mx-cli',
'@open-matrix/host-service',
'@open-matrix/host-control',
'@open-matrix/system-auth',
'@open-matrix/system-gateway-http',
'@open-matrix/system-',
'@open-matrix/cli',
'hivecast',
'projects/matrix-3',
'matrix-work-harness',
];
const foundForbidden = forbidden.find((needle) => combinedAuthorFiles.includes(needle));
if (foundForbidden) throw new Error(`Fresh author package contains forbidden dependency/path: ${foundForbidden}`);
if (!actorSource.includes("from '@open-matrix/core'")) {
throw new Error('Fresh actor source does not import @open-matrix/core');
}
if (!existsSync(path.join(authorDir, '.matrix', 'run.json'))) {
throw new Error('matrix run did not write package-local .matrix/run.json');
}
await stopChild(runner);
runner = undefined;
2026-06-07 23:11:26 +00:00
if (managedHostProofEnabled) {
if (!runtimeHostBin || !existsSync(runtimeHostBin)) {
throw new Error(`Managed host matrix command not found for optional managed proof: ${runtimeHostBin}`);
}
const up = run('pnpm', [
'exec', 'matrix', 'up',
'--dir', authorDir,
'--http-port', 'auto',
'--host-nats-port', 'auto',
'--host-nats-ws-port', 'auto',
'--runtime-id', 'SDK-UAT-GREETER',
'--json',
'--timeout', '45000',
], { cwd: toolingDir, stdio: 'pipe' });
runtimeHostStarted = true;
const upResult = parseJsonFromOutput(up.stdout, 'matrix up');
if (upResult.ok !== true || upResult.mode !== 'natural-launch-up') {
throw new Error(`Unexpected matrix up output: ${up.stdout}`);
}
const upService = upResult.services?.find?.((service) => service.runtimeId === 'SDK-UAT-GREETER');
if (!upService || upService.ok !== true) {
throw new Error(`matrix up did not start SDK-UAT-GREETER: ${up.stdout}`);
}
const managedInvokeOutput = runRuntimeHost(runtimeHostBin, [
'invoke',
'demo.greeter',
'getStatus',
'{}',
'--json',
'--timeout', '10000',
], { cwd: authorDir, stdio: 'pipe' });
const managedInvoke = parseJsonFromOutput(managedInvokeOutput.stdout, 'managed matrix invoke');
if (managedInvoke.ok !== true || managedInvoke.result?.mount !== 'demo.greeter') {
throw new Error(`Unexpected managed matrix invoke output: ${managedInvokeOutput.stdout}`);
}
managedHostProof = {
runtimeHostBin,
up: {
ok: upResult.ok,
mode: upResult.mode,
runtimeId: upService.runtimeId,
packageDir: upService.packageDir,
},
invoke: {
ok: managedInvoke.ok,
target: managedInvoke.target,
result: managedInvoke.result,
},
};
}
run('pnpm', ['pack', '--pack-destination', packageTarballDir], { cwd: authorDir, stdio: 'pipe' });
const authorPackageTarball = await findOnlyTarball(packageTarballDir, 'generated author package');
await writeFile(path.join(packageConsumerDir, 'package.json'), JSON.stringify({
name: 'hivecast-sdk-package-manager-consumer',
version: '0.0.0',
private: true,
type: 'module',
dependencies: {
'@open-matrix/cli': tarballDependencies['@open-matrix/cli'],
[authorPackage.name]: `file:${authorPackageTarball}`,
},
pnpm: {
overrides: tarballDependencies,
},
}, null, 2));
run('pnpm', ['install', '--ignore-scripts'], { cwd: packageConsumerDir });
const installedPackageDir = path.join(packageConsumerDir, 'node_modules', authorPackage.name);
const installedPackageJson = JSON.parse(await readFile(path.join(installedPackageDir, 'package.json'), 'utf8'));
if (installedPackageJson.private === true) {
throw new Error('Generated author package is still private after package-manager install');
}
const installedActorSource = await readFile(path.join(installedPackageDir, 'src', 'GreeterActor.ts'), 'utf8');
if (!installedActorSource.includes("from '@open-matrix/core'")) {
throw new Error('Package-manager installed source does not keep @open-matrix/core import');
}
packageRunner = spawn('pnpm', [
'exec', 'matrix', 'run',
installedPackageDir,
'--dir', packageConsumerDir,
'--port', String(packagePort),
], {
cwd: packageConsumerDir,
stdio: ['ignore', 'pipe', 'pipe'],
env: process.env,
});
await waitForRunReady(packageRunner);
const packageInvoke = run('pnpm', [
'exec', 'matrix', 'invoke',
'demo.greeter',
'echo',
'{"message":"hello-from-package-manager"}',
'--dir', packageConsumerDir,
'--port', String(packagePort),
], { cwd: packageConsumerDir, stdio: 'pipe' });
const packageInvoked = JSON.parse(packageInvoke.stdout.trim());
if (packageInvoked.ok !== true || packageInvoked.result?.message !== 'hello-from-package-manager') {
throw new Error(`Unexpected package-manager matrix invoke output: ${packageInvoke.stdout}`);
}
await stopChild(packageRunner);
packageRunner = undefined;
console.log(JSON.stringify({
ok: true,
proof: 'sdk-cli-uat',
sourceWorkspaceDir,
sdkWorkspaceDir: workspaceDir,
destructiveBuildIsolated: proofWorkspace.copied,
toolingDir,
authorDir,
cliInstalledSeparately: true,
authorPackageDependsOnCli: false,
2026-06-07 23:11:26 +00:00
legacyMonorepoPathReferences: false,
actorImport: '@open-matrix/core',
configuredKeyStored: existsSync(path.join(authorDir, '.matrix', 'credentials', 'matrix-api-key.json')),
validKeyConfigSupplied: Boolean(liveKeyConfig),
liveKeyValidation,
runState: '.matrix/run.json',
invoke: {
ok: invoked.ok,
result: invoked.result,
},
2026-06-07 23:11:26 +00:00
managedHostProof: managedHostProof ?? {
skipped: true,
reason: 'MATRIX_SDK_RUNTIME_HOST_BIN not supplied',
},
packageManagerInstall: {
tarball: path.basename(authorPackageTarball),
consumerDir: packageConsumerDir,
installedPackage: authorPackage.name,
sourceImportUnchanged: true,
packagePrivate: installedPackageJson.private === true,
invoke: {
ok: packageInvoked.ok,
result: packageInvoked.result,
},
},
}, null, 2));
} catch (error) {
keep = true;
throw error;
} finally {
if (runner) await stopChild(runner);
if (packageRunner) await stopChild(packageRunner);
2026-06-07 23:11:26 +00:00
if (runtimeHostStarted && runtimeHostBin) {
try {
runRuntimeHost(runtimeHostBin, ['stop', '--timeout', '15000'], { cwd: authorDir, stdio: 'pipe' });
} catch (error) {
2026-06-07 23:11:26 +00:00
console.error(`[prove-sdk-cli-uat] failed to stop managed host: ${error instanceof Error ? error.message : String(error)}`);
}
}
if (!keep) {
await rm(proofRoot, { recursive: true, force: true });
} else {
console.error(`[prove-sdk-cli-uat] kept proof directory: ${proofRoot}`);
}
}
}
main().catch((error) => {
console.error(error instanceof Error ? error.stack : error);
process.exit(1);
});