fix(e2e): bound release fixture response bodies

This commit is contained in:
Vincent Koc
2026-05-28 21:31:38 +02:00
parent 8fbdfc0a76
commit 605e2976ed
2 changed files with 98 additions and 2 deletions

View File

@@ -11,6 +11,10 @@ const CLICKCLACK_HTTP_TIMEOUT_MS = readPositiveInt(
process.env.OPENCLAW_RELEASE_USER_JOURNEY_HTTP_TIMEOUT_MS,
5000,
);
const CLICKCLACK_HTTP_BODY_MAX_BYTES = readPositiveInt(
process.env.OPENCLAW_RELEASE_USER_JOURNEY_HTTP_BODY_MAX_BYTES,
1024 * 1024,
);
function readJson(file) {
return JSON.parse(fs.readFileSync(file, "utf8"));
@@ -38,6 +42,55 @@ async function withClickClackFixtureResponse(url, init, consume, options = {}) {
}
}
function bodyTooLargeError(label, byteLimit) {
return Object.assign(new Error(`${label} response body exceeded ${byteLimit} bytes`), {
code: "ETOOBIG",
});
}
async function readBoundedResponseText(
response,
label,
byteLimit = CLICKCLACK_HTTP_BODY_MAX_BYTES,
) {
const contentLength = response.headers.get("content-length");
if (contentLength) {
const parsedLength = Number(contentLength);
if (Number.isSafeInteger(parsedLength) && parsedLength > byteLimit) {
await response.body?.cancel().catch(() => {});
throw bodyTooLargeError(label, byteLimit);
}
}
if (!response.body) {
return "";
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
let byteCount = 0;
let text = "";
try {
while (true) {
const { done, value } = await reader.read();
if (done) {
return text + decoder.decode();
}
byteCount += value.byteLength;
if (byteCount > byteLimit) {
await reader.cancel().catch(() => {});
throw bodyTooLargeError(label, byteLimit);
}
text += decoder.decode(value, { stream: true });
}
} finally {
reader.releaseLock();
}
}
async function readBoundedResponseJson(response, label) {
return JSON.parse(await readBoundedResponseText(response, label));
}
function resolveHomePath(value) {
if (value === "~") {
return process.env.HOME;
@@ -242,7 +295,8 @@ async function postClickClackInbound() {
body: JSON.stringify({ body }),
},
async (response) => {
assert(response.ok, `fixture inbound failed: ${response.status} ${await response.text()}`);
const text = response.ok ? "" : await readBoundedResponseText(response, "ClickClack inbound");
assert(response.ok, `fixture inbound failed: ${response.status} ${text}`);
},
);
}
@@ -256,7 +310,10 @@ async function waitClickClackSocket() {
const state = await withClickClackFixtureResponse(
`${baseUrl}/fixture/state`,
{},
async (response) => (response.ok ? await response.json() : undefined),
async (response) =>
response.ok
? await readBoundedResponseJson(response, "ClickClack fixture state")
: undefined,
{
timeoutMs: Math.min(CLICKCLACK_HTTP_TIMEOUT_MS, remainingMs),
},

View File

@@ -244,4 +244,43 @@ describe("release user journey assertions", () => {
rmSync(root, { force: true, recursive: true });
}
});
it("bounds ClickClack fixture error response bodies", async () => {
const root = mkdtempSync(path.join(tmpdir(), "openclaw-release-user-assertions-"));
const home = path.join(root, "home");
const portPath = path.join(root, "port.txt");
const server = startTcpFixture(
portPath,
[
"(socket) => {",
' const body = "x".repeat(128);',
" socket.end(`HTTP/1.1 500 Internal Server Error\\r\\nContent-Type: text/plain\\r\\nContent-Length: ${Buffer.byteLength(body)}\\r\\n\\r\\n${body}`);",
"}",
].join("\n"),
);
try {
const port = Number.parseInt((await waitForFile(portPath)).trim(), 10);
const result = runAssertion(
home,
["post-clickclack-inbound", `http://127.0.0.1:${port}`, "hello"],
{
env: {
OPENCLAW_RELEASE_USER_JOURNEY_HTTP_BODY_MAX_BYTES: "16",
OPENCLAW_RELEASE_USER_JOURNEY_HTTP_TIMEOUT_MS: "1000",
},
timeoutMs: 2500,
},
);
expect(result.error).toBeUndefined();
expect(result.signal).not.toBe("SIGKILL");
expect(result.status).not.toBe(0);
expect(result.stderr).toContain("ClickClack inbound response body exceeded 16 bytes");
expect(result.stderr).not.toContain("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx");
} finally {
await stopChild(server);
rmSync(root, { force: true, recursive: true });
}
});
});