117 lines
3.5 KiB
JavaScript
117 lines
3.5 KiB
JavaScript
#!/usr/bin/env node
|
|
import { spawn } from 'node:child_process';
|
|
import path from 'node:path';
|
|
|
|
import { assertPortalRuntimePath, inspectPortalRuntime } from '../release-gate/artifact.mjs';
|
|
|
|
const root = path.resolve(new URL('..', import.meta.url).pathname);
|
|
const runtime = assertPortalRuntimePath(path.join(root, '.runtime', 'portal'), { repoRoot: root });
|
|
const image = process.env.RELEASE_GATE_NODE_IMAGE || 'node:24-bookworm';
|
|
|
|
async function runDocker(commandArgs, { input = '' } = {}) {
|
|
return new Promise((resolve, reject) => {
|
|
const child = spawn('docker', commandArgs, {
|
|
cwd: root,
|
|
env: process.env,
|
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
});
|
|
let stdout = '';
|
|
let stderr = '';
|
|
child.stdout.setEncoding('utf8');
|
|
child.stderr.setEncoding('utf8');
|
|
child.stdout.on('data', (chunk) => { stdout += chunk; });
|
|
child.stderr.on('data', (chunk) => { stderr += chunk; });
|
|
child.once('error', reject);
|
|
child.once('close', (code) => resolve({ code: code ?? 1, stdout, stderr }));
|
|
child.stdin.end(input);
|
|
});
|
|
}
|
|
|
|
function baseDockerArgs() {
|
|
return [
|
|
'run',
|
|
'--rm',
|
|
'-i',
|
|
'--network',
|
|
'none',
|
|
'--mount',
|
|
`type=bind,src=${runtime},dst=/opt/portal,readonly`,
|
|
'--tmpfs',
|
|
'/workspace:rw,noexec,nosuid,size=64m',
|
|
image,
|
|
];
|
|
}
|
|
|
|
async function requireSuccess(label, args, options) {
|
|
const result = await runDocker([...baseDockerArgs(), ...args], options);
|
|
if (result.code !== 0) {
|
|
throw new Error(`${label} failed (${result.code}): ${result.stderr.slice(-2_000)}`);
|
|
}
|
|
console.log(`PASS REL-11 ${label}`);
|
|
return result.stdout;
|
|
}
|
|
|
|
const inspection = await inspectPortalRuntime(runtime);
|
|
if (inspection.missing.length || inspection.forbidden.length) {
|
|
throw new Error(
|
|
`runtime inspection failed: missing=${inspection.missing.join(',')} forbidden=${inspection.forbidden.join(',')}`,
|
|
);
|
|
}
|
|
|
|
await requireSuccess('server bundle parses in Linux ARM64 Node', [
|
|
'node',
|
|
'--check',
|
|
'/opt/portal/server.mjs',
|
|
]);
|
|
await requireSuccess('WeChat bundle parses in Linux ARM64 Node', [
|
|
'node',
|
|
'--check',
|
|
'/opt/portal/wechat-mp.bundle.mjs',
|
|
]);
|
|
await requireSuccess('Agent Run worker loads its runtime dependencies', [
|
|
'node',
|
|
'/opt/portal/scripts/agent-run-worker.mjs',
|
|
'--help',
|
|
]);
|
|
|
|
const initialize = `${JSON.stringify({
|
|
jsonrpc: '2.0',
|
|
id: 1,
|
|
method: 'initialize',
|
|
params: {},
|
|
})}\n`;
|
|
const sandboxOutput = await requireSuccess(
|
|
'sandbox MCP starts and answers initialize',
|
|
['node', '/opt/portal/mindspace-sandbox-mcp.mjs', '/workspace'],
|
|
{ input: initialize },
|
|
);
|
|
if (!sandboxOutput.includes('"name":"mindspace-sandbox"')) {
|
|
throw new Error('sandbox MCP initialize response is missing server identity');
|
|
}
|
|
|
|
const searchOutput = await requireSuccess(
|
|
'MindSearch MCP starts and answers initialize',
|
|
['node', '/opt/portal/tkmind-search-mcp.mjs'],
|
|
{ input: initialize },
|
|
);
|
|
if (!searchOutput.includes('"name":"tkmind-search"')) {
|
|
throw new Error('MindSearch MCP initialize response is missing server identity');
|
|
}
|
|
|
|
const excelResult = await runDocker(
|
|
[
|
|
...baseDockerArgs(),
|
|
'env',
|
|
'EXCEL_ANALYST_ENABLED=1',
|
|
'MINDSPACE_WORKSPACE_ROOT=/workspace',
|
|
'node',
|
|
'/opt/portal/tkmind-excel-mcp.mjs',
|
|
'/workspace',
|
|
],
|
|
{ input: initialize },
|
|
);
|
|
if (excelResult.code !== 0 || !excelResult.stdout.includes('"name":"tkmind-excel"')) {
|
|
throw new Error(`Excel MCP failed to start: ${excelResult.stderr.slice(-2_000)}`);
|
|
}
|
|
console.log('PASS REL-11 Excel MCP starts and answers initialize');
|