mission-control/scripts/e2e-openclaw/start-e2e-server.mjs

142 lines
4.2 KiB
JavaScript
Executable File

#!/usr/bin/env node
import { spawn } from 'node:child_process'
import fs from 'node:fs'
import net from 'node:net'
import path from 'node:path'
import process from 'node:process'
async function findAvailablePort(host = '127.0.0.1') {
return await new Promise((resolve, reject) => {
const server = net.createServer()
server.unref()
server.on('error', reject)
server.listen(0, host, () => {
const address = server.address()
if (!address || typeof address === 'string') {
server.close(() => reject(new Error('failed to resolve dynamic port')))
return
}
const { port } = address
server.close((err) => {
if (err) reject(err)
else resolve(port)
})
})
})
}
const modeArg = process.argv.find((arg) => arg.startsWith('--mode='))
const mode = modeArg ? modeArg.split('=')[1] : 'local'
if (mode !== 'local' && mode !== 'gateway') {
process.stderr.write(`Invalid mode: ${mode}\n`)
process.exit(1)
}
const repoRoot = process.cwd()
const fixtureSource = path.join(repoRoot, 'tests', 'fixtures', 'openclaw')
const runtimeRoot = path.join(repoRoot, '.tmp', 'e2e-openclaw', mode)
const dataDir = path.join(runtimeRoot, 'data')
const mockBinDir = path.join(repoRoot, 'scripts', 'e2e-openclaw', 'bin')
const skillsRoot = path.join(runtimeRoot, 'skills')
fs.rmSync(runtimeRoot, { recursive: true, force: true })
fs.mkdirSync(runtimeRoot, { recursive: true })
fs.mkdirSync(dataDir, { recursive: true })
fs.cpSync(fixtureSource, runtimeRoot, { recursive: true })
const gatewayHost = '127.0.0.1'
const gatewayPort = String(await findAvailablePort(gatewayHost))
const baseEnv = {
...process.env,
API_KEY: process.env.API_KEY || 'test-api-key-e2e-12345',
AUTH_USER: process.env.AUTH_USER || 'admin',
AUTH_PASS: process.env.AUTH_PASS || 'admin',
MISSION_CONTROL_TEST_MODE: process.env.MISSION_CONTROL_TEST_MODE || '1',
MC_DISABLE_RATE_LIMIT: '1',
MISSION_CONTROL_DATA_DIR: dataDir,
MISSION_CONTROL_DB_PATH: path.join(dataDir, 'mission-control.db'),
OPENCLAW_STATE_DIR: runtimeRoot,
OPENCLAW_CONFIG_PATH: path.join(runtimeRoot, 'openclaw.json'),
OPENCLAW_GATEWAY_HOST: gatewayHost,
OPENCLAW_GATEWAY_PORT: gatewayPort,
OPENCLAW_BIN: path.join(mockBinDir, 'openclaw'),
CLAWDBOT_BIN: path.join(mockBinDir, 'clawdbot'),
MC_SKILLS_USER_AGENTS_DIR: path.join(skillsRoot, 'user-agents'),
MC_SKILLS_USER_CODEX_DIR: path.join(skillsRoot, 'user-codex'),
MC_SKILLS_PROJECT_AGENTS_DIR: path.join(skillsRoot, 'project-agents'),
MC_SKILLS_PROJECT_CODEX_DIR: path.join(skillsRoot, 'project-codex'),
MC_SKILLS_OPENCLAW_DIR: path.join(skillsRoot, 'openclaw'),
PATH: `${mockBinDir}:${process.env.PATH || ''}`,
E2E_GATEWAY_EXPECTED: mode === 'gateway' ? '1' : '0',
}
const children = []
let app = null
if (mode === 'gateway') {
const gw = spawn('node', ['scripts/e2e-openclaw/mock-gateway.mjs'], {
cwd: repoRoot,
env: baseEnv,
stdio: 'inherit',
})
gw.on('error', (err) => {
process.stderr.write(`[openclaw-e2e] mock gateway failed to start: ${String(err)}\n`)
shutdown('SIGTERM')
process.exit(1)
})
gw.on('exit', (code, signal) => {
const exitCode = code ?? (signal ? 1 : 0)
if (exitCode !== 0) {
process.stderr.write(`[openclaw-e2e] mock gateway exited unexpectedly (code=${exitCode}, signal=${signal ?? 'none'})\n`)
shutdown('SIGTERM')
process.exit(exitCode)
}
})
children.push(gw)
}
const standaloneServerPath = path.join(repoRoot, '.next', 'standalone', 'server.js')
app = fs.existsSync(standaloneServerPath)
? spawn('node', [standaloneServerPath], {
cwd: repoRoot,
env: {
...baseEnv,
HOSTNAME: '127.0.0.1',
PORT: '3005',
},
stdio: 'inherit',
})
: spawn('pnpm', ['start'], {
cwd: repoRoot,
env: baseEnv,
stdio: 'inherit',
})
children.push(app)
function shutdown(signal = 'SIGTERM') {
for (const child of children) {
if (!child.killed) {
try {
child.kill(signal)
} catch {
// noop
}
}
}
}
process.on('SIGINT', () => {
shutdown('SIGINT')
process.exit(130)
})
process.on('SIGTERM', () => {
shutdown('SIGTERM')
process.exit(143)
})
app.on('exit', (code) => {
shutdown('SIGTERM')
process.exit(code ?? 0)
})