From 27dc1bd0fcf9586675804f2aa28e1e30b574eef9 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 6 Apr 2026 19:35:06 +0100 Subject: [PATCH] fix(qa-lab): improve health timeout error message and fix port-free check --- .../qa-lab/src/docker-up.runtime.test.ts | 2 ++ extensions/qa-lab/src/docker-up.runtime.ts | 21 ++++++++++++------- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/extensions/qa-lab/src/docker-up.runtime.test.ts b/extensions/qa-lab/src/docker-up.runtime.test.ts index 61aaa52e24a..b1ba1516e20 100644 --- a/extensions/qa-lab/src/docker-up.runtime.test.ts +++ b/extensions/qa-lab/src/docker-up.runtime.test.ts @@ -35,6 +35,7 @@ describe("runQaDockerUp", () => { expect(calls).toEqual([ "pnpm qa:lab:build @/repo/openclaw", + `docker compose -f ${outputDir}/docker-compose.qa.yml down --remove-orphans @/repo/openclaw`, expect.stringContaining( `docker compose -f ${outputDir}/docker-compose.qa.yml up --build -d @/repo/openclaw`, ), @@ -77,6 +78,7 @@ describe("runQaDockerUp", () => { ); expect(calls).toEqual([ + `docker compose -f ${outputDir}/docker-compose.qa.yml down --remove-orphans @/repo/openclaw`, `docker compose -f ${outputDir}/docker-compose.qa.yml up -d @/repo/openclaw`, ]); } finally { diff --git a/extensions/qa-lab/src/docker-up.runtime.ts b/extensions/qa-lab/src/docker-up.runtime.ts index 250d159ba94..e548c04336f 100644 --- a/extensions/qa-lab/src/docker-up.runtime.ts +++ b/extensions/qa-lab/src/docker-up.runtime.ts @@ -36,7 +36,7 @@ async function isPortFree(port: number) { return await new Promise((resolve) => { const server = createServer(); server.once("error", () => resolve(false)); - server.listen(port, () => { + server.listen(port, "127.0.0.1", () => { server.close(() => resolve(true)); }); }); @@ -112,6 +112,7 @@ async function execCommand(command: string, args: string[], cwd: string) { async function waitForHealth( url: string, deps: { + label?: string; fetchImpl: FetchLike; sleepImpl: (ms: number) => Promise; timeoutMs?: number; @@ -120,7 +121,8 @@ async function waitForHealth( ) { const timeoutMs = deps.timeoutMs ?? 240_000; const pollMs = deps.pollMs ?? 1_000; - const deadline = Date.now() + timeoutMs; + const startMs = Date.now(); + const deadline = startMs + timeoutMs; let lastError: unknown = null; while (Date.now() < deadline) { @@ -136,9 +138,14 @@ async function waitForHealth( await deps.sleepImpl(pollMs); } - throw new Error( - `Timed out waiting for ${url}${lastError ? `: ${describeError(lastError)}` : ""}`, - ); + const elapsedSec = Math.round((Date.now() - startMs) / 1000); + const service = deps.label ?? url; + const lines = [ + `${service} did not become healthy within ${elapsedSec}s (limit ${Math.round(timeoutMs / 1000)}s).`, + lastError ? `Last error: ${describeError(lastError)}` : "", + "Hint: check container logs with `docker compose -f logs` and verify the port is not already in use.", + ]; + throw new Error(lines.filter(Boolean).join("\n")); } export async function runQaDockerUp( @@ -213,8 +220,8 @@ export async function runQaDockerUp( const qaLabUrl = `http://127.0.0.1:${qaLabPort}`; const gatewayUrl = `http://127.0.0.1:${gatewayPort}/`; - await waitForHealth(`${qaLabUrl}/healthz`, { fetchImpl, sleepImpl }); - await waitForHealth(`${gatewayUrl}healthz`, { fetchImpl, sleepImpl }); + await waitForHealth(`${qaLabUrl}/healthz`, { label: "QA Lab", fetchImpl, sleepImpl }); + await waitForHealth(`${gatewayUrl}healthz`, { label: "Gateway", fetchImpl, sleepImpl }); return { outputDir,