From a79c40a789c31f07408be59b5fbdee5c8a916eec Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 24 Apr 2026 09:28:57 +0100 Subject: [PATCH] test: harden docker live readiness --- scripts/e2e/cron-mcp-cleanup-docker.sh | 27 +++----- scripts/e2e/mcp-channels-docker.sh | 27 +++----- scripts/e2e/openwebui-docker.sh | 10 ++- scripts/e2e/openwebui-probe.mjs | 62 ++++++++++++++----- .../gateway-cli-backend.live-helpers.ts | 8 ++- 5 files changed, 79 insertions(+), 55 deletions(-) diff --git a/scripts/e2e/cron-mcp-cleanup-docker.sh b/scripts/e2e/cron-mcp-cleanup-docker.sh index 4051237bebd..d71b645a2d1 100644 --- a/scripts/e2e/cron-mcp-cleanup-docker.sh +++ b/scripts/e2e/cron-mcp-cleanup-docker.sh @@ -52,28 +52,19 @@ docker run --rm \ } trap cleanup_inner EXIT trap dump_gateway_log_on_error ERR - for _ in \$(seq 1 80); do - if node --input-type=module -e ' - import net from \"node:net\"; - const socket = net.createConnection({ host: \"127.0.0.1\", port: $PORT }); - const timeout = setTimeout(() => { - socket.destroy(); - process.exit(1); - }, 400); - socket.on(\"connect\", () => { - clearTimeout(timeout); - socket.end(); - process.exit(0); - }); - socket.on(\"error\", () => { - clearTimeout(timeout); - process.exit(1); - }); - ' >/dev/null 2>&1; then + gateway_ready=0 + for _ in \$(seq 1 160); do + if grep -q '\[gateway\] ready' /tmp/cron-mcp-cleanup-gateway.log 2>/dev/null; then + gateway_ready=1 break fi sleep 0.25 done + if [ \"\$gateway_ready\" -ne 1 ]; then + echo \"Gateway did not become ready\" + tail -n 120 /tmp/cron-mcp-cleanup-gateway.log 2>/dev/null || true + exit 1 + fi node --import tsx scripts/e2e/cron-mcp-cleanup-docker-client.ts " >"$CLIENT_LOG" 2>&1 status=${PIPESTATUS[0]} diff --git a/scripts/e2e/mcp-channels-docker.sh b/scripts/e2e/mcp-channels-docker.sh index 79169e6ada7..125024df725 100644 --- a/scripts/e2e/mcp-channels-docker.sh +++ b/scripts/e2e/mcp-channels-docker.sh @@ -52,28 +52,19 @@ docker run --rm \ } trap cleanup_inner EXIT trap dump_gateway_log_on_error ERR - for _ in \$(seq 1 80); do - if node --input-type=module -e ' - import net from \"node:net\"; - const socket = net.createConnection({ host: \"127.0.0.1\", port: $PORT }); - const timeout = setTimeout(() => { - socket.destroy(); - process.exit(1); - }, 400); - socket.on(\"connect\", () => { - clearTimeout(timeout); - socket.end(); - process.exit(0); - }); - socket.on(\"error\", () => { - clearTimeout(timeout); - process.exit(1); - }); - ' >/dev/null 2>&1; then + gateway_ready=0 + for _ in \$(seq 1 160); do + if grep -q '\[gateway\] ready' /tmp/mcp-channels-gateway.log 2>/dev/null; then + gateway_ready=1 break fi sleep 0.25 done + if [ \"\$gateway_ready\" -ne 1 ]; then + echo \"Gateway did not become ready\" + tail -n 120 /tmp/mcp-channels-gateway.log 2>/dev/null || true + exit 1 + fi node --import tsx scripts/e2e/mcp-channels-docker-client.ts " >"$CLIENT_LOG" 2>&1 status=${PIPESTATUS[0]} diff --git a/scripts/e2e/openwebui-docker.sh b/scripts/e2e/openwebui-docker.sh index d757f3700df..66764c1fb37 100755 --- a/scripts/e2e/openwebui-docker.sh +++ b/scripts/e2e/openwebui-docker.sh @@ -54,6 +54,10 @@ docker run -d \ --network "$NET_NAME" \ -e "OPENCLAW_GATEWAY_TOKEN=$TOKEN" \ -e "OPENCLAW_OPENWEBUI_MODEL=$MODEL" \ + -e "OPENCLAW_SKIP_CHANNELS=1" \ + -e "OPENCLAW_SKIP_GMAIL_WATCHER=1" \ + -e "OPENCLAW_SKIP_CRON=1" \ + -e "OPENCLAW_SKIP_CANVAS_HOST=1" \ -e OPENAI_API_KEY \ ${OPENAI_BASE_URL_VALUE:+-e OPENAI_BASE_URL} \ "$IMAGE_NAME" \ @@ -110,7 +114,7 @@ EOF echo "Waiting for gateway HTTP surface..." gateway_ready=0 -for _ in $(seq 1 60); do +for _ in $(seq 1 240); do if [ "$(docker inspect -f '{{.State.Running}}' "$GW_NAME" 2>/dev/null || echo false)" != "true" ]; then break fi @@ -128,6 +132,10 @@ done if [ "$gateway_ready" -ne 1 ]; then echo "Gateway failed to start" + docker inspect "$GW_NAME" --format '{{json .State}}' 2>/dev/null || true + if [ "$(docker inspect -f '{{.State.Running}}' "$GW_NAME" 2>/dev/null || echo false)" = "true" ]; then + docker exec "$GW_NAME" bash -lc 'tail -n 200 /tmp/openwebui-gateway.log' || true + fi docker logs "$GW_NAME" 2>&1 | tail -n 200 || true exit 1 fi diff --git a/scripts/e2e/openwebui-probe.mjs b/scripts/e2e/openwebui-probe.mjs index e7c7b5499c6..30195715a90 100644 --- a/scripts/e2e/openwebui-probe.mjs +++ b/scripts/e2e/openwebui-probe.mjs @@ -31,6 +31,25 @@ function buildAuthHeaders(token, cookie) { return headers; } +function sleep(ms) { + return new Promise((resolve) => { + setTimeout(resolve, ms); + }); +} + +function extractModelIds(modelsJson) { + const models = Array.isArray(modelsJson) + ? modelsJson + : Array.isArray(modelsJson?.data) + ? modelsJson.data + : Array.isArray(modelsJson?.models) + ? modelsJson.models + : []; + return models + .map((entry) => entry?.id ?? entry?.model ?? entry?.name) + .filter((value) => typeof value === "string"); +} + const signinRes = await fetch(`${baseUrl}/api/v1/auths/signin`, { method: "POST", headers: { "content-type": "application/json" }, @@ -50,25 +69,34 @@ const authHeaders = { accept: "application/json", }; -const modelsRes = await fetch(`${baseUrl}/api/models`, { headers: authHeaders }); -if (!modelsRes.ok) { - throw new Error(`/api/models failed: HTTP ${modelsRes.status} ${await modelsRes.text()}`); +let modelIds = []; +let targetModel = ""; +let lastModelsError = ""; +for (let attempt = 1; attempt <= 24; attempt += 1) { + const modelsRes = await fetch(`${baseUrl}/api/models`, { headers: authHeaders }).catch( + (error) => { + lastModelsError = error instanceof Error ? error.message : String(error); + return undefined; + }, + ); + if (modelsRes?.ok) { + const modelsJson = await modelsRes.json(); + modelIds = extractModelIds(modelsJson); + targetModel = + modelIds.find((id) => id === "openclaw/default") ?? modelIds.find((id) => id === "openclaw"); + if (targetModel) { + break; + } + lastModelsError = `missing openclaw model: ${JSON.stringify(modelIds)}`; + } else if (modelsRes) { + lastModelsError = `HTTP ${modelsRes.status} ${await modelsRes.text()}`; + } + await sleep(5_000); } -const modelsJson = await modelsRes.json(); -const models = Array.isArray(modelsJson) - ? modelsJson - : Array.isArray(modelsJson?.data) - ? modelsJson.data - : Array.isArray(modelsJson?.models) - ? modelsJson.models - : []; -const modelIds = models - .map((entry) => entry?.id ?? entry?.model ?? entry?.name) - .filter((value) => typeof value === "string"); -const targetModel = - modelIds.find((id) => id === "openclaw/default") ?? modelIds.find((id) => id === "openclaw"); if (!targetModel) { - throw new Error(`openclaw model missing from Open WebUI model list: ${JSON.stringify(modelIds)}`); + throw new Error( + `openclaw model missing from Open WebUI model list after retry: ${JSON.stringify(modelIds)} (${lastModelsError})`, + ); } const chatRes = await fetch(`${baseUrl}/api/chat/completions`, { diff --git a/src/gateway/gateway-cli-backend.live-helpers.ts b/src/gateway/gateway-cli-backend.live-helpers.ts index 72dbe554e63..347d22a1587 100644 --- a/src/gateway/gateway-cli-backend.live-helpers.ts +++ b/src/gateway/gateway-cli-backend.live-helpers.ts @@ -136,7 +136,13 @@ export function shouldRunCliModelSwitchProbe(providerId: string, modelRef: strin export function matchesCliBackendReply(text: string, expected: string): boolean { const normalized = text.trim(); const target = expected.trim(); - return normalized === target || normalized === target.slice(0, -1); + const targetWithoutPeriod = target.slice(0, -1); + return ( + normalized === target || + normalized === targetWithoutPeriod || + normalized.includes(target) || + normalized.includes(targetWithoutPeriod) + ); } export function withClaudeMcpConfigOverrides(args: string[], mcpConfigPath: string): string[] {