mirror of
https://github.com/openclaw/openclaw.git
synced 2026-10-03 09:39:25 +00:00
* 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
283 lines
11 KiB
TypeScript
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]);
|
|
});
|
|
});
|