Skip to content

Commit be41b8c

Browse files
authored
test: stabilize gateway server shard (openclaw#77131)
1 parent a9282f3 commit be41b8c

5 files changed

Lines changed: 63 additions & 7 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ Docs: https://docs.openclaw.ai
110110
- 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.
111111
- 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.
112112
- 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.
113+
- 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.
113114
- 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.
114115
- 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.
115116
- Control UI: point the Appearance tweakcn browse action and docs at the live tweakcn editor route instead of the removed `/themes` page. Fixes #77048.

src/gateway/server-http.request-trace.test.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -88,9 +88,13 @@ describe("gateway HTTP request trace scope", () => {
8888
expect(activeTraceInHandler?.spanId).toMatch(/^[0-9a-f]{16}$/);
8989
expect(events).toEqual([{ trace: activeTraceInHandler, type: "message.queued" }]);
9090

91-
const [line] = fs.readFileSync(logPath, "utf8").trim().split("\n");
92-
const record = JSON.parse(line ?? "{}") as Record<string, unknown>;
93-
expect(record).toMatchObject({
91+
const traceRecord = fs
92+
.readFileSync(logPath, "utf8")
93+
.trim()
94+
.split("\n")
95+
.map((line) => JSON.parse(line) as Record<string, unknown>)
96+
.find((record) => record.message === "handled request trace");
97+
expect(traceRecord).toMatchObject({
9498
traceId: activeTraceInHandler?.traceId,
9599
spanId: activeTraceInHandler?.spanId,
96100
});

src/gateway/server-network-runtime.e2e.test.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,14 @@ function isEnvHttpProxyDispatcher(dispatcher: unknown): boolean {
3838
);
3939
}
4040

41+
async function closeTestDispatcher(dispatcher: unknown): Promise<void> {
42+
const close = (dispatcher as { close?: () => Promise<void> | void } | undefined)?.close;
43+
if (typeof close !== "function") {
44+
return;
45+
}
46+
await close.call(dispatcher);
47+
}
48+
4149
describe("gateway network runtime", () => {
4250
beforeEach(() => {
4351
clearRuntimeConfigSnapshot();
@@ -64,7 +72,8 @@ describe("gateway network runtime", () => {
6472
let server: Awaited<ReturnType<typeof startGatewayServer>> | undefined;
6573

6674
try {
67-
setGlobalDispatcher(new Agent());
75+
const testDispatcher = new Agent();
76+
setGlobalDispatcher(testDispatcher);
6877
for (const key of NETWORK_GATEWAY_ENV_KEYS) {
6978
delete process.env[key];
7079
}
@@ -101,7 +110,11 @@ describe("gateway network runtime", () => {
101110
expect(isEnvHttpProxyDispatcher(getGlobalDispatcher())).toBe(true);
102111
} finally {
103112
await server?.close({ reason: "gateway proxy bootstrap test complete" });
113+
const dispatcherToClose = getGlobalDispatcher();
104114
setGlobalDispatcher(originalDispatcher);
115+
if (dispatcherToClose !== originalDispatcher) {
116+
await closeTestDispatcher(dispatcherToClose);
117+
}
105118
await fs.rm(tempHome, { recursive: true, force: true });
106119
envSnapshot.restore();
107120
}

src/gateway/shared-auth.test-helpers.ts

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,42 @@ import { expect } from "vitest";
22
import { WebSocket } from "ws";
33
import { connectOk, rpcReq, trackConnectChallengeNonce } from "./test-helpers.js";
44

5-
export async function openAuthenticatedGatewayWs(port: number, token: string): Promise<WebSocket> {
5+
export async function openAuthenticatedGatewayWs(
6+
port: number,
7+
token: string,
8+
timeoutMs = 10_000,
9+
): Promise<WebSocket> {
610
const ws = new WebSocket(`ws://127.0.0.1:${port}`);
711
trackConnectChallengeNonce(ws);
8-
await new Promise<void>((resolve) => ws.once("open", resolve));
12+
await new Promise<void>((resolve, reject) => {
13+
const cleanup = () => {
14+
clearTimeout(timer);
15+
ws.off("open", onOpen);
16+
ws.off("error", onError);
17+
ws.off("close", onClose);
18+
};
19+
const onOpen = () => {
20+
cleanup();
21+
resolve();
22+
};
23+
const onError = (error: unknown) => {
24+
cleanup();
25+
reject(error instanceof Error ? error : new Error(String(error)));
26+
};
27+
const onClose = (code: number, reason: Buffer) => {
28+
cleanup();
29+
reject(new Error(`gateway websocket closed before open (${code}: ${reason.toString()})`));
30+
};
31+
const timer = setTimeout(() => {
32+
cleanup();
33+
ws.close();
34+
reject(new Error(`gateway websocket did not open within ${timeoutMs}ms`));
35+
}, timeoutMs);
36+
timer.unref?.();
37+
ws.once("open", onOpen);
38+
ws.once("error", onError);
39+
ws.once("close", onClose);
40+
});
941
await connectOk(ws, { token });
1042
return ws;
1143
}
@@ -17,8 +49,11 @@ export async function waitForGatewayWsClose(
1749
return await new Promise((resolve, reject) => {
1850
const timer = setTimeout(() => {
1951
ws.off("close", onClose);
20-
reject(new Error(`gateway websocket did not close within ${timeoutMs}ms`));
52+
reject(
53+
new Error(`gateway websocket did not close within ${timeoutMs}ms (state=${ws.readyState})`),
54+
);
2155
}, timeoutMs);
56+
timer.unref?.();
2257
const onClose = (code: number, reason: Buffer) => {
2358
clearTimeout(timer);
2459
resolve({ code, reason: reason.toString() });

test/vitest/vitest.gateway-server.config.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ export function createGatewayServerVitestConfig(env?: Record<string, string | un
2020
"src/gateway/server.startup-matrix-migration.integration.test.ts",
2121
"src/gateway/sessions-history-http.test.ts",
2222
],
23+
// Gateway server suites share process-level env, logger, and server helper state.
24+
// Isolate files so parallel shards cannot cross-wire suite-scoped servers.
25+
isolate: true,
2326
name: "gateway-server",
2427
},
2528
);

0 commit comments

Comments
 (0)