mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-27 23:30:21 +00:00
fix(test): bound codex media path log polling
This commit is contained in:
@@ -1,9 +1,10 @@
|
||||
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
||||
import fs from "node:fs";
|
||||
import { setTimeout as delay } from "node:timers/promises";
|
||||
import { WebSocket } from "ws";
|
||||
import { PROTOCOL_VERSION } from "../../../../dist/gateway/protocol/index.js";
|
||||
import { renderBitmapTextPngBase64 } from "../../../../test/helpers/live-image-probe.ts";
|
||||
import { createJsonlRequestTailer } from "./jsonl-request-tail.mjs";
|
||||
import { waitForWebSocketOpen } from "./open-websocket.mjs";
|
||||
|
||||
const port = process.env.PORT;
|
||||
const token = process.env.OPENCLAW_GATEWAY_TOKEN;
|
||||
@@ -14,6 +15,10 @@ const timeoutSeconds = Number.parseInt(
|
||||
process.env.OPENCLAW_CODEX_MEDIA_PATH_TIMEOUT_SECONDS ?? "180",
|
||||
10,
|
||||
);
|
||||
const logTailMaxBytes = Number.parseInt(
|
||||
process.env.OPENCLAW_CODEX_MEDIA_PATH_LOG_TAIL_MAX_BYTES ?? `${2 * 1024 * 1024}`,
|
||||
10,
|
||||
);
|
||||
|
||||
if (!port || !token) {
|
||||
throw new Error("missing PORT/OPENCLAW_GATEWAY_TOKEN");
|
||||
@@ -29,16 +34,12 @@ function sha256Base64(data) {
|
||||
return createHash("sha256").update(Buffer.from(data, "base64")).digest("hex");
|
||||
}
|
||||
|
||||
function readLoggedRequests() {
|
||||
if (!fs.existsSync(appServerLog)) {
|
||||
return [];
|
||||
}
|
||||
return fs
|
||||
.readFileSync(appServerLog, "utf8")
|
||||
.split("\n")
|
||||
.filter(Boolean)
|
||||
.map((line) => JSON.parse(line));
|
||||
}
|
||||
const loggedRequests = createJsonlRequestTailer(appServerLog, {
|
||||
maxReadBytes:
|
||||
Number.isSafeInteger(logTailMaxBytes) && logTailMaxBytes > 0
|
||||
? logTailMaxBytes
|
||||
: 2 * 1024 * 1024,
|
||||
});
|
||||
|
||||
async function waitFor(label, predicate, timeoutMs) {
|
||||
const started = Date.now();
|
||||
@@ -67,18 +68,7 @@ function wsDataToString(data) {
|
||||
|
||||
async function connectGateway() {
|
||||
const ws = new WebSocket(`ws://127.0.0.1:${port}`);
|
||||
await new Promise((resolve, reject) => {
|
||||
const timer = setTimeout(() => reject(new Error("gateway ws open timeout")), 45_000);
|
||||
timer.unref?.();
|
||||
ws.once("open", () => {
|
||||
clearTimeout(timer);
|
||||
resolve();
|
||||
});
|
||||
ws.once("error", (error) => {
|
||||
clearTimeout(timer);
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
await waitForWebSocketOpen(ws, 45_000, "gateway ws open timeout");
|
||||
|
||||
const events = [];
|
||||
const pending = new Map();
|
||||
@@ -220,7 +210,7 @@ try {
|
||||
const turnRequest = await waitFor(
|
||||
"Codex turn/start image input",
|
||||
() =>
|
||||
readLoggedRequests().find((request) => {
|
||||
loggedRequests.read().find((request) => {
|
||||
if (request.method !== "turn/start") {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
105
scripts/e2e/lib/codex-media-path/jsonl-request-tail.mjs
Normal file
105
scripts/e2e/lib/codex-media-path/jsonl-request-tail.mjs
Normal file
@@ -0,0 +1,105 @@
|
||||
import fs from "node:fs";
|
||||
|
||||
const DEFAULT_MAX_READ_BYTES = 2 * 1024 * 1024;
|
||||
const DEFAULT_HISTORY_LIMIT = 1024;
|
||||
|
||||
function positiveInteger(value, fallback) {
|
||||
return Number.isSafeInteger(value) && value > 0 ? value : fallback;
|
||||
}
|
||||
|
||||
function readSlice(filePath, start, length) {
|
||||
if (length <= 0) {
|
||||
return "";
|
||||
}
|
||||
const fd = fs.openSync(filePath, "r");
|
||||
try {
|
||||
const buffer = Buffer.allocUnsafe(length);
|
||||
const bytesRead = fs.readSync(fd, buffer, 0, length, start);
|
||||
return buffer.subarray(0, bytesRead).toString("utf8");
|
||||
} finally {
|
||||
fs.closeSync(fd);
|
||||
}
|
||||
}
|
||||
|
||||
export function createJsonlRequestTailer(filePath, options = {}) {
|
||||
const maxReadBytes = positiveInteger(options.maxReadBytes, DEFAULT_MAX_READ_BYTES);
|
||||
const historyLimit = positiveInteger(options.historyLimit, DEFAULT_HISTORY_LIMIT);
|
||||
let offset = 0;
|
||||
let pending = "";
|
||||
let requests = [];
|
||||
|
||||
function parseLine(line) {
|
||||
try {
|
||||
return JSON.parse(line);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
throw new Error(`invalid app-server JSONL at ${filePath}: ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
read() {
|
||||
if (!fs.existsSync(filePath)) {
|
||||
return requests;
|
||||
}
|
||||
|
||||
const stats = fs.statSync(filePath);
|
||||
if (!stats.isFile()) {
|
||||
return requests;
|
||||
}
|
||||
if (stats.size < offset) {
|
||||
offset = 0;
|
||||
pending = "";
|
||||
requests = [];
|
||||
}
|
||||
if (stats.size === offset) {
|
||||
return requests;
|
||||
}
|
||||
|
||||
let start = offset;
|
||||
let discardFirstLine = false;
|
||||
let clamped = false;
|
||||
if (start === 0 && stats.size > maxReadBytes) {
|
||||
start = stats.size - maxReadBytes;
|
||||
pending = "";
|
||||
clamped = true;
|
||||
} else if (stats.size - start > maxReadBytes) {
|
||||
start = stats.size - maxReadBytes;
|
||||
pending = "";
|
||||
clamped = true;
|
||||
}
|
||||
if (clamped && start > 0) {
|
||||
discardFirstLine = readSlice(filePath, start - 1, 1) !== "\n";
|
||||
}
|
||||
|
||||
const text = readSlice(filePath, start, stats.size - start);
|
||||
offset = stats.size;
|
||||
if (!text) {
|
||||
return requests;
|
||||
}
|
||||
|
||||
let chunk = pending + text;
|
||||
if (discardFirstLine) {
|
||||
const newlineIndex = chunk.indexOf("\n");
|
||||
if (newlineIndex === -1) {
|
||||
pending = "";
|
||||
return requests;
|
||||
}
|
||||
chunk = chunk.slice(newlineIndex + 1);
|
||||
}
|
||||
|
||||
const lines = chunk.split("\n");
|
||||
pending = lines.pop() ?? "";
|
||||
for (const line of lines) {
|
||||
if (!line.trim()) {
|
||||
continue;
|
||||
}
|
||||
requests.push(parseLine(line));
|
||||
}
|
||||
if (requests.length > historyLimit) {
|
||||
requests = requests.slice(-historyLimit);
|
||||
}
|
||||
return requests;
|
||||
},
|
||||
};
|
||||
}
|
||||
40
scripts/e2e/lib/codex-media-path/open-websocket.mjs
Normal file
40
scripts/e2e/lib/codex-media-path/open-websocket.mjs
Normal file
@@ -0,0 +1,40 @@
|
||||
export function waitForWebSocketOpen(ws, timeoutMs, message = "gateway ws open timeout") {
|
||||
return new Promise((resolve, reject) => {
|
||||
let settled = false;
|
||||
|
||||
const settle = (fn, value) => {
|
||||
if (settled) {
|
||||
return;
|
||||
}
|
||||
settled = true;
|
||||
clearTimeout(timer);
|
||||
ws.off?.("open", onOpen);
|
||||
ws.off?.("error", onError);
|
||||
fn(value);
|
||||
};
|
||||
const onOpen = () => settle(resolve);
|
||||
const onError = (error) => settle(reject, error);
|
||||
const timer = setTimeout(() => {
|
||||
const consumeAbortError = () => {};
|
||||
const removeAbortErrorConsumer = () => {
|
||||
ws.off?.("error", consumeAbortError);
|
||||
ws.off?.("close", removeAbortErrorConsumer);
|
||||
};
|
||||
try {
|
||||
ws.off?.("error", onError);
|
||||
ws.on?.("error", consumeAbortError);
|
||||
ws.once?.("close", removeAbortErrorConsumer);
|
||||
ws.terminate?.();
|
||||
if (typeof ws.terminate !== "function") {
|
||||
ws.close?.();
|
||||
}
|
||||
} finally {
|
||||
settle(reject, new Error(message));
|
||||
}
|
||||
}, timeoutMs);
|
||||
|
||||
timer.unref?.();
|
||||
ws.once("open", onOpen);
|
||||
ws.once("error", onError);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user