codexmobile-relay / scripts /relay-smoke-support.mjs
Codex
deploy: CodexMobile Relay
90f0300
Raw
History Blame Contribute Delete
5.06 kB
import WebSocket from 'ws';
export function nextBrowserEvent(ws, expectedType, timeoutMs = 3000) {
return new Promise((resolve, reject) => {
function onMessage(raw) {
const event = JSON.parse(raw.toString());
if (event.type !== expectedType) {
return;
}
clearTimeout(timer);
ws.off('message', onMessage);
resolve(event);
}
const timer = setTimeout(() => {
ws.off('message', onMessage);
reject(new Error(`timed out waiting for browser event ${expectedType}`));
}, timeoutMs);
ws.on('message', onMessage);
});
}
export function expectRejectedMacSecret(relayUrl) {
const ws = new WebSocket(relayUrl, {
headers: {
authorization: 'Bearer wrong-relay-secret-0123456789abcdef'
}
});
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
ws.terminate();
reject(new Error('timed out waiting for Mac secret rejection'));
}, 3000);
ws.on('unexpected-response', (_req, response) => {
clearTimeout(timer);
if (response.statusCode === 401) {
resolve();
} else {
reject(new Error(`expected 401 for invalid Mac secret, got ${response.statusCode}`));
}
});
ws.on('open', () => {
clearTimeout(timer);
ws.close();
reject(new Error('invalid Mac secret should not open relay socket'));
});
ws.on('error', () => {});
});
}
export function connectMac({
relayUrl,
secret,
reachable = true,
delayProjects = false,
rateLimitChat = false,
connectorInstanceId = 'test-mac',
deviceName = 'test-mac'
} = {}) {
const ws = new WebSocket(relayUrl, {
headers: {
authorization: `Bearer ${secret}`
}
});
const messages = [];
ws.on('message', (raw) => {
const message = JSON.parse(raw.toString());
messages.push(message);
if (message.type === 'ping') {
ws.send(JSON.stringify({ type: 'pong', sentAt: message.sentAt }));
return;
}
if (message.type === 'auth.validate') {
replyAuthValidate(ws, message);
return;
}
if (message.type === 'http.request') {
replyHttpRequest(ws, message, { delayProjects, rateLimitChat });
}
});
return new Promise((resolve, reject) => {
ws.on('open', () => {
ws.send(JSON.stringify({
type: 'mac.hello',
protocolVersion: 1,
connectorInstanceId,
deviceName,
clientVersion: '0.1.0',
localStatus: { reachable, checkedAt: new Date().toISOString() },
capabilities: ['http', 'events']
}));
});
ws.on('message', (raw) => {
const message = JSON.parse(raw.toString());
if (message.type === 'relay.hello') {
resolve({ ws, messages });
}
});
ws.on('error', reject);
});
}
function replyAuthValidate(ws, message) {
const token = String(message.token || '');
ws.send(JSON.stringify({
type: 'auth.validate.result',
requestId: message.requestId,
macConnectionEpoch: message.macConnectionEpoch,
valid: token === 'valid-token' || token.startsWith('valid-token-')
}));
}
function replyHttpRequest(ws, message, { delayProjects = false, rateLimitChat = false } = {}) {
const reply = () => {
if (message.path === '/api/pair') {
sendHttpResponse(ws, message, 200, {
token: 'valid-token',
device: { id: 'device-1', name: 'iPhone' }
});
return;
}
if (message.path === '/api/projects') {
sendHttpResponse(ws, message, 200, {
projects: [{ id: 'mac-project', name: 'Mac Project', path: '/tmp/codexmobile-relay-fixture' }]
});
return;
}
if (message.path === '/api/projects/mac-project/sessions') {
sendHttpResponse(ws, message, 200, { sessions: [] });
return;
}
if (message.path.startsWith('/api/sessions/') && message.path.endsWith('/messages?limit=120')) {
sendHttpResponse(ws, message, 200, { messages: [] });
return;
}
if (message.path === '/api/sync') {
sendHttpResponse(ws, message, 200, { ok: true });
return;
}
if (message.path === '/api/chat/send') {
if (rateLimitChat) {
sendHttpResponse(ws, message, 429, { error: 'relay_rate_limited', retryAfter: 20 });
return;
}
sendHttpResponse(ws, message, 202, { accepted: true, turnId: 'turn-1' });
ws.send(JSON.stringify({
type: 'event',
eventId: 'event-1',
payload: { type: 'status-update', status: 'running', label: '正在思考' }
}));
return;
}
sendHttpResponse(ws, message, 404, { error: 'Not found' });
};
if (delayProjects && message.path === '/api/projects') {
setTimeout(reply, 800);
} else {
reply();
}
}
function sendHttpResponse(ws, message, status, body) {
ws.send(JSON.stringify({
type: 'http.response',
requestId: message.requestId,
macConnectionEpoch: message.macConnectionEpoch,
status,
headers: { 'content-type': 'application/json; charset=utf-8' },
bodyEncoding: 'json',
body
}));
}