Skip to content

Commit 4ba3383

Browse files
dmytrovkcursoragent
andcommitted
chore(scripts): add repro for #79603 handshake phase log
Drop-in script that wires the production gateway ws-connection handler against a fresh HTTP + WebSocketServer, opens an idle WebSocket, and lets the preauth handshake timer fire so maintainers can independently reproduce the new "phase=ws_upgrade_started" diagnostic line. Refs: #79603 Co-authored-by: Cursor <[email protected]>
1 parent 990ea02 commit 4ba3383

1 file changed

Lines changed: 118 additions & 0 deletions

File tree

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
// Standalone reproduction for #79603 — captures the new last-completed
2+
// handshake phase log line emitted on a real Gateway WebSocket handshake
3+
// failure. Wires the production `attachGatewayWsConnectionHandler` against
4+
// a fresh HTTP + WebSocketServer, opens a raw WS, never sends `connect`,
5+
// and lets the preauth handshake timeout fire.
6+
//
7+
// Run:
8+
// pnpm exec tsx scripts/dev/repro-79603-handshake-phase.ts
9+
//
10+
// Expected stdout includes a line that contains both `handshake timeout`
11+
// and `phase=ws_upgrade_started` (the new diagnostic field).
12+
13+
import { createServer } from "node:http";
14+
import { setTimeout as sleep } from "node:timers/promises";
15+
import { WebSocket, WebSocketServer } from "ws";
16+
import type { ResolvedGatewayAuth } from "../../src/gateway/auth.js";
17+
import { attachGatewayWsConnectionHandler } from "../../src/gateway/server/ws-connection.js";
18+
import type { GatewayWsClient } from "../../src/gateway/server/ws-types.js";
19+
20+
const HANDSHAKE_TIMEOUT_MS = 250;
21+
const HANDSHAKE_GRACE_MS = 1500;
22+
23+
const capturedLines: string[] = [];
24+
const originalWrite = process.stderr.write.bind(process.stderr);
25+
process.stderr.write = ((chunk: unknown, ...rest: unknown[]) => {
26+
const text =
27+
typeof chunk === "string"
28+
? chunk
29+
: Buffer.isBuffer(chunk)
30+
? chunk.toString("utf8")
31+
: String(chunk);
32+
if (text.includes("handshake timeout") || text.includes("closed before connect")) {
33+
capturedLines.push(text.trimEnd());
34+
}
35+
return originalWrite(chunk as never, ...(rest as []));
36+
}) as typeof process.stderr.write;
37+
38+
function makeLogger(name: string) {
39+
const emit = (level: string) => (message: string, meta?: Record<string, unknown>) => {
40+
const metaStr = meta ? ` ${JSON.stringify(meta)}` : "";
41+
process.stderr.write(`[${level}] ${name}: ${message}${metaStr}\n`);
42+
};
43+
return {
44+
debug: emit("debug"),
45+
info: emit("info"),
46+
warn: emit("warn"),
47+
error: emit("error"),
48+
} as never;
49+
}
50+
51+
const clients = new Set<GatewayWsClient>();
52+
const resolvedAuth: ResolvedGatewayAuth = { mode: "none", allowTailscale: false };
53+
const httpServer = createServer((_req, res) => {
54+
res.writeHead(404);
55+
res.end();
56+
});
57+
const wss = new WebSocketServer({ server: httpServer });
58+
59+
attachGatewayWsConnectionHandler({
60+
wss,
61+
clients,
62+
preauthConnectionBudget: { release: () => {} } as never,
63+
port: 0,
64+
resolvedAuth,
65+
preauthHandshakeTimeoutMs: HANDSHAKE_TIMEOUT_MS,
66+
gatewayMethods: [],
67+
events: [],
68+
refreshHealthSnapshot: async () => ({}) as never,
69+
logGateway: makeLogger("gateway"),
70+
logHealth: makeLogger("gateway/health"),
71+
logWsControl: makeLogger("gateway/ws"),
72+
extraHandlers: {},
73+
broadcast: () => {},
74+
buildRequestContext: () =>
75+
({
76+
unsubscribeAllSessionEvents: () => {},
77+
nodeRegistry: { unregister: () => null, register: () => ({}) },
78+
nodeUnsubscribeAll: () => {},
79+
}) as never,
80+
});
81+
82+
await new Promise<void>((resolve) => httpServer.listen(0, "127.0.0.1", resolve));
83+
const address = httpServer.address();
84+
const port = typeof address === "object" && address ? address.port : 0;
85+
86+
process.stdout.write(`[repro] gateway listening on ws://127.0.0.1:${port}\n`);
87+
process.stdout.write(`[repro] preauth handshake timeout = ${HANDSHAKE_TIMEOUT_MS}ms\n`);
88+
89+
const ws = new WebSocket(`ws://127.0.0.1:${port}/`);
90+
ws.on("message", (data) => {
91+
process.stdout.write(`[repro] received: ${data.toString().slice(0, 120)}\n`);
92+
});
93+
await new Promise<void>((resolve, reject) => {
94+
ws.once("open", () => resolve());
95+
ws.once("error", reject);
96+
});
97+
process.stdout.write("[repro] ws opened, idling so server's preauth timer fires...\n");
98+
99+
await sleep(HANDSHAKE_TIMEOUT_MS + HANDSHAKE_GRACE_MS);
100+
ws.removeAllListeners();
101+
ws.terminate();
102+
103+
process.stdout.write("\n========== captured log lines ==========\n");
104+
for (const line of capturedLines) {
105+
process.stdout.write(`${line}\n`);
106+
}
107+
process.stdout.write("========================================\n\n");
108+
109+
const phaseLine = capturedLines.find((line) => line.includes("phase="));
110+
if (!phaseLine) {
111+
process.stdout.write("[repro] FAIL: no phase=... line was captured\n");
112+
process.exit(1);
113+
}
114+
process.stdout.write(`[repro] OK: phase log emitted (${capturedLines.length} matching line(s))\n`);
115+
116+
wss.close();
117+
httpServer.close();
118+
process.exit(0);

0 commit comments

Comments
 (0)