diff --git a/scripts/docker-e2e-rerun.mjs b/scripts/docker-e2e-rerun.mjs index bb4d92c147de..2a5833e75d92 100644 --- a/scripts/docker-e2e-rerun.mjs +++ b/scripts/docker-e2e-rerun.mjs @@ -8,6 +8,7 @@ import { spawnSync } from "node:child_process"; import fs from "node:fs"; import os from "node:os"; import path from "node:path"; +import { readDockerE2eJsonArtifact } from "./lib/docker-e2e-json-artifacts.mjs"; const DEFAULT_WORKFLOW = "openclaw-live-and-e2e-checks-reusable.yml"; @@ -76,7 +77,7 @@ function run(command, args, options = {}) { } function readJson(file) { - return JSON.parse(fs.readFileSync(file, "utf8")); + return readDockerE2eJsonArtifact(file); } function shellQuote(value) { diff --git a/scripts/docker-e2e-timings.mjs b/scripts/docker-e2e-timings.mjs index 968cddaf1dfd..96487821c770 100644 --- a/scripts/docker-e2e-timings.mjs +++ b/scripts/docker-e2e-timings.mjs @@ -2,12 +2,9 @@ // Summarizes Docker E2E timing artifacts. // Accepts scheduler summary.json or lane-timings.json so agents can see the // slowest lanes and phase critical path before deciding what to rerun. -import fs from "node:fs"; +import { readDockerE2eJsonArtifact } from "./lib/docker-e2e-json-artifacts.mjs"; 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 [--limit N]"; } @@ -44,29 +41,7 @@ function parseArgs(argv) { } function readJson(file) { - 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); + return readDockerE2eJsonArtifact(file); } function seconds(value) { diff --git a/scripts/docker-e2e.mjs b/scripts/docker-e2e.mjs index df3f409bc334..f67db1c8b350 100644 --- a/scripts/docker-e2e.mjs +++ b/scripts/docker-e2e.mjs @@ -1,11 +1,7 @@ // Docker E2E CI helper. // 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; +import { readDockerE2eJsonArtifact } from "./lib/docker-e2e-json-artifacts.mjs"; function usage() { return [ @@ -17,29 +13,7 @@ function usage() { } function readJson(file) { - 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); + return readDockerE2eJsonArtifact(file); } function boolOutput(value) { diff --git a/scripts/lib/docker-e2e-json-artifacts.mjs b/scripts/lib/docker-e2e-json-artifacts.mjs new file mode 100644 index 000000000000..a62ce03e177b --- /dev/null +++ b/scripts/lib/docker-e2e-json-artifacts.mjs @@ -0,0 +1,31 @@ +import fs from "node:fs"; +import { parsePositiveInt } from "./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; + +export function readDockerE2eJsonArtifact(file) { + return JSON.parse(readDockerE2eJsonArtifactText(file)); +} + +function readDockerE2eJsonArtifactText(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); +} diff --git a/test/scripts/docker-e2e-helper-cli.test.ts b/test/scripts/docker-e2e-helper-cli.test.ts index c2db152c83a5..232db1477a9e 100644 --- a/test/scripts/docker-e2e-helper-cli.test.ts +++ b/test/scripts/docker-e2e-helper-cli.test.ts @@ -122,6 +122,26 @@ describe("Docker E2E helper CLIs", () => { ); }); + it("rejects oversized rerun JSON artifacts without a Node stack trace", () => { + const root = mkdtempSync(`${tmpdir()}/openclaw-docker-e2e-rerun-`); + try { + const file = path.join(root, "summary.json"); + writeFileSync(file, `${JSON.stringify({ filler: "x".repeat(128) })}\n`, "utf8"); + + const result = runHelper("scripts/docker-e2e-rerun.mjs", file, "--ref", "abc123", { + 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.each(["summary.json", "failures.json"])( "prints local cleanup reruns without synthesizing Docker lane reruns from %s", (fileName) => {