diff --git a/scripts/e2e/openwebui-probe.mjs b/scripts/e2e/openwebui-probe.mjs index f173b8f73df8..99ef9161e1b7 100644 --- a/scripts/e2e/openwebui-probe.mjs +++ b/scripts/e2e/openwebui-probe.mjs @@ -7,14 +7,15 @@ const email = process.env.OPENWEBUI_ADMIN_EMAIL ?? ""; const password = process.env.OPENWEBUI_ADMIN_PASSWORD ?? ""; const expectedNonce = process.env.OPENWEBUI_EXPECTED_NONCE ?? ""; const prompt = process.env.OPENWEBUI_PROMPT ?? ""; +const MAX_TIMER_TIMEOUT_MS = 2_147_000_000; const modelAttempts = readPositiveInt("OPENWEBUI_MODEL_ATTEMPTS", 72); -const modelRetryMs = readNonNegativeInt("OPENWEBUI_MODEL_RETRY_MS", 5000); -const fetchTimeoutMs = readPositiveInt("OPENWEBUI_FETCH_TIMEOUT_MS", 720000); -const controlTimeoutMs = readPositiveInt( +const modelRetryMs = readNonNegativeTimerMs("OPENWEBUI_MODEL_RETRY_MS", 5000); +const fetchTimeoutMs = readPositiveTimerMs("OPENWEBUI_FETCH_TIMEOUT_MS", 720000); +const controlTimeoutMs = readPositiveTimerMs( "OPENWEBUI_CONTROL_TIMEOUT_MS", Math.min(fetchTimeoutMs, 30000), ); -const chatTimeoutMs = readPositiveInt("OPENWEBUI_CHAT_TIMEOUT_MS", fetchTimeoutMs); +const chatTimeoutMs = readPositiveTimerMs("OPENWEBUI_CHAT_TIMEOUT_MS", fetchTimeoutMs); const responseBodyMaxBytes = readPositiveInt("OPENWEBUI_RESPONSE_BODY_MAX_BYTES", 1024 * 1024); const smokeMode = process.env.OPENWEBUI_SMOKE_MODE ?? process.env.OPENCLAW_OPENWEBUI_SMOKE_MODE ?? "chat"; @@ -65,6 +66,20 @@ function readNonNegativeInt(name, fallback) { return parsed; } +function clampTimerTimeoutMs(valueMs, minMs = 1) { + const min = Math.max(0, Math.floor(minMs)); + const value = Number.isFinite(valueMs) ? valueMs : min; + return Math.min(Math.max(Math.floor(value), min), MAX_TIMER_TIMEOUT_MS); +} + +function readPositiveTimerMs(name, fallback) { + return clampTimerTimeoutMs(readPositiveInt(name, fallback)); +} + +function readNonNegativeTimerMs(name, fallback) { + return clampTimerTimeoutMs(readNonNegativeInt(name, fallback), 0); +} + function createTimeoutError(label, timeoutMs) { const error = new Error(`${label} timed out after ${timeoutMs}ms`); error.code = "ETIMEDOUT"; @@ -72,14 +87,15 @@ function createTimeoutError(label, timeoutMs) { } async function withRequestTimeout(label, timeoutMs, run) { + const resolvedTimeoutMs = clampTimerTimeoutMs(timeoutMs); const controller = new AbortController(); - const timeoutError = createTimeoutError(label, timeoutMs); + const timeoutError = createTimeoutError(label, resolvedTimeoutMs); let timer; const timeoutPromise = new Promise((_, reject) => { timer = setTimeout(() => { controller.abort(timeoutError); reject(timeoutError); - }, timeoutMs); + }, resolvedTimeoutMs); timer.unref?.(); }); try { @@ -138,7 +154,7 @@ function buildAuthHeaders(token, cookie) { function sleep(ms) { return new Promise((resolve) => { - setTimeout(resolve, ms); + setTimeout(resolve, clampTimerTimeoutMs(ms, 0)); }); } diff --git a/test/e2e/qa-lab/runtime/openwebui-probe.e2e.test.ts b/test/e2e/qa-lab/runtime/openwebui-probe.e2e.test.ts index 188bfea6934c..ab36ddaf5714 100644 --- a/test/e2e/qa-lab/runtime/openwebui-probe.e2e.test.ts +++ b/test/e2e/qa-lab/runtime/openwebui-probe.e2e.test.ts @@ -4,6 +4,7 @@ import { readFileSync } from "node:fs"; import { createServer, type IncomingMessage, type Server as HttpServer } from "node:http"; import { createServer as createTcpServer, type Server as TcpServer, type Socket } from "node:net"; import path from "node:path"; +import { MAX_TIMER_TIMEOUT_MS } from "@openclaw/normalization-core/number-coercion"; import { describe, expect, it } from "vitest"; import { createBoundedChildOutput } from "../../../helpers/bounded-child-output.js"; @@ -361,11 +362,13 @@ describe("scripts/e2e/openwebui-probe.mjs", () => { it("passes in models mode when Open WebUI exposes the OpenClaw model", async () => { const server = createServer((request, response) => { if (request.url === "/api/v1/auths/signin") { - response.writeHead(200, { - "content-type": "application/json", - "set-cookie": "openwebui-session=test; Path=/", - }); - response.end(JSON.stringify({ token: "test-token" })); + setTimeout(() => { + response.writeHead(200, { + "content-type": "application/json", + "set-cookie": "openwebui-session=test; Path=/", + }); + response.end(JSON.stringify({ token: "test-token" })); + }, 25); return; } if (request.url === "/api/models") { @@ -379,7 +382,10 @@ describe("scripts/e2e/openwebui-probe.mjs", () => { }); const baseUrl = await listen(server); try { - const result = await runProbe(baseUrl); + const result = await runProbe(baseUrl, { + OPENWEBUI_CONTROL_TIMEOUT_MS: String(MAX_TIMER_TIMEOUT_MS + 1), + OPENWEBUI_FETCH_TIMEOUT_MS: String(MAX_TIMER_TIMEOUT_MS + 1), + }); expect(result.status).toBe(0); expect(JSON.parse(result.stdout)).toMatchObject({