openclaw/test/scripts/codex-media-path-client.test.ts
Peter Steinberger 47b7b4717f
refactor(scripts): deslop e2e harness (#159247)
* refactor(scripts): deslop e2e harness

Consolidate fixture generation, bounded readers, command handling, and gateway/controller lifecycle owners while preserving every existing scenario and assertion. Fix leaked mobile pairing frame waiters on failed WebSocket opening or sending.

* fix(scripts): repair e2e discovery and recovery fixtures

* test(scripts): follow shared Bun timeout validation
2026-09-27 06:08:12 +00:00

283 lines
11 KiB
TypeScript

// Codex Media Path Client tests cover codex media path client script behavior.
import { type ChildProcessWithoutNullStreams, spawn, spawnSync } from "node:child_process";
import { once } from "node:events";
import { appendFileSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { createJsonlRequestTailer } from "../../scripts/e2e/lib/codex-media-path/jsonl-request-tail.mts";
import { createBoundedChildOutput } from "../helpers/bounded-child-output.js";
import { useAutoCleanupTempDirTracker } from "../helpers/temp-dir.js";
const tempRoots = useAutoCleanupTempDirTracker(afterEach);
const fakeAppServerPath = path.resolve(
"scripts/e2e/lib/codex-media-path/fake-codex-app-server.mjs",
);
const writeConfigPath = path.resolve("scripts/e2e/lib/codex-media-path/write-config.mjs");
function jsonl(value: unknown): string {
return `${JSON.stringify(value)}\n`;
}
function padJsonlToLength(value: Record<string, unknown>, length: number): string {
const base = jsonl({ ...value, pad: "" });
if (base.length > length) {
throw new Error(`cannot pad JSONL down from ${base.length} to ${length}`);
}
return jsonl({ ...value, pad: "x".repeat(length - base.length) });
}
function runWriteConfig(root: string, env: Record<string, string> = {}) {
return spawnSync(process.execPath, [writeConfigPath], {
encoding: "utf8",
env: {
...process.env,
OPENCLAW_CONFIG_PATH: path.join(root, "openclaw.json"),
OPENCLAW_GATEWAY_TOKEN: "test-token",
OPENCLAW_STATE_DIR: path.join(root, "state"),
OPENCLAW_TEST_WORKSPACE_DIR: path.join(root, "workspace"),
PORT: "18790",
...env,
},
});
}
async function readStdoutLine(child: ChildProcessWithoutNullStreams): Promise<string> {
return await new Promise<string>((resolve, reject) => {
const stdout = createBoundedChildOutput();
const timeout = setTimeout(() => {
reject(new Error(`timed out waiting for fake app-server response: ${stdout.text()}`));
}, 3_000);
child.stdout.setEncoding("utf8");
child.stdout.on("data", (chunk: string) => {
stdout.append(chunk);
const line = stdout
.text()
.split("\n")
.find((entry) => entry.trim());
if (line) {
clearTimeout(timeout);
resolve(line);
}
});
child.once("error", (error) => {
clearTimeout(timeout);
reject(error);
});
child.once("exit", (code, signal) => {
clearTimeout(timeout);
reject(new Error(`fake app-server exited before response: code=${code} signal=${signal}`));
});
});
}
async function stopChild(child: ChildProcessWithoutNullStreams): Promise<void> {
if (child.exitCode !== null || child.signalCode !== null) {
return;
}
await new Promise<void>((resolve) => {
child.once("close", () => resolve());
child.kill("SIGTERM");
});
}
describe("codex media path limits", () => {
it("writes strict positive timeout and port values into generated config", () => {
const root = tempRoots.make("openclaw-codex-media-path-");
const result = runWriteConfig(root, {
OPENCLAW_CODEX_MEDIA_PATH_TIMEOUT_SECONDS: "240",
PORT: "19002",
});
expect(result.status).toBe(0);
const config = JSON.parse(readFileSync(path.join(root, "openclaw.json"), "utf8"));
expect(config.gateway.port).toBe(19002);
expect(config.agents.defaults.timeoutSeconds).toBe(240);
expect(config.plugins.entries.codex.config.appServer.requestTimeoutMs).toBe(240_000);
});
it("rejects loose write-config timeout env values", () => {
const root = tempRoots.make("openclaw-codex-media-path-");
const result = runWriteConfig(root, {
OPENCLAW_CODEX_MEDIA_PATH_TIMEOUT_SECONDS: "1e3",
});
expect(result.status).not.toBe(0);
expect(result.stderr).toContain("invalid OPENCLAW_CODEX_MEDIA_PATH_TIMEOUT_SECONDS: 1e3");
});
it("rejects out-of-range write-config gateway ports", () => {
const root = tempRoots.make("openclaw-codex-media-path-");
const result = runWriteConfig(root, { PORT: "65536" });
expect(result.status).not.toBe(0);
expect(result.stderr).toContain("invalid PORT: 65536");
});
});
describe("codex media path fake app-server", () => {
it("advertises the managed Codex version and image-capable catalog over stdio", async () => {
const requestLog = path.join(tempRoots.make("openclaw-codex-media-path-"), "requests.jsonl");
const version = JSON.parse(readFileSync("extensions/codex/package.json", "utf8")).dependencies[
"@openai/codex"
];
const child = spawn(process.execPath, [fakeAppServerPath], {
env: { ...process.env, OPENCLAW_CODEX_MEDIA_PATH_APP_SERVER_LOG: requestLog },
stdio: ["pipe", "pipe", "pipe"],
});
try {
const initialized = readStdoutLine(child);
child.stdin.write(jsonl({ id: "initialize", method: "initialize" }));
expect(JSON.parse(await initialized).result).toMatchObject({
serverInfo: { version },
userAgent: expect.stringContaining(`/${version} `),
});
const models = readStdoutLine(child);
child.stdin.write(jsonl({ id: "models", method: "model/list", params: {} }));
expect.soft(JSON.parse(await models)).toMatchObject({
id: "models",
result: {
data: [
{
id: "gpt-5.6-luna",
model: "gpt-5.6-luna",
displayName: expect.any(String),
description: expect.any(String),
hidden: false,
isDefault: true,
inputModalities: ["text", "image"],
defaultReasoningEffort: "low",
supportedReasoningEfforts: [
{ reasoningEffort: "low", description: expect.any(String) },
],
},
],
nextCursor: null,
},
});
const threads = readStdoutLine(child);
child.stdin.write(jsonl({ id: "threads", method: "thread/list", params: { limit: 64 } }));
expect.soft(JSON.parse(await threads)).toEqual({
id: "threads",
result: { data: [], nextCursor: null, backwardsCursor: null },
});
const started = readStdoutLine(child);
child.stdin.write(jsonl({ id: "thread", method: "thread/start", params: {} }));
expect(JSON.parse(await started).result.thread.cliVersion).toBe(version);
} finally {
await stopChild(child);
}
});
it("returns a structured error when request logging fails", async () => {
const requestLogDirectory = tempRoots.make("openclaw-codex-media-path-");
const child: ChildProcessWithoutNullStreams = spawn(process.execPath, [fakeAppServerPath], {
env: {
...process.env,
OPENCLAW_CODEX_MEDIA_PATH_APP_SERVER_LOG: requestLogDirectory,
},
stdio: ["pipe", "pipe", "pipe"],
});
const stderr = createBoundedChildOutput();
child.stderr.setEncoding("utf8");
child.stderr.on("data", (chunk: string) => {
stderr.append(chunk);
});
// Reproduce independent pipe delivery: stdout can arrive before stderr.
child.stderr.pause();
try {
const responseLine = readStdoutLine(child);
child.stdin.write(jsonl({ id: "request-1", method: "initialize" }));
const response = JSON.parse(await responseLine);
expect(response).toMatchObject({
error: {
message: expect.stringContaining("fake Codex app-server request log write failed"),
},
id: "request-1",
});
// Closing stdin lets the fixture exit; close joins both output streams.
const closed = once(child, "close");
child.stdin.end();
child.stderr.resume();
await closed;
expect(stderr.text()).toContain("fake Codex app-server request log write failed");
} finally {
await stopChild(child);
}
});
});
describe("codex media path JSONL tailer", () => {
it("keeps parsed app-server requests and reads only appended lines", () => {
const logPath = path.join(tempRoots.make("openclaw-codex-media-path-"), "app-server.jsonl");
const tailer = createJsonlRequestTailer(logPath, { maxReadBytes: 1024, historyLimit: 10 });
expect(tailer.read()).toEqual([]);
writeFileSync(logPath, jsonl({ method: "initialize" }));
expect(tailer.read()).toEqual([{ method: "initialize" }]);
appendFileSync(logPath, JSON.stringify({ method: "turn/start" }));
expect(tailer.read()).toEqual([{ method: "initialize" }]);
appendFileSync(logPath, "\n");
expect(tailer.read()).toEqual([{ method: "initialize" }, { method: "turn/start" }]);
});
it("starts from a bounded tail of oversized logs", () => {
const logPath = path.join(tempRoots.make("openclaw-codex-media-path-"), "app-server.jsonl");
const lastLine = jsonl({ method: "turn/start" });
writeFileSync(logPath, `${"x".repeat(256)}\n${jsonl({ method: "old" })}${lastLine}`);
const tailer = createJsonlRequestTailer(logPath, {
maxReadBytes: lastLine.length + 2,
historyLimit: 10,
});
expect(tailer.read()).toEqual([{ method: "turn/start" }]);
});
it("keeps a complete line when the bounded tail starts on its boundary", () => {
const logPath = path.join(tempRoots.make("openclaw-codex-media-path-"), "app-server.jsonl");
const lastLine = jsonl({ method: "turn/start" });
writeFileSync(logPath, `${"x".repeat(256)}\n${lastLine}`);
const tailer = createJsonlRequestTailer(logPath, {
maxReadBytes: lastLine.length,
historyLimit: 10,
});
expect(tailer.read()).toEqual([{ method: "turn/start" }]);
});
it("resets request history when the app-server log is truncated", () => {
const logPath = path.join(tempRoots.make("openclaw-codex-media-path-"), "app-server.jsonl");
const tailer = createJsonlRequestTailer(logPath, { maxReadBytes: 1024, historyLimit: 10 });
writeFileSync(logPath, jsonl({ method: "initialize", payload: "long enough to rotate" }));
expect(tailer.read()).toEqual([{ method: "initialize", payload: "long enough to rotate" }]);
writeFileSync(logPath, jsonl({ method: "turn/start" }));
expect(tailer.read()).toEqual([{ method: "turn/start" }]);
});
it("resets request history when a rotated app-server log keeps the same size", () => {
const logPath = path.join(tempRoots.make("openclaw-codex-media-path-"), "app-server.jsonl");
const tailer = createJsonlRequestTailer(logPath, { maxReadBytes: 1024, historyLimit: 10 });
const oldText = jsonl({ method: "initialize", pad: "x".repeat(64) });
const replacementText = padJsonlToLength({ method: "turn/start" }, oldText.length);
const replacement = JSON.parse(replacementText) as Record<string, unknown>;
expect(replacementText.length).toBe(oldText.length);
writeFileSync(logPath, oldText);
expect(tailer.read()).toEqual([{ method: "initialize", pad: "x".repeat(64) }]);
rmSync(logPath, { force: true });
writeFileSync(logPath, replacementText);
expect(tailer.read()).toEqual([replacement]);
});
});