#!/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');