mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-06 05:51:15 +08:00
fix(e2e): bound clickclack fixture bodies
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user