mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-04 23:31:36 +00:00
test(gateway): pin MCP loopback drain proof to a server-side barrier (#117181)
The drain test asserted "close has not settled" after a fixed 20ms sleep. Node only exempts a connection from close()'s idle sweep once its parser has begun a message, so a client-side connect barrier let a loaded machine reach close() before the server parsed the request: the socket was swept as idle, the client saw ECONNRESET, and the drain settled inside the window. Wait on capture admission (onRequestStart) instead, which fires server-side after the headers are accepted, and move the assertion to just before the successor is used so it proves the successor was minted mid-drain.
This commit is contained in:
@@ -3413,13 +3413,22 @@ describe("createMcpLoopbackServerConfig", () => {
|
||||
if (!oldRuntime) {
|
||||
throw new Error("expected old MCP loopback runtime");
|
||||
}
|
||||
let stalledRequest: ReturnType<typeof request> | undefined;
|
||||
let resolveSocketReady: () => void = () => {};
|
||||
let rejectSocketReady: (error: Error) => void = () => {};
|
||||
const socketReady = new Promise<void>((resolve, reject) => {
|
||||
resolveSocketReady = resolve;
|
||||
rejectSocketReady = reject;
|
||||
// Node only exempts a connection from close()'s idle sweep once its parser has
|
||||
// begun a message, so the drain is pinned by the server-side request start, not
|
||||
// by the client-side connect. Capture admission is that server-side signal.
|
||||
const captureKey = "capture-stalled-drain";
|
||||
let resolveRequestStarted: () => void = () => {};
|
||||
let rejectRequestStarted: (error: Error) => void = () => {};
|
||||
const requestStarted = new Promise<void>((resolve, reject) => {
|
||||
resolveRequestStarted = resolve;
|
||||
rejectRequestStarted = reject;
|
||||
});
|
||||
beginMcpLoopbackToolCallCapture({
|
||||
captureKey,
|
||||
onRequestStart: () => resolveRequestStarted(),
|
||||
onToolCallResult: vi.fn(),
|
||||
});
|
||||
let stalledRequest: ReturnType<typeof request> | undefined;
|
||||
const responsePromise = new Promise<void>((resolve, reject) => {
|
||||
const req = request(
|
||||
{
|
||||
@@ -3431,6 +3440,7 @@ describe("createMcpLoopbackServerConfig", () => {
|
||||
authorization: `Bearer ${oldRuntime.ownerToken}`,
|
||||
connection: "close",
|
||||
"content-type": "application/json",
|
||||
"x-openclaw-cli-capture-key": captureKey,
|
||||
},
|
||||
},
|
||||
(res) => {
|
||||
@@ -3438,15 +3448,8 @@ describe("createMcpLoopbackServerConfig", () => {
|
||||
res.once("end", resolve);
|
||||
},
|
||||
);
|
||||
req.once("socket", (socket) => {
|
||||
if (!socket.connecting) {
|
||||
resolveSocketReady();
|
||||
return;
|
||||
}
|
||||
socket.once("connect", resolveSocketReady);
|
||||
});
|
||||
req.once("error", (error) => {
|
||||
rejectSocketReady(error);
|
||||
rejectRequestStarted(error);
|
||||
reject(error);
|
||||
});
|
||||
req.write("{");
|
||||
@@ -3462,20 +3465,13 @@ describe("createMcpLoopbackServerConfig", () => {
|
||||
};
|
||||
let oldClose: Promise<void> | undefined;
|
||||
try {
|
||||
await socketReady;
|
||||
await new Promise<void>((resolve) => {
|
||||
setImmediate(resolve);
|
||||
});
|
||||
await requestStarted;
|
||||
|
||||
let closeSettled = false;
|
||||
oldClose = closeMcpLoopbackServer().finally(() => {
|
||||
closeSettled = true;
|
||||
});
|
||||
expect(getActiveMcpLoopbackRuntime()).toBeUndefined();
|
||||
await new Promise<void>((resolve) => {
|
||||
setTimeout(resolve, 20);
|
||||
});
|
||||
expect(closeSettled).toBe(false);
|
||||
|
||||
const successor = await startLoopbackServerForTest();
|
||||
const successorGrant = mintMcpLoopbackClientGrant({
|
||||
@@ -3487,6 +3483,9 @@ describe("createMcpLoopbackServerConfig", () => {
|
||||
runtimeOwnerToken: successor.runtime.ownerToken,
|
||||
captureKey: "capture-successor",
|
||||
});
|
||||
// The unfinished body still holds the old connection, so the successor was
|
||||
// minted mid-drain: exactly the window where a late close could fence it.
|
||||
expect(closeSettled).toBe(false);
|
||||
|
||||
finishStalledRequest();
|
||||
await responsePromise;
|
||||
@@ -3503,6 +3502,7 @@ describe("createMcpLoopbackServerConfig", () => {
|
||||
).status,
|
||||
).toBe(200);
|
||||
} finally {
|
||||
clearMcpLoopbackToolCallCapture(captureKey);
|
||||
finishStalledRequest();
|
||||
await responsePromise.catch(() => undefined);
|
||||
await (oldClose ?? oldServer.close()).catch(() => undefined);
|
||||
|
||||
Reference in New Issue
Block a user