import assert from "node:assert/strict"; import { spawn } from "node:child_process"; import type { ChildProcessWithoutNullStreams } from "node:child_process"; import { once } from "node:events"; import { createServer } from "node:net"; import { fileURLToPath } from "node:url"; import test from "node:test"; import { TestWebSocketPeer } from "./websocket-peer.ts"; const repositoryRoot = fileURLToPath(new URL("..", import.meta.url)); const executable = fileURLToPath( new URL("../bin/codex-app-server-bridge.js", import.meta.url), ); type RunningBridge = Readonly<{ child: ChildProcessWithoutNullStreams; stdout: string[]; stderr: string[]; }>; function startBridge(port: number, arguments_: string[] = ["app-server"]): RunningBridge { const child = spawn(process.execPath, [executable, ...arguments_], { cwd: repositoryRoot, env: { ...process.env, CODEX_APP_SERVER_URL: `ws://127.0.0.1:${port}`, }, stdio: ["pipe", "pipe", "pipe"], }); const stdout: string[] = []; const stderr: string[] = []; child.stdout.setEncoding("utf8"); child.stderr.setEncoding("utf8"); child.stdout.on("data", (chunk: string) => stdout.push(chunk)); child.stderr.on("data", (chunk: string) => stderr.push(chunk)); return { child, stdout, stderr }; } async function waitForExit(bridge: RunningBridge): Promise<{ code: number | null; signal: NodeJS.Signals | null; }> { const [code, signal] = await once(bridge.child, "exit"); return { code: code as number | null, signal: signal as NodeJS.Signals | null }; } async function unusedPort(): Promise { const server = createServer(); server.listen(0, "127.0.0.1"); await once(server, "listening"); const address = server.address(); if (address === null || typeof address === "string") { throw new Error("temporary server did not receive a TCP port"); } const { port } = address; server.close(); await once(server, "close"); return port; } test("unsupported arguments fail without opening a socket", async (context) => { const peer = await TestWebSocketPeer.start(); context.after(async () => peer.stop()); for (const arguments_ of [[], ["other"], ["app-server", "extra"]]) { const bridge = startBridge(peer.port, arguments_); const result = await waitForExit(bridge); assert.equal(result.code, 1); assert.match(bridge.stderr.join(""), /Usage: codex-app-server-bridge app-server/); } assert.equal(peer.connections.length, 0); }); test("startup input is preserved and duplex payload text stays ordered", async (context) => { const peer = await TestWebSocketPeer.start({ handshakeDelayMs: 100 }); context.after(async () => peer.stop()); const bridge = startBridge(peer.port); const initialize = '{"jsonrpc":"2.0","id":1,"method":"initialize"}'; const logout = '{"jsonrpc":"2.0","id":2,"method":"account/logout","secret":"never-log-me"}'; bridge.child.stdin.write(`${initialize}\n\n \n${logout}\n`); const connection = await peer.nextConnection(); await once(connection, "text"); if (connection.textFrames.length < 2) { await once(connection, "text"); } assert.deepEqual(connection.textFrames, [initialize, logout]); const responseOne = '{"jsonrpc":"2.0","id":1,"result":{"ready":true}}'; const responseTwo = '{"jsonrpc":"2.0","id":2,"result":null}'; connection.sendText(responseOne); connection.sendText(responseTwo); while (!bridge.stdout.join("").includes(responseTwo)) { await once(bridge.child.stdout, "data"); } assert.equal(bridge.stdout.join(""), `${responseOne}\n${responseTwo}\n`); bridge.child.stdin.end(); const result = await waitForExit(bridge); assert.equal(result.code, 0); assert.equal(bridge.stderr.join(""), ""); }); test("binary frames are private fatal protocol errors", async (context) => { const peer = await TestWebSocketPeer.start(); context.after(async () => peer.stop()); const bridge = startBridge(peer.port); const connection = await peer.nextConnection(); connection.sendBinary(Buffer.from("binary-secret")); const result = await waitForExit(bridge); assert.equal(result.code, 1); assert.equal(bridge.stdout.join(""), ""); assert.match(bridge.stderr.join(""), /binary WebSocket frames are not supported/); assert.doesNotMatch(bridge.stderr.join(""), /binary-secret/); }); test("unexpected remote closure is nonzero and does not reconnect", async (context) => { const peer = await TestWebSocketPeer.start(); context.after(async () => peer.stop()); const bridge = startBridge(peer.port); const connection = await peer.nextConnection(); connection.close(); const result = await waitForExit(bridge); assert.equal(result.code, 1); assert.match(bridge.stderr.join(""), /connection closed unexpectedly/); await new Promise((resolve) => setTimeout(resolve, 50)); assert.equal(peer.connections.length, 1); }); test("connection refusal is sanitized and nonzero", async () => { const port = await unusedPort(); const bridge = startBridge(port); bridge.child.stdin.write('{"secret":"must-not-appear"}\n'); const result = await waitForExit(bridge); assert.equal(result.code, 1); assert.match(bridge.stderr.join(""), new RegExp(`connection error for ws://127\\.0\\.0\\.1:${port}`)); assert.doesNotMatch(bridge.stderr.join(""), /must-not-appear/); }); test("a stalled handshake fails at the five-second startup deadline", async (context) => { const peer = await TestWebSocketPeer.start({ completeHandshake: false }); context.after(async () => peer.stop()); const bridge = startBridge(peer.port); const startedAt = Date.now(); bridge.child.stdin.write('{"id":1,"secret":"timeout-secret"}\n'); const result = await waitForExit(bridge); const elapsed = Date.now() - startedAt; assert.equal(result.code, 1); assert.ok(elapsed >= 4_800, `timeout fired too early after ${elapsed} ms`); assert.ok(elapsed < 7_000, `timeout fired too late after ${elapsed} ms`); assert.match(bridge.stderr.join(""), /startup timeout after 5000 ms/); assert.doesNotMatch(bridge.stderr.join(""), /timeout-secret/); assert.equal(peer.connections.length, 0); }); test("stdin EOF stays successful when the peer aborts during local close", async (context) => { const peer = await TestWebSocketPeer.start(); context.after(async () => peer.stop()); const bridge = startBridge(peer.port); const connection = await peer.nextConnection(); connection.abortOnCloseFrame = true; bridge.child.stdin.end(); const result = await waitForExit(bridge); assert.equal(result.code, 0); assert.equal(result.signal, null); assert.equal(bridge.stderr.join(""), ""); }); for (const signal of ["SIGINT", "SIGTERM"] as const) { test(`${signal} closes locally with a zero exit`, async (context) => { const peer = await TestWebSocketPeer.start(); context.after(async () => peer.stop()); const bridge = startBridge(peer.port); const connection = await peer.nextConnection(); const closeFrame = once(connection, "closeFrame"); bridge.child.kill(signal); await closeFrame; const result = await waitForExit(bridge); assert.equal(result.code, 0); assert.equal(result.signal, null); assert.equal(bridge.stderr.join(""), ""); }); } test("two bridge processes use distinct connections and leave the peer alive", async (context) => { const peer = await TestWebSocketPeer.start(); context.after(async () => peer.stop()); const first = startBridge(peer.port); const second = startBridge(peer.port); first.child.stdin.write("first\n"); second.child.stdin.write("second\n"); while (peer.connections.length < 2) { await once(peer, "connection"); } while (peer.connections.some((connection) => connection.textFrames.length === 0)) { await Promise.race(peer.connections.map(async (connection) => once(connection, "text"))); } assert.equal(peer.connections.length, 2); assert.deepEqual( peer.connections.flatMap((connection) => connection.textFrames).sort(), ["first", "second"], ); first.child.stdin.end(); second.child.stdin.end(); const [firstResult, secondResult] = await Promise.all([ waitForExit(first), waitForExit(second), ]); assert.equal(firstResult.code, 0); assert.equal(secondResult.code, 0); assert.equal(peer.server.listening, true); });