| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221 |
- 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<number> {
- 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);
- });
|