codexmobile-relay / scripts /relay-smoke-env.mjs
Codex
deploy: CodexMobile Relay
90f0300
Raw
History Blame Contribute Delete
5.41 kB
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(''));
}