import { spawn } from 'node:child_process'; import { fileURLToPath } from 'node:url'; export const secret = process.env.CODEXMOBILE_RELAY_SECRET || 'test-relay-secret-0123456789abcdef'; export const previousSecret = process.env.CODEXMOBILE_RELAY_PREVIOUS_SECRET || 'previous-relay-secret-0123456789abcdef'; export const port = Number(process.env.CODEXMOBILE_RELAY_TEST_PORT || 9786); export const baseUrl = `http://127.0.0.1:${port}`; export const relayUrl = `ws://127.0.0.1:${port}/relay/mac`; export const localFixturePort = Number(process.env.CODEXMOBILE_RELAY_LOCAL_FIXTURE_PORT || 9788); export const tokenRequestLimit = Number(process.env.CODEXMOBILE_RELAY_TOKEN_REQUESTS_PER_MINUTE || 120); export const rootDir = fileURLToPath(new URL('..', import.meta.url)); const RELAY_STARTUP_TIMEOUT_MS = 10000; const RELAY_CONNECT_TIMEOUT_MS = 10000; const RELAY_STATUS_POLL_INTERVAL_MS = 100; const RELAY_REQUEST_TIMEOUT_MS = 5000; export function fail(message, detail) { console.error(`Relay smoke failed: ${message}`); if (detail) { console.error(detail); } process.exit(1); } export function spawnRelay() { const child = spawn(process.execPath, ['server/relay-server.js'], { cwd: rootDir, env: { ...process.env, HOST: '127.0.0.1', PORT: String(port), CODEXMOBILE_MODE: 'relay', CODEXMOBILE_RELAY_SECRET: secret, CODEXMOBILE_RELAY_PREVIOUS_SECRET: previousSecret, CODEXMOBILE_RELAY_HEARTBEAT_MS: '100', CODEXMOBILE_RELAY_IDLE_HEARTBEAT_MS: '500', CODEXMOBILE_RELAY_REQUEST_TIMEOUT_MS: '3000', CODEXMOBILE_RELAY_SMALL_BODY_BYTES: '1024', CODEXMOBILE_RELAY_PENDING_REQUESTS_MAX: '2', CODEXMOBILE_RELAY_BROWSER_PENDING_REQUESTS_MAX: '1', CODEXMOBILE_RELAY_TOKEN_REQUESTS_PER_MINUTE: String(tokenRequestLimit), CODEXMOBILE_RELAY_TOKEN_REQUEST_WINDOW_MS: '60000' }, stdio: ['ignore', 'pipe', 'pipe'] }); child.stderr.on('data', (chunk) => process.stderr.write(chunk)); return child; } export function spawnConnector(localUrl) { const child = spawn(process.execPath, ['scripts/relay-mac-client.mjs'], { cwd: rootDir, env: { ...process.env, CODEXMOBILE_RELAY_URL: relayUrl, CODEXMOBILE_RELAY_SECRET: secret, CODEXMOBILE_RELAY_LOCAL_URL: localUrl, CODEXMOBILE_RELAY_CONNECTOR_ID: 'test-mac', CODEXMOBILE_RELAY_DEVICE_NAME: 'fixture-mac', CODEXMOBILE_RELAY_HEARTBEAT_MS: '100', CODEXMOBILE_RELAY_IDLE_HEARTBEAT_MS: '500', CODEXMOBILE_RELAY_REQUEST_TIMEOUT_MS: '3000' }, stdio: ['ignore', 'pipe', 'pipe'] }); child.stderr.on('data', (chunk) => process.stderr.write(chunk)); return child; } export async function waitForRelay(child) { let output = ''; child.stdout.on('data', (chunk) => { output += chunk.toString(); }); const startedAt = Date.now(); while (Date.now() - startedAt < RELAY_STARTUP_TIMEOUT_MS) { if (output.includes('CodexMobile relay listening')) { return; } if (child.exitCode !== null) { fail('relay exited before listening', output); } await new Promise((resolve) => setTimeout(resolve, RELAY_STATUS_POLL_INTERVAL_MS)); } fail('relay did not start in time', output); } export async function waitForMacConnected() { const startedAt = Date.now(); while (Date.now() - startedAt < RELAY_CONNECT_TIMEOUT_MS) { const result = await request('/api/status', { headers: { authorization: 'Bearer valid-token' } }); if (result.data.macConnected && result.data.localStatus?.reachable) { return result.data; } await new Promise((resolve) => setTimeout(resolve, RELAY_STATUS_POLL_INTERVAL_MS)); } fail('relay did not observe connector in time', await request('/api/status')); } export async function request(path, options = {}) { let response; try { response = await fetch(`${baseUrl}${path}`, { ...options, signal: options.signal || AbortSignal.timeout(RELAY_REQUEST_TIMEOUT_MS), headers: { ...(options.body && !(options.body instanceof Buffer) ? { 'content-type': 'application/json' } : {}), ...(options.headers || {}) }, body: options.body && !(options.body instanceof Buffer) && typeof options.body !== 'string' ? JSON.stringify(options.body) : options.body }); } catch (error) { throw new Error(`request ${path} failed: ${error.message}`); } const text = await response.text(); let data = {}; try { data = text ? JSON.parse(text) : {}; } catch { data = { text }; } return { response, data }; } export async function requestBuffer(path, options = {}) { let response; try { response = await fetch(`${baseUrl}${path}`, { ...options, signal: options.signal || AbortSignal.timeout(RELAY_REQUEST_TIMEOUT_MS), headers: options.headers || {} }); } catch (error) { throw new Error(`request ${path} failed: ${error.message}`); } const responseBuffer = await response.arrayBuffer(); const body = Buffer.from(responseBuffer); return { response, body }; } export function multipartBody(boundary, fieldName, filename, contentType, content) { return Buffer.from([ `--${boundary}\r\n`, `content-disposition: form-data; name="${fieldName}"; filename="${filename}"\r\n`, `content-type: ${contentType}\r\n\r\n`, content, `\r\n--${boundary}--\r\n` ].join('')); }