fix(e2e): bound clickclack fixture bodies

This commit is contained in:
Vincent Koc
2026-06-03 12:45:49 +02:00
parent 9947a26768
commit 1b5cb4a0d3
3 changed files with 141 additions and 4 deletions

View File

@@ -61,6 +61,7 @@ Docs: https://docs.openclaw.ai
- Release/CI/E2E: bound mock OpenAI readiness probes in web-search and Telegram RTT Docker smokes so stalled HTTP accepts cannot hang cleanup or fall through.
- Tooling: cancel oversized pnpm audit advisory responses before failing so registry error paths do not leave response bodies open.
- Release/CI/E2E: stop tracked gateway and mock service process groups so descendant helpers do not survive E2E cleanup.
- Release/CI/E2E: reject oversized ClickClack fixture request bodies before release journey smokes can accumulate unbounded payloads.
- Release/CI/E2E: fail secret-provider proof runs when temporary state cleanup still fails after retries instead of hiding the cleanup error.
- Release/CI/E2E: fail package-candidate ref proofs when temporary source worktree cleanup fails instead of leaving stale worktrees behind.
- Release/CI/E2E: remove package tarball extract directories when tar extraction fails before validation can continue.

View File

@@ -4,6 +4,7 @@ import http from "node:http";
import { readPositiveIntEnv } from "../env-limits.mjs";
const port = readPositiveIntEnv("CLICKCLACK_FIXTURE_PORT", 44181);
const requestMaxBytes = readPositiveIntEnv("CLICKCLACK_FIXTURE_REQUEST_MAX_BYTES", 4 * 1024 * 1024);
const token = process.env.CLICKCLACK_FIXTURE_TOKEN ?? "clickclack-release-token";
const statePath = process.env.CLICKCLACK_FIXTURE_STATE ?? "/tmp/openclaw-clickclack-fixture.json";
const workspace = {
@@ -86,21 +87,65 @@ function checkAuth(req, res) {
function readBody(req) {
return new Promise((resolve, reject) => {
let body = "";
let bytes = 0;
let settled = false;
req.setEncoding("utf8");
req.on("data", (chunk) => {
if (settled) {
return;
}
bytes += Buffer.byteLength(chunk, "utf8");
if (bytes > requestMaxBytes) {
settled = true;
body = "";
req.resume();
reject(requestBodyTooLargeError());
return;
}
body += chunk;
});
req.on("end", () => {
if (settled) {
return;
}
settled = true;
try {
resolve(body ? JSON.parse(body) : {});
} catch {
resolve({});
}
});
req.on("error", reject);
req.on("error", (error) => {
if (!settled) {
settled = true;
reject(error instanceof Error ? error : new Error(String(error)));
}
});
});
}
function requestBodyTooLargeError() {
return Object.assign(new Error(`ClickClack fixture request body exceeded ${requestMaxBytes} bytes`), {
code: "ETOOBIG",
});
}
function isRequestBodyTooLargeError(error) {
return error instanceof Error && error.code === "ETOOBIG";
}
function handleRequestError(res, error) {
if (res.headersSent) {
res.destroy();
return;
}
if (isRequestBodyTooLargeError(error)) {
json(res, 413, { error: error.message });
return;
}
json(res, 500, { error: String(error instanceof Error ? error.message : error) });
}
function createMessage({ body, author = humanUser, parentMessageId }) {
messageSeq += 1;
const id = `msg_${messageSeq}`;
@@ -171,8 +216,8 @@ function broadcast(event) {
}
}
const server = http.createServer((req, res) => {
void (async () => {
async function handleRequest(req, res) {
try {
const url = new URL(req.url ?? "/", "http://127.0.0.1");
if (!checkAuth(req, res)) {
return;
@@ -244,7 +289,13 @@ const server = http.createServer((req, res) => {
return;
}
json(res, 404, { error: `unhandled ${req.method} ${url.pathname}` });
})();
} catch (error) {
handleRequestError(res, error);
}
}
const server = http.createServer((req, res) => {
void handleRequest(req, res);
});
server.on("upgrade", (req, socket) => {

View File

@@ -1,5 +1,8 @@
import { spawn, spawnSync } from "node:child_process";
import fs from "node:fs";
import { createServer, type Server } from "node:http";
import os from "node:os";
import path from "node:path";
import { describe, expect, it } from "vitest";
const browserFixturePath = "scripts/e2e/lib/browser-cdp-snapshot/fixture-server.mjs";
@@ -57,6 +60,45 @@ async function listen(server: Server): Promise<string> {
return `http://127.0.0.1:${address.port}`;
}
async function allocatePort(): Promise<number> {
const server = createServer();
const url = await listen(server);
await new Promise<void>((resolve) => server.close(() => resolve()));
return Number(new URL(url).port);
}
async function waitForOutput(
child: ReturnType<typeof spawn>,
matches: (text: string) => boolean,
getOutput: () => string,
): Promise<void> {
const startedAt = Date.now();
while (Date.now() - startedAt < 3_000) {
if (matches(getOutput())) {
return;
}
await new Promise((resolve) => setTimeout(resolve, 20));
}
throw new Error(`timed out waiting for fixture output. Output: ${getOutput()}`);
}
async function stopChild(child: ReturnType<typeof spawn>): Promise<void> {
if (child.exitCode !== null || child.signalCode !== null) {
return;
}
child.kill("SIGTERM");
await new Promise<void>((resolve) => {
const timer = setTimeout(() => {
child.kill("SIGKILL");
resolve();
}, 1_000);
child.once("exit", () => {
clearTimeout(timer);
resolve();
});
});
}
describe("e2e helper numeric env limits", () => {
it("rejects loose Browser CDP fixture ports", async () => {
const result = await runScriptAsync(browserFixturePath, [], { FIXTURE_PORT: "18080http" });
@@ -74,6 +116,49 @@ describe("e2e helper numeric env limits", () => {
expect(result.stderr).toContain("invalid CLICKCLACK_FIXTURE_PORT: 44181tcp");
});
it("rejects oversized ClickClack fixture request bodies", async () => {
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-clickclack-fixture-"));
const port = await allocatePort();
const child = spawn(process.execPath, [clickclackFixturePath], {
env: {
...process.env,
CLICKCLACK_FIXTURE_PORT: String(port),
CLICKCLACK_FIXTURE_REQUEST_MAX_BYTES: "16",
CLICKCLACK_FIXTURE_STATE: path.join(tempDir, "state.json"),
},
stdio: ["ignore", "pipe", "pipe"],
});
let output = "";
child.stdout.setEncoding("utf8");
child.stderr.setEncoding("utf8");
child.stdout.on("data", (chunk) => {
output += chunk;
});
child.stderr.on("data", (chunk) => {
output += chunk;
});
try {
await waitForOutput(
child,
(text) => text.includes(`clickclack fixture listening on ${port}`),
() => output,
);
const response = await fetch(`http://127.0.0.1:${port}/fixture/inbound`, {
body: JSON.stringify({ body: "x".repeat(64) }),
headers: { "content-type": "application/json" },
method: "POST",
});
const body = await response.json();
expect(response.status).toBe(413);
expect(body).toEqual({ error: "ClickClack fixture request body exceeded 16 bytes" });
} finally {
await stopChild(child);
fs.rmSync(tempDir, { force: true, recursive: true });
}
});
it("rejects loose Open WebUI HTTP probe timeouts", () => {
const result = runScript(httpProbePath, ["http://127.0.0.1:9"], {
OPENCLAW_HTTP_PROBE_TIMEOUT_MS: "8000ms",