openclaw/scripts/e2e/lib/agent-turn-output.mjs
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

188 lines
5.2 KiB
JavaScript

// Helpers for extracting agent turn output from E2E protocol events.
import { isRecord } from "../../lib/record-shared.mjs";
import { readTextFileTail, tailText, textFileContains } from "./text-file-utils.mjs";
const ERROR_DETAIL_TAIL_BYTES = 64 * 1024;
const OUTPUT_SCAN_TAIL_BYTES = 2 * 1024 * 1024;
const REPLY_TEXT_PREVIEW_BYTES = 8 * 1024;
const REPLY_TEXT_PREVIEW_COUNT = 5;
const OPENAI_REQUEST_PATH_PATTERN = /\/v1\/(responses|chat\/completions)/u;
function textByteLength(text) {
return Buffer.byteLength(text, "utf8");
}
function summarizeReplyTexts(replyTexts) {
const previewStart = Math.max(0, replyTexts.length - REPLY_TEXT_PREVIEW_COUNT);
const recent = replyTexts.slice(previewStart).map((text, index) => ({
index: previewStart + index,
bytes: textByteLength(text),
tail: tailText(text, REPLY_TEXT_PREVIEW_BYTES),
}));
return JSON.stringify({ count: replyTexts.length, recent });
}
function parseJson(text) {
try {
return JSON.parse(text);
} catch {
return undefined;
}
}
function isJsonObjectRecordStart(text, index) {
for (let cursor = index - 1; cursor >= 0; cursor -= 1) {
const char = text[cursor];
if (char === "\n" || char === "\r") {
return true;
}
if (char !== " " && char !== "\t") {
return false;
}
}
return true;
}
function parseJsonObjectsFromText(text) {
const payloads = [];
let start = -1;
let depth = 0;
let inString = false;
let escaped = false;
for (let index = 0; index < text.length; index += 1) {
const char = text[index];
if (start === -1) {
if (char === "{" && isJsonObjectRecordStart(text, index)) {
start = index;
depth = 1;
inString = false;
escaped = false;
}
continue;
}
if (inString) {
if (escaped) {
escaped = false;
} else if (char === "\\") {
escaped = true;
} else if (char === '"') {
inString = false;
}
continue;
}
if (char === '"') {
inString = true;
continue;
}
if (char === "{") {
depth += 1;
continue;
}
if (char !== "}") {
continue;
}
depth -= 1;
if (depth === 0) {
const parsed = parseJson(text.slice(start, index + 1));
if (parsed !== undefined) {
payloads.push(parsed);
}
start = -1;
}
}
return payloads;
}
function parseJsonPayloads(text) {
const trimmed = text.trim();
if (!trimmed) {
return [];
}
const parsed = parseJson(trimmed);
if (parsed !== undefined) {
return [parsed];
}
return parseJsonObjectsFromText(trimmed);
}
function textValues(values) {
return values.filter((value) => typeof value === "string" && value.length > 0);
}
function isFailureStatus(value) {
return (
typeof value === "string" &&
["blocked", "canceled", "cancelled", "error", "failed", "failure"].includes(value.toLowerCase())
);
}
function hasFailureSignal(value) {
if (!isRecord(value)) {
return false;
}
return (
value.isError === true ||
value.ok === false ||
isFailureStatus(value.status) ||
isFailureStatus(value.livenessState) ||
(Object.hasOwn(value, "error") && value.error !== null && value.error !== undefined)
);
}
export function extractAgentReplyTexts(text) {
return parseJsonPayloads(text).flatMap((payload) => {
const envelopeFailed =
hasFailureSignal(payload) ||
hasFailureSignal(payload?.meta) ||
hasFailureSignal(payload?.result) ||
hasFailureSignal(payload?.result?.meta);
if (envelopeFailed) {
return [];
}
const payloadEntries = Array.isArray(payload?.payloads)
? payload.payloads
: Array.isArray(payload?.result?.payloads)
? payload.result.payloads
: [];
const directTexts = textValues([
payload?.finalAssistantVisibleText,
payload?.finalAssistantRawText,
payload?.meta?.finalAssistantVisibleText,
payload?.meta?.finalAssistantRawText,
payload?.result?.finalAssistantVisibleText,
payload?.result?.finalAssistantRawText,
payload?.result?.meta?.finalAssistantVisibleText,
payload?.result?.meta?.finalAssistantRawText,
]);
const payloadTexts = payloadEntries.flatMap((entry) =>
entry?.isError !== true && typeof entry?.text === "string" && entry.text.length > 0
? [entry.text]
: [],
);
return directTexts.concat(payloadTexts);
});
}
export function assertAgentReplyContainsMarker(marker, outputPath) {
const output = readTextFileTail(outputPath, OUTPUT_SCAN_TAIL_BYTES);
const replyTexts = extractAgentReplyTexts(output);
if (replyTexts.some((text) => text.includes(marker))) {
return;
}
const outputTail = tailText(output, ERROR_DETAIL_TAIL_BYTES);
throw new Error(
`agent reply payload did not contain marker ${marker}. Reply payload summary: ${summarizeReplyTexts(replyTexts)}. Output tail: ${outputTail}`,
);
}
export function assertOpenAiRequestLogUsed(requestLogPath, label = "mock OpenAI server") {
if (textFileContains(requestLogPath, OPENAI_REQUEST_PATH_PATTERN)) {
return;
}
const requestLogTail = readTextFileTail(requestLogPath, ERROR_DETAIL_TAIL_BYTES);
throw new Error(`${label} was not used. Request log tail: ${requestLogTail}`);
}