diff --git a/scripts/e2e/lib/browser-cdp-snapshot/assert-snapshot.mjs b/scripts/e2e/lib/browser-cdp-snapshot/assert-snapshot.mjs index fbc2f03cbf0..5c5fd7d23e1 100644 --- a/scripts/e2e/lib/browser-cdp-snapshot/assert-snapshot.mjs +++ b/scripts/e2e/lib/browser-cdp-snapshot/assert-snapshot.mjs @@ -1,8 +1,57 @@ // Assertions for browser CDP snapshot E2E fixtures. import fs from "node:fs"; +const DEFAULT_SNAPSHOT_MAX_BYTES = 512 * 1024; +const SNAPSHOT_DIAGNOSTIC_MAX_BYTES = 32 * 1024; const snapshotPath = process.argv[2] ?? "/tmp/browser-cdp-snapshot.txt"; -const snapshot = fs.readFileSync(snapshotPath, "utf8"); + +function readPositiveIntEnv(name, fallback) { + const raw = process.env[name]; + if (raw === undefined || raw === "") { + return fallback; + } + const text = raw.trim(); + if (!/^\d+$/u.test(text)) { + throw new Error(`${name} must be a positive integer; got: ${raw}`); + } + const parsed = Number(text); + if (!Number.isSafeInteger(parsed) || parsed <= 0) { + throw new Error(`${name} must be a positive integer; got: ${raw}`); + } + return parsed; +} + +function readBoundedSnapshot(file, maxBytes) { + const stats = fs.statSync(file); + if (!stats.isFile()) { + throw new Error(`${file} is not a file`); + } + if (stats.size > maxBytes) { + throw new Error(`browser CDP snapshot exceeded ${maxBytes} bytes: ${stats.size} bytes`); + } + const snapshot = fs.readFileSync(file, "utf8"); + const bytes = Buffer.byteLength(snapshot, "utf8"); + if (bytes > maxBytes) { + throw new Error(`browser CDP snapshot exceeded ${maxBytes} bytes: ${bytes} bytes`); + } + return snapshot; +} + +function snapshotDiagnostic(snapshot) { + const buffer = Buffer.from(snapshot, "utf8"); + if (buffer.byteLength <= SNAPSHOT_DIAGNOSTIC_MAX_BYTES) { + return snapshot; + } + return `[truncated snapshot diagnostic to ${SNAPSHOT_DIAGNOSTIC_MAX_BYTES} bytes]\n${buffer + .subarray(buffer.byteLength - SNAPSHOT_DIAGNOSTIC_MAX_BYTES) + .toString("utf8")}`; +} + +const snapshotMaxBytes = readPositiveIntEnv( + "OPENCLAW_BROWSER_CDP_SNAPSHOT_MAX_BYTES", + DEFAULT_SNAPSHOT_MAX_BYTES, +); +const snapshot = readBoundedSnapshot(snapshotPath, snapshotMaxBytes); for (const needle of [ 'button "Save"', @@ -14,7 +63,7 @@ for (const needle of [ 'button "Inside"', ]) { if (!snapshot.includes(needle)) { - console.error(snapshot); + console.error(snapshotDiagnostic(snapshot)); throw new Error(`missing snapshot needle: ${needle}`); } } diff --git a/test/scripts/browser-cdp-snapshot.test.ts b/test/scripts/browser-cdp-snapshot.test.ts new file mode 100644 index 00000000000..2df0c2a7442 --- /dev/null +++ b/test/scripts/browser-cdp-snapshot.test.ts @@ -0,0 +1,57 @@ +// Browser CDP snapshot tests cover bounded snapshot assertions. +import { spawnSync } from "node:child_process"; +import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import { afterEach, describe, expect, it } from "vitest"; + +const SCRIPT_PATH = "scripts/e2e/lib/browser-cdp-snapshot/assert-snapshot.mjs"; +const tempDirs: string[] = []; + +function makeTempRoot(): string { + const root = mkdtempSync(path.join(tmpdir(), "openclaw-browser-cdp-snapshot-")); + tempDirs.push(root); + return root; +} + +function runAssertSnapshot(snapshotPath: string, env: Record = {}) { + return spawnSync(process.execPath, [SCRIPT_PATH, snapshotPath], { + encoding: "utf8", + env: { ...process.env, ...env }, + }); +} + +afterEach(() => { + for (const dir of tempDirs.splice(0)) { + rmSync(dir, { force: true, recursive: true }); + } +}); + +describe("browser CDP snapshot assertions", () => { + it("rejects oversized snapshots before reading them into diagnostics", () => { + const root = makeTempRoot(); + const snapshotPath = path.join(root, "snapshot.txt"); + writeFileSync(snapshotPath, "x".repeat(33), "utf8"); + + const result = runAssertSnapshot(snapshotPath, { + OPENCLAW_BROWSER_CDP_SNAPSHOT_MAX_BYTES: "32", + }); + + expect(result.status).toBe(1); + expect(result.stderr).toContain("browser CDP snapshot exceeded 32 bytes"); + expect(result.stderr).not.toContain("x".repeat(33)); + }); + + it("bounds missing-needle snapshot diagnostics", () => { + const root = makeTempRoot(); + const snapshotPath = path.join(root, "snapshot.txt"); + writeFileSync(snapshotPath, `${"old snapshot line\n".repeat(6 * 1024)}recent tail`, "utf8"); + + const result = runAssertSnapshot(snapshotPath); + + expect(result.status).toBe(1); + expect(result.stderr).toContain("recent tail"); + expect(result.stderr).toContain("truncated snapshot diagnostic"); + expect(result.stderr.length).toBeLessThan(80 * 1024); + }); +});