const fs = require('node:fs'); const path = require('node:path'); const net = require('node:net'); const { createInterface } = require('node:readline'); const GLOBAL_VALUES = new Set(['-p', '--project-name', '-f', '--file', '--env-file', '--profile']); const OPTION_VALUES = new Set(['--entrypoint', '-e', '--env', '--tail', '--format', '--type', '--timeout', '-t']); const SWITCHES = new Set(['-d', '--detach', '--build', '--no-build', '--no-deps', '--rm', '-T', '-q', '--quiet', '-a', '--all', '-f', '--force', '--no-trunc']); const LIFECYCLE = new Set(['up', 'down', 'run', 'start', 'stop', 'restart', 'rm', 'create', 'kill', 'pause', 'unpause']); function parseCompose(argv) { const args = [...argv]; if (args[0] === 'compose') args.shift(); const globals = {}; while (args[0]?.startsWith('-')) { const option = args.shift(); if (!GLOBAL_VALUES.has(option) || !args.length) throw new Error('unknown_compose_global'); globals[option] = args.shift(); } const operation = args.shift(); const targets = []; let noDeps = false; while (args.length) { const argument = args.shift(); if (argument.startsWith('-')) { if (OPTION_VALUES.has(argument)) { if (!args.length) throw new Error('missing_option_value'); args.shift(); } else if (!SWITCHES.has(argument)) throw new Error('unknown_compose_option'); if (argument === '--no-deps') noDeps = true; } else { targets.push(argument); if (['run', 'exec', 'port'].includes(operation)) break; } } return { operation, targets, noDeps, globals, containerArgs: args }; } function lifecycleServices(call, services) { if (!LIFECYCLE.has(call.operation)) return []; const selected = call.operation === 'down' || !call.targets.length ? Object.keys(services) : call.targets; const result = new Set(); function visit(service) { if (!Object.hasOwn(services, service)) throw new Error('unknown_compose_service'); if (result.has(service)) return; result.add(service); if (['up', 'run'].includes(call.operation) && !call.noDeps) { const dependencies = services[service].depends_on ?? {}; for (const dependency of Array.isArray(dependencies) ? dependencies : Object.keys(dependencies)) visit(dependency); } } selected.forEach(visit); return [...result]; } const COMMANDS = new Set(['docker', 'docker-compose', 'curl', 'timeout', 'sleep', 'ssh', 'rsync', 'node', 'git', 'sudo']); const result = (code = 0, stdout = '') => ({ code, stdout }); const output = value => result(0, String(value) + '\n'); async function executeCommand(root, command, args, readStdin, signal) { if (!COMMANDS.has(command) || !Array.isArray(args) || args.some(arg => typeof arg !== 'string')) throw new Error('fixture_only'); const config = JSON.parse(fs.readFileSync(path.join(root, 'config.json'), 'utf8')); const event = { command, args }; if (command === 'docker-compose' || (command === 'docker' && args[0] === 'compose')) { event.compose = parseCompose(args); event.lifecycle = lifecycleServices(event.compose, config.services); } fs.appendFileSync(path.join(root, 'trace.jsonl'), JSON.stringify(event) + '\n'); if (event.compose && config.driftAfter === event.compose.operation + ':' + event.compose.targets.join()) { fs.writeFileSync(path.join(root, 'health-drift'), '1'); } if (command === 'timeout') { return { action: 'timeout' }; } if (command === 'sleep') return result(); if (command === 'curl') { if (args.join(' ') !== '-fsS http://127.0.0.1:31001/ready') throw new Error('unexpected_network_request'); return output(JSON.stringify({ data: { buildSha: '123456789abc' } })); } if (command === 'docker' && args[0] === 'inspect') { if (args.at(-1) === 'e'.repeat(64) && args.includes('{{.Config.Image}}')) { return output('synthetic-project-worker:latest'); } if (args.at(-1) !== 'd'.repeat(64)) throw new Error('unexpected_container'); if (config.health === 'inspect_error') return result(18); const counterFile = path.join(root, 'inspect-count'); const count = fs.existsSync(counterFile) ? Number(fs.readFileSync(counterFile, 'utf8')) : 0; fs.writeFileSync(counterFile, String(count + 1)); if (config.health === 'query_hang') { await new Promise(resolve => { const finish = () => { clearTimeout(timer); signal.removeEventListener('abort', finish); resolve(); }; const timer = setTimeout(finish, 20000); signal.addEventListener('abort', finish, { once: true }); if (signal.aborted) finish(); }); return result(); } const states = { stopped: 'exited|healthy', unhealthy: 'running|unhealthy', no_health: 'running|missing', starting: 'running|starting' }; const state = fs.existsSync(path.join(root, 'health-drift')) ? 'running|unhealthy' : config.health === 'warming' && count === 0 ? 'running|starting' : states[config.health] ?? 'running|healthy'; return output(state); } if (command === 'docker' && args.join(' ') === 'image inspect synthetic-project-worker:latest') return result(config.workerTagPresent ? 0 : 1); const call = event.compose; if (!call) throw new Error('forbidden_external_command'); if (call.operation === 'version') { return result(command === 'docker' && config.engine === 'v1' ? 1 : 0); } if (call.globals['-p'] !== 'synthetic-project') throw new Error('wrong_compose_project'); if (call.noDeps && config.unsupportedIsolation) return result(2); if (LIFECYCLE.has(call.operation)) { if (call.operation === 'up' && call.targets.includes('db')) fs.writeFileSync(path.join(root, 'db-started'), '1'); if (call.operation === 'run') { if (call.targets.join() !== 'init' || call.containerArgs.join(' ') !== '-lc cd /app/server && npm run deploy:init' || !args.includes('DEPLOY_DATA_MODE=' + config.mode)) { throw new Error('unexpected_init_command'); } if (config.initFails) return result(23); } return result(); } if (call.operation === 'ps') { if (!call.targets.length) return result(); if (call.targets.join() === 'worker') { return config.workerImagePresent ? output('e'.repeat(64)) : result(); } if (call.targets.join() !== 'db') throw new Error('unexpected_ps_service'); if (config.health === 'query_error') return result(17); if (config.health === 'missing' && !fs.existsSync(path.join(root, 'db-started'))) return result(); return output(config.health === 'multiple' ? 'd'.repeat(64) + '\n' + 'e'.repeat(64) : 'd'.repeat(64)); } if (call.operation === 'port' && call.targets.join() === 'server' && call.containerArgs.join() === '3000') { return output('127.0.0.1:31001'); } if (call.operation === 'config') return result(); if (call.operation === 'build' && ['init', 'server', 'worker'].includes(call.targets.join())) return result(); if (call.operation === 'logs' && call.targets.join() === 'server') return result(); if (call.operation === 'exec' && call.targets.join() === 'server') { const expected = ['sh -lc cd /app/server && npx prisma migrate status', 'node src/scripts/smoke-external-deps.js', 'node src/scripts/smoke-trialrun-runtime.js']; if (expected.includes(call.containerArgs.join(' '))) return result(); if (call.containerArgs[0] === 'node' && call.containerArgs[1] === '-e' && call.containerArgs[2]?.includes('EXPECTED_BUILD_SHA')) { const input = JSON.parse(await readStdin()); if (input.data?.buildSha !== '123456789abc') throw new Error('wrong_ready_build'); return result(); } } throw new Error('unknown_fake_command'); } async function startCommandServer(root) { if (!root || !path.basename(root).startsWith('hyb-db-lifecycle-')) throw new Error('fixture_only'); const sockets = new Set(); const server = net.createServer(async socket => { sockets.add(socket); const abort = new AbortController(); socket.on('error', () => socket.destroy()); socket.on('close', () => { sockets.delete(socket); abort.abort(); }); const lines = createInterface({ input: socket, crlfDelay: Infinity }); const reader = lines[Symbol.asyncIterator](); try { const request = JSON.parse((await reader.next()).value); const reply = await executeCommand(root, request.command, request.args, async () => { socket.write(JSON.stringify({ action: 'stdin' }) + '\n'); const input = JSON.parse((await reader.next()).value); if (typeof input !== 'string') throw new Error('invalid_stdin'); return input; }, abort.signal); if (!socket.destroyed) socket.end(JSON.stringify(reply) + '\n'); } catch (error) { if (!socket.destroyed) socket.end(JSON.stringify({ code: 97, stderr: '[deployment-fixture] ' + (/^[a-z_]+$/.test(error.message) ? error.message : 'unexpected_failure') + '\n' }) + '\n'); } finally { lines.close(); } }); await new Promise((resolve, reject) => { server.once('error', reject); server.listen(path.join(root, 'command.sock'), resolve); }); fs.chmodSync(path.join(root, 'command.sock'), 0o600); return async () => { for (const socket of sockets) socket.destroy(); await new Promise((resolve, reject) => server.close(error => error ? reject(error) : resolve())); }; } module.exports = { parseCompose, lifecycleServices, startCommandServer };