Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
201 lines
6.8 KiB
JavaScript
201 lines
6.8 KiB
JavaScript
const test = require("node:test");
|
|
const assert = require("node:assert/strict");
|
|
const { EventEmitter, once } = require("node:events");
|
|
const { PassThrough } = require("node:stream");
|
|
const {
|
|
CodexAppServerConnection,
|
|
buildCodexAppServerKey,
|
|
buildCodexAppServerLaunch,
|
|
} = require("./connection.cjs");
|
|
|
|
function createFakeChild() {
|
|
const child = new EventEmitter();
|
|
child.stdin = new PassThrough();
|
|
child.stdout = new PassThrough();
|
|
child.stderr = new PassThrough();
|
|
child.killed = false;
|
|
child.kill = () => { child.killed = true; };
|
|
return child;
|
|
}
|
|
|
|
async function readJsonLine(stream) {
|
|
const [chunk] = await once(stream, "data");
|
|
return JSON.parse(String(chunk).trim());
|
|
}
|
|
|
|
test("buildCodexAppServerLaunch runs JS entries through Node without a shell", () => {
|
|
assert.deepEqual(
|
|
buildCodexAppServerLaunch("/opt/codex/bin/codex.js", ["app-server", "--help"], { nodePath: "/usr/bin/node" }),
|
|
{
|
|
command: "/usr/bin/node",
|
|
args: ["/opt/codex/bin/codex.js", "app-server", "--help"],
|
|
env: { ELECTRON_RUN_AS_NODE: "1" },
|
|
},
|
|
);
|
|
assert.deepEqual(
|
|
buildCodexAppServerLaunch("/usr/local/bin/codex"),
|
|
{ command: "/usr/local/bin/codex", args: ["app-server", "--stdio"] },
|
|
);
|
|
assert.throws(() => buildCodexAppServerLaunch("C:\\npm\\codex.cmd"), /shell shim/);
|
|
});
|
|
|
|
test("App Server connection initializes once and correlates JSONL requests", async () => {
|
|
const child = createFakeChild();
|
|
const notifications = [];
|
|
const connection = new CodexAppServerConnection({
|
|
binPath: "/usr/bin/codex",
|
|
env: { HOME: "/tmp/home" },
|
|
appVersion: "1.2.3",
|
|
spawnImpl: () => child,
|
|
onNotification: (message) => notifications.push(message),
|
|
});
|
|
|
|
const startPromise = connection.start();
|
|
const initialize = await readJsonLine(child.stdin);
|
|
assert.equal(initialize.method, "initialize");
|
|
assert.equal(initialize.params.clientInfo.name, "netcatty");
|
|
assert.equal(initialize.params.capabilities.experimentalApi, true);
|
|
child.stdout.write(`${JSON.stringify({ id: initialize.id, result: { userAgent: "codex" } })}\n`);
|
|
await startPromise;
|
|
const initialized = await readJsonLine(child.stdin);
|
|
assert.equal(initialized.method, "initialized");
|
|
|
|
const requestPromise = connection.request("model/list", { limit: 100 });
|
|
const request = await readJsonLine(child.stdin);
|
|
assert.equal(request.method, "model/list");
|
|
child.stdout.write(`${JSON.stringify({ id: request.id, result: { data: [], nextCursor: null } })}\n`);
|
|
assert.deepEqual(await requestPromise, { data: [], nextCursor: null });
|
|
|
|
child.stdout.write(`${JSON.stringify({ method: "warning", params: { message: "heads up" } })}\n`);
|
|
await new Promise((resolve) => setImmediate(resolve));
|
|
assert.equal(notifications[0].method, "warning");
|
|
connection.close();
|
|
});
|
|
|
|
test("App Server connection preserves a Chinese response split across UTF-8 chunks", async () => {
|
|
const child = createFakeChild();
|
|
const connection = new CodexAppServerConnection({
|
|
binPath: "/usr/bin/codex",
|
|
env: {},
|
|
spawnImpl: () => child,
|
|
});
|
|
|
|
const startPromise = connection.start();
|
|
const initialize = await readJsonLine(child.stdin);
|
|
const response = Buffer.from(`${JSON.stringify({
|
|
id: initialize.id,
|
|
result: { message: "中文" },
|
|
})}\n`, "utf8");
|
|
const split = response.indexOf(Buffer.from("中", "utf8")) + 1;
|
|
child.stdout.write(response.subarray(0, split));
|
|
child.stdout.write(response.subarray(split));
|
|
await startPromise;
|
|
await readJsonLine(child.stdin);
|
|
connection.close();
|
|
});
|
|
|
|
test("App Server connection rejects an unterminated oversized JSONL message", async () => {
|
|
const child = createFakeChild();
|
|
let fatal;
|
|
const connection = new CodexAppServerConnection({
|
|
binPath: "/usr/bin/codex",
|
|
env: {},
|
|
maxJsonlLineBytes: 8,
|
|
spawnImpl: () => child,
|
|
onFatal: (error) => { fatal = error; },
|
|
});
|
|
|
|
const startPromise = connection.start();
|
|
await readJsonLine(child.stdin);
|
|
child.stdout.write("123456789");
|
|
|
|
await assert.rejects(startPromise, /message exceeded 8 bytes/);
|
|
assert.match(fatal.message, /message exceeded 8 bytes/);
|
|
assert.equal(child.killed, true);
|
|
});
|
|
|
|
test("App Server connection rejects pending RPCs when the process exits", async () => {
|
|
const child = createFakeChild();
|
|
let fatal;
|
|
const connection = new CodexAppServerConnection({
|
|
binPath: "/usr/bin/codex",
|
|
env: {},
|
|
spawnImpl: () => child,
|
|
onFatal: (error) => { fatal = error; },
|
|
});
|
|
const startPromise = connection.start();
|
|
const initialize = await readJsonLine(child.stdin);
|
|
child.stdout.write(`${JSON.stringify({ id: initialize.id, result: {} })}\n`);
|
|
await startPromise;
|
|
await readJsonLine(child.stdin); // initialized notification
|
|
|
|
const request = connection.request("thread/start", {});
|
|
await readJsonLine(child.stdin);
|
|
child.emit("exit", 1, null);
|
|
await assert.rejects(request, /exited unexpectedly/);
|
|
assert.match(fatal.message, /code 1/);
|
|
});
|
|
|
|
test("App Server close force-kills a child that ignores SIGTERM", async () => {
|
|
const child = createFakeChild();
|
|
const signals = [];
|
|
child.kill = (signal) => {
|
|
signals.push(signal);
|
|
return true;
|
|
};
|
|
const connection = new CodexAppServerConnection({
|
|
binPath: "/usr/bin/codex",
|
|
env: {},
|
|
closeKillGraceMs: 5,
|
|
spawnImpl: () => child,
|
|
});
|
|
const startPromise = connection.start();
|
|
const initialize = await readJsonLine(child.stdin);
|
|
child.stdout.write(`${JSON.stringify({ id: initialize.id, result: {} })}\n`);
|
|
await startPromise;
|
|
await readJsonLine(child.stdin);
|
|
|
|
connection.close();
|
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
|
|
assert.deepEqual(signals, ["SIGTERM", "SIGKILL"]);
|
|
});
|
|
|
|
test("App Server close does not retain or re-kill a child that exits synchronously on SIGTERM", async () => {
|
|
const child = createFakeChild();
|
|
const signals = [];
|
|
child.kill = (signal) => {
|
|
signals.push(signal);
|
|
if (signal === "SIGTERM") child.emit("exit", 0, "SIGTERM");
|
|
return true;
|
|
};
|
|
const connection = new CodexAppServerConnection({
|
|
binPath: "/usr/bin/codex",
|
|
env: {},
|
|
closeKillGraceMs: 5,
|
|
spawnImpl: () => child,
|
|
});
|
|
const startPromise = connection.start();
|
|
const initialize = await readJsonLine(child.stdin);
|
|
child.stdout.write(`${JSON.stringify({ id: initialize.id, result: {} })}\n`);
|
|
await startPromise;
|
|
await readJsonLine(child.stdin);
|
|
|
|
connection.close();
|
|
assert.equal(connection.getClosingProcessCountForTests(), 0);
|
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
|
|
assert.deepEqual(signals, ["SIGTERM"]);
|
|
});
|
|
|
|
test("App Server process keys include executable and environment identity", () => {
|
|
assert.notEqual(
|
|
buildCodexAppServerKey("/a/codex", { HOME: "/a" }),
|
|
buildCodexAppServerKey("/b/codex", { HOME: "/a" }),
|
|
);
|
|
assert.notEqual(
|
|
buildCodexAppServerKey("/a/codex", { HOME: "/a" }),
|
|
buildCodexAppServerKey("/a/codex", { HOME: "/b" }),
|
|
);
|
|
});
|