fix(e2e): stream installer session scans

This commit is contained in:
Vincent Koc
2026-06-07 11:28:21 +02:00
parent c2d825ae53
commit dcba17d019
2 changed files with 143 additions and 14 deletions
+123 -14
View File
@@ -496,9 +496,11 @@ const fs = require("node:fs");
const jsonl = process.argv[2];
const required = new Set(process.argv.slice(3));
const raw = fs.readFileSync(jsonl, "utf8");
const lines = raw.split("\n").map((l) => l.trim()).filter(Boolean);
const seen = new Set();
const head = [];
let scannedBytes = 0;
let truncated = false;
let skippedOversizedLines = 0;
const toolTypes = new Set([
"tool_use",
@@ -511,10 +513,30 @@ const toolTypes = new Set([
"toolresult",
"tool-result",
]);
function walk(node, parent) {
function readPositiveIntEnv(name, fallback) {
const raw = process.env[name];
if (raw === undefined || raw === "") return fallback;
if (!/^[1-9][0-9]*$/.test(raw)) {
console.error(`${name} must be a positive integer`);
process.exit(2);
}
return Number(raw);
}
const maxBytes = readPositiveIntEnv("OPENCLAW_INSTALL_E2E_SESSION_SCAN_BYTES", 16 * 1024 * 1024);
const maxLineBytes = readPositiveIntEnv("OPENCLAW_INSTALL_E2E_SESSION_LINE_BYTES", 1024 * 1024);
const maxDepth = readPositiveIntEnv("OPENCLAW_INSTALL_E2E_SESSION_SCAN_DEPTH", 64);
const maxNodes = readPositiveIntEnv("OPENCLAW_INSTALL_E2E_SESSION_SCAN_NODES", 100000);
function missingTools() {
return [...required].filter((t) => !seen.has(t));
}
function walk(node, depth, state) {
if (!node) return;
if (depth > maxDepth || state.nodes >= maxNodes) return;
state.nodes += 1;
if (Array.isArray(node)) {
for (const item of node) walk(item, node);
for (const item of node) walk(item, depth + 1, state);
return;
}
if (typeof node !== "object") return;
@@ -542,26 +564,113 @@ function walk(node, parent) {
}
}
if (obj.function && typeof obj.function.name === "string") seen.add(obj.function.name);
for (const v of Object.values(obj)) walk(v, obj);
for (const v of Object.values(obj)) walk(v, depth + 1, state);
}
for (const line of lines) {
function processLine(lineBuffer) {
const line = lineBuffer.toString("utf8").trim();
if (!line) return;
if (head.length < 5) head.push(line.slice(0, maxLineBytes));
try {
const entry = JSON.parse(line);
walk(entry, null);
walk(entry, 0, { nodes: 0 });
} catch {
// ignore unparsable lines
}
}
const missing = [...required].filter((t) => !seen.has(t));
if (missing.length > 0) {
console.error(`Missing tools in transcript: ${missing.join(", ")}`);
console.error(`Seen tools: ${[...seen].sort().join(", ")}`);
console.error("Transcript head:");
console.error(lines.slice(0, 5).join("\n"));
process.exit(1);
async function scan() {
const stream = fs.createReadStream(jsonl, { highWaterMark: 64 * 1024 });
let lineParts = [];
let lineBytes = 0;
let skippingLine = false;
function resetLine() {
lineParts = [];
lineBytes = 0;
skippingLine = false;
}
function appendLineChunk(chunk) {
if (skippingLine) return;
if (lineBytes + chunk.length > maxLineBytes) {
skippedOversizedLines += 1;
lineParts = [];
lineBytes = 0;
skippingLine = true;
return;
}
lineParts.push(chunk);
lineBytes += chunk.length;
}
function finishLine() {
if (!skippingLine && lineBytes > 0) {
processLine(Buffer.concat(lineParts, lineBytes));
}
resetLine();
}
for await (const rawChunk of stream) {
let chunk = rawChunk;
let stopAfterChunk = false;
if (scannedBytes + rawChunk.length > maxBytes) {
const remaining = Math.max(0, maxBytes - scannedBytes);
chunk = rawChunk.subarray(0, remaining);
truncated = true;
stopAfterChunk = true;
}
scannedBytes += chunk.length;
let offset = 0;
while (offset < chunk.length) {
const newline = chunk.indexOf(10, offset);
const end = newline === -1 ? chunk.length : newline;
if (end > offset) {
appendLineChunk(chunk.subarray(offset, end));
}
if (newline === -1) {
break;
}
finishLine();
if (missingTools().length === 0) {
stream.destroy();
return;
}
offset = newline + 1;
}
if (stopAfterChunk) {
stream.destroy();
break;
}
}
if (!truncated && lineBytes > 0) {
finishLine();
}
}
scan()
.then(() => {
const missing = missingTools();
if (missing.length > 0) {
console.error(`Missing tools in transcript: ${missing.join(", ")}`);
console.error(`Seen tools: ${[...seen].sort().join(", ")}`);
if (truncated) {
console.error(`Transcript scan stopped after ${maxBytes} bytes`);
}
if (skippedOversizedLines > 0) {
console.error(`Skipped ${skippedOversizedLines} oversized transcript line(s)`);
}
console.error("Transcript head:");
console.error(head.join("\n"));
process.exit(1);
}
})
.catch((error) => {
console.error(
`Could not scan transcript ${jsonl}: ${error instanceof Error ? error.message : String(error)}`,
);
process.exit(1);
});
NODE
}
+20
View File
@@ -2820,6 +2820,26 @@ output="$(cat "$sampler_log")"
expect(runner).toContain('TURN4_SESSION_ID="${SESSION_ID_PREFIX}-image-write"');
});
it("bounds installer E2E session transcript tool scans", () => {
const runner = readFileSync(INSTALL_E2E_RUNNER_PATH, "utf8");
const start = runner.indexOf("assert_session_used_tools() {");
const end = runner.indexOf("\nsession_jsonl_path()", start);
const helper = runner.slice(start, end);
expect(start).toBeGreaterThanOrEqual(0);
expect(end).toBeGreaterThan(start);
expect(helper).toContain("OPENCLAW_INSTALL_E2E_SESSION_SCAN_BYTES");
expect(helper).toContain("OPENCLAW_INSTALL_E2E_SESSION_LINE_BYTES");
expect(helper).toContain("OPENCLAW_INSTALL_E2E_SESSION_SCAN_DEPTH");
expect(helper).toContain("OPENCLAW_INSTALL_E2E_SESSION_SCAN_NODES");
expect(helper).toContain("fs.createReadStream");
expect(helper).toContain("Buffer.concat");
expect(helper).toContain("skippedOversizedLines");
expect(helper).not.toContain('require("node:readline")');
expect(helper).not.toContain("fs.readFileSync");
expect(helper).not.toContain('.split("\\n")');
});
it("keeps OpenAI web search smoke on one gateway agent connection", () => {
const runner = readFileSync(OPENAI_WEB_SEARCH_MINIMAL_E2E_PATH, "utf8");
const scenario = readFileSync(OPENAI_WEB_SEARCH_MINIMAL_SCENARIO_PATH, "utf8");