fix(scripts): bound Docker E2E JSON helpers

This commit is contained in:
Vincent Koc
2026-06-16 07:30:48 +02:00
parent 325d0208d0
commit 9a0aefb73f
3 changed files with 101 additions and 4 deletions
+26 -1
View File
@@ -5,6 +5,9 @@
import fs from "node:fs";
import { parsePositiveInt } from "./lib/numeric-options.mjs";
const JSON_ARTIFACT_MAX_BYTES_ENV = "OPENCLAW_DOCKER_E2E_JSON_ARTIFACT_MAX_BYTES";
const DEFAULT_JSON_ARTIFACT_MAX_BYTES = 16 * 1024 * 1024;
function usage() {
return "Usage: node scripts/docker-e2e-timings.mjs <summary.json|lane-timings.json> [--limit N]";
}
@@ -41,7 +44,29 @@ function parseArgs(argv) {
}
function readJson(file) {
return JSON.parse(fs.readFileSync(file, "utf8"));
return JSON.parse(readJsonArtifactText(file));
}
function readJsonArtifactText(file) {
const maxBytes = readPositiveIntEnv(JSON_ARTIFACT_MAX_BYTES_ENV, DEFAULT_JSON_ARTIFACT_MAX_BYTES);
const stat = fs.statSync(file);
if (!stat.isFile()) {
throw new Error(`JSON artifact is not a file: ${file}`);
}
if (stat.size > maxBytes) {
throw new Error(`JSON artifact exceeded ${maxBytes} bytes: ${file} (${stat.size} bytes)`);
}
const text = fs.readFileSync(file, "utf8");
const bytes = Buffer.byteLength(text, "utf8");
if (bytes > maxBytes) {
throw new Error(`JSON artifact exceeded ${maxBytes} bytes: ${file} (${bytes} bytes)`);
}
return text;
}
function readPositiveIntEnv(name, fallback) {
const raw = process.env[name];
return raw === undefined || raw === "" ? fallback : parsePositiveInt(raw, name);
}
function seconds(value) {
+27 -1
View File
@@ -2,6 +2,10 @@
// Converts scheduler JSON into GitHub Actions outputs and compact markdown
// summaries so the workflow does not duplicate Docker E2E planning logic.
import fs from "node:fs";
import { parsePositiveInt } from "./lib/numeric-options.mjs";
const JSON_ARTIFACT_MAX_BYTES_ENV = "OPENCLAW_DOCKER_E2E_JSON_ARTIFACT_MAX_BYTES";
const DEFAULT_JSON_ARTIFACT_MAX_BYTES = 16 * 1024 * 1024;
function usage() {
return [
@@ -13,7 +17,29 @@ function usage() {
}
function readJson(file) {
return JSON.parse(fs.readFileSync(file, "utf8"));
return JSON.parse(readJsonArtifactText(file));
}
function readJsonArtifactText(file) {
const maxBytes = readPositiveIntEnv(JSON_ARTIFACT_MAX_BYTES_ENV, DEFAULT_JSON_ARTIFACT_MAX_BYTES);
const stat = fs.statSync(file);
if (!stat.isFile()) {
throw new Error(`JSON artifact is not a file: ${file}`);
}
if (stat.size > maxBytes) {
throw new Error(`JSON artifact exceeded ${maxBytes} bytes: ${file} (${stat.size} bytes)`);
}
const text = fs.readFileSync(file, "utf8");
const bytes = Buffer.byteLength(text, "utf8");
if (bytes > maxBytes) {
throw new Error(`JSON artifact exceeded ${maxBytes} bytes: ${file} (${bytes} bytes)`);
}
return text;
}
function readPositiveIntEnv(name, fallback) {
const raw = process.env[name];
return raw === undefined || raw === "" ? fallback : parsePositiveInt(raw, name);
}
function boolOutput(value) {
+48 -2
View File
@@ -5,14 +5,20 @@ import { tmpdir } from "node:os";
import path from "node:path";
import { describe, expect, it } from "vitest";
function runHelper(script: string, ...args: string[]) {
return spawnSync(process.execPath, [script, ...args], {
function runHelper(script: string, ...args: Array<string | Record<string, string>>) {
const maybeEnv = args.at(-1);
const env =
maybeEnv && typeof maybeEnv === "object"
? (args.pop() as unknown as Record<string, string>)
: {};
return spawnSync(process.execPath, [script, ...(args as string[])], {
cwd: process.cwd(),
encoding: "utf8",
env: {
...process.env,
GH_FORCE_TTY: "0",
NO_COLOR: "1",
...env,
},
});
}
@@ -36,6 +42,26 @@ describe("Docker E2E helper CLIs", () => {
expect(result.stderr).not.toContain("at file:");
});
it("rejects oversized scheduler helper JSON artifacts without a Node stack trace", () => {
const root = mkdtempSync(`${tmpdir()}/openclaw-docker-e2e-helper-`);
try {
const file = path.join(root, "summary.json");
writeFileSync(file, `${JSON.stringify({ filler: "x".repeat(128) })}\n`, "utf8");
const result = runHelper("scripts/docker-e2e.mjs", "failed-reruns", file, {
OPENCLAW_DOCKER_E2E_JSON_ARTIFACT_MAX_BYTES: "64",
});
expect(result.status).toBe(1);
expect(result.stdout).toBe("");
expect(result.stderr).toContain("JSON artifact exceeded 64 bytes");
expect(result.stderr).not.toContain("Error:");
expect(result.stderr).not.toContain("at file:");
} finally {
rmSync(root, { force: true, recursive: true });
}
});
it("prints timings help without treating --help as an artifact path", () => {
const result = runHelper("scripts/docker-e2e-timings.mjs", "--help");
@@ -56,6 +82,26 @@ describe("Docker E2E helper CLIs", () => {
expect(result.stderr).not.toContain("at file:");
});
it("rejects oversized timing JSON artifacts without a Node stack trace", () => {
const root = mkdtempSync(`${tmpdir()}/openclaw-docker-e2e-timings-`);
try {
const file = path.join(root, "summary.json");
writeFileSync(file, `${JSON.stringify({ filler: "x".repeat(128) })}\n`, "utf8");
const result = runHelper("scripts/docker-e2e-timings.mjs", file, {
OPENCLAW_DOCKER_E2E_JSON_ARTIFACT_MAX_BYTES: "64",
});
expect(result.status).toBe(1);
expect(result.stdout).toBe("");
expect(result.stderr).toContain("JSON artifact exceeded 64 bytes");
expect(result.stderr).not.toContain("Error:");
expect(result.stderr).not.toContain("at file:");
} finally {
rmSync(root, { force: true, recursive: true });
}
});
it("rejects missing timings limits without a Node stack trace", () => {
const result = runHelper("scripts/docker-e2e-timings.mjs", "summary.json", "--limit");