From be41b8cbc73c7a7927ec09bac879d2ce7bdeee70 Mon Sep 17 00:00:00 2001 From: Alex Knight Date: Mon, 4 May 2026 18:42:05 +1000 Subject: [PATCH] test: stabilize gateway server shard (#77131) --- CHANGELOG.md | 1 + src/gateway/server-http.request-trace.test.ts | 10 +++-- .../server-network-runtime.e2e.test.ts | 15 ++++++- src/gateway/shared-auth.test-helpers.ts | 41 +++++++++++++++++-- test/vitest/vitest.gateway-server.config.ts | 3 ++ 5 files changed, 63 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9efc8d182d1..c1d228281b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -110,6 +110,7 @@ Docs: https://docs.openclaw.ai - Web search: keep first-class assistant `web_search` auto-detect and configured runtime providers visible when active runtime metadata or the active plugin registry is incomplete. Fixes #77073. Thanks @joeykrug. - Plugins/tools: mark manifest-optional sibling tools as optional even when they come from a shared non-optional factory, so cached/status/MCP metadata keeps opt-in tool policy accurate. Thanks @vincentkoc. - Matrix: keep `streaming.progress.toolProgress` scoped to progress draft mode, so partial and quiet Matrix previews do not lose tool progress unless `streaming.preview.toolProgress` is disabled. Thanks @vincentkoc. +- Gateway/validation: isolate gateway server validation files, ignore unrelated startup logs in request-trace coverage, and fail fast on stuck shared-auth sockets, reducing false main-branch CI failures for contributors. Thanks @amknight. - Channels/streaming: keep `streaming.progress.toolProgress` scoped to progress draft mode, so disabling compact progress lines does not silence partial/block preview tool updates. Thanks @vincentkoc. - Plugins/update: treat OpenClaw stable correction versions like `2026.5.3-1` as stable releases for npm installs, plugin updates, and bundled-version comparisons, so `latest` can advance official plugins without prerelease opt-in. Thanks @vincentkoc. - Control UI: point the Appearance tweakcn browse action and docs at the live tweakcn editor route instead of the removed `/themes` page. Fixes #77048. diff --git a/src/gateway/server-http.request-trace.test.ts b/src/gateway/server-http.request-trace.test.ts index e45e6898bde..8ccd5039c07 100644 --- a/src/gateway/server-http.request-trace.test.ts +++ b/src/gateway/server-http.request-trace.test.ts @@ -88,9 +88,13 @@ describe("gateway HTTP request trace scope", () => { expect(activeTraceInHandler?.spanId).toMatch(/^[0-9a-f]{16}$/); expect(events).toEqual([{ trace: activeTraceInHandler, type: "message.queued" }]); - const [line] = fs.readFileSync(logPath, "utf8").trim().split("\n"); - const record = JSON.parse(line ?? "{}") as Record; - expect(record).toMatchObject({ + const traceRecord = fs + .readFileSync(logPath, "utf8") + .trim() + .split("\n") + .map((line) => JSON.parse(line) as Record) + .find((record) => record.message === "handled request trace"); + expect(traceRecord).toMatchObject({ traceId: activeTraceInHandler?.traceId, spanId: activeTraceInHandler?.spanId, }); diff --git a/src/gateway/server-network-runtime.e2e.test.ts b/src/gateway/server-network-runtime.e2e.test.ts index 03bd74ea8a6..e97ff7c55d1 100644 --- a/src/gateway/server-network-runtime.e2e.test.ts +++ b/src/gateway/server-network-runtime.e2e.test.ts @@ -38,6 +38,14 @@ function isEnvHttpProxyDispatcher(dispatcher: unknown): boolean { ); } +async function closeTestDispatcher(dispatcher: unknown): Promise { + const close = (dispatcher as { close?: () => Promise | void } | undefined)?.close; + if (typeof close !== "function") { + return; + } + await close.call(dispatcher); +} + describe("gateway network runtime", () => { beforeEach(() => { clearRuntimeConfigSnapshot(); @@ -64,7 +72,8 @@ describe("gateway network runtime", () => { let server: Awaited> | undefined; try { - setGlobalDispatcher(new Agent()); + const testDispatcher = new Agent(); + setGlobalDispatcher(testDispatcher); for (const key of NETWORK_GATEWAY_ENV_KEYS) { delete process.env[key]; } @@ -101,7 +110,11 @@ describe("gateway network runtime", () => { expect(isEnvHttpProxyDispatcher(getGlobalDispatcher())).toBe(true); } finally { await server?.close({ reason: "gateway proxy bootstrap test complete" }); + const dispatcherToClose = getGlobalDispatcher(); setGlobalDispatcher(originalDispatcher); + if (dispatcherToClose !== originalDispatcher) { + await closeTestDispatcher(dispatcherToClose); + } await fs.rm(tempHome, { recursive: true, force: true }); envSnapshot.restore(); } diff --git a/src/gateway/shared-auth.test-helpers.ts b/src/gateway/shared-auth.test-helpers.ts index b589087e28d..8fe2462c741 100644 --- a/src/gateway/shared-auth.test-helpers.ts +++ b/src/gateway/shared-auth.test-helpers.ts @@ -2,10 +2,42 @@ import { expect } from "vitest"; import { WebSocket } from "ws"; import { connectOk, rpcReq, trackConnectChallengeNonce } from "./test-helpers.js"; -export async function openAuthenticatedGatewayWs(port: number, token: string): Promise { +export async function openAuthenticatedGatewayWs( + port: number, + token: string, + timeoutMs = 10_000, +): Promise { const ws = new WebSocket(`ws://127.0.0.1:${port}`); trackConnectChallengeNonce(ws); - await new Promise((resolve) => ws.once("open", resolve)); + await new Promise((resolve, reject) => { + const cleanup = () => { + clearTimeout(timer); + ws.off("open", onOpen); + ws.off("error", onError); + ws.off("close", onClose); + }; + const onOpen = () => { + cleanup(); + resolve(); + }; + const onError = (error: unknown) => { + cleanup(); + reject(error instanceof Error ? error : new Error(String(error))); + }; + const onClose = (code: number, reason: Buffer) => { + cleanup(); + reject(new Error(`gateway websocket closed before open (${code}: ${reason.toString()})`)); + }; + const timer = setTimeout(() => { + cleanup(); + ws.close(); + reject(new Error(`gateway websocket did not open within ${timeoutMs}ms`)); + }, timeoutMs); + timer.unref?.(); + ws.once("open", onOpen); + ws.once("error", onError); + ws.once("close", onClose); + }); await connectOk(ws, { token }); return ws; } @@ -17,8 +49,11 @@ export async function waitForGatewayWsClose( return await new Promise((resolve, reject) => { const timer = setTimeout(() => { ws.off("close", onClose); - reject(new Error(`gateway websocket did not close within ${timeoutMs}ms`)); + reject( + new Error(`gateway websocket did not close within ${timeoutMs}ms (state=${ws.readyState})`), + ); }, timeoutMs); + timer.unref?.(); const onClose = (code: number, reason: Buffer) => { clearTimeout(timer); resolve({ code, reason: reason.toString() }); diff --git a/test/vitest/vitest.gateway-server.config.ts b/test/vitest/vitest.gateway-server.config.ts index a1bdcb00068..b26a8098d54 100644 --- a/test/vitest/vitest.gateway-server.config.ts +++ b/test/vitest/vitest.gateway-server.config.ts @@ -20,6 +20,9 @@ export function createGatewayServerVitestConfig(env?: Record