diff --git a/scripts/docker-e2e-timings.mjs b/scripts/docker-e2e-timings.mjs index 8c3f557a4d0a..968cddaf1dfd 100644 --- a/scripts/docker-e2e-timings.mjs +++ b/scripts/docker-e2e-timings.mjs @@ -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 [--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) { diff --git a/scripts/docker-e2e.mjs b/scripts/docker-e2e.mjs index 2ce290c2a5c1..df3f409bc334 100644 --- a/scripts/docker-e2e.mjs +++ b/scripts/docker-e2e.mjs @@ -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) { diff --git a/test/scripts/docker-e2e-helper-cli.test.ts b/test/scripts/docker-e2e-helper-cli.test.ts index d56b01e8c9f9..c2db152c83a5 100644 --- a/test/scripts/docker-e2e-helper-cli.test.ts +++ b/test/scripts/docker-e2e-helper-cli.test.ts @@ -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>) { + const maybeEnv = args.at(-1); + const env = + maybeEnv && typeof maybeEnv === "object" + ? (args.pop() as unknown as Record) + : {}; + 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");