Skip to content

Commit 7ea8971

Browse files
authored
Add OpenClaw gateway auth timeout diagnostics (#373)
- Extract gateway test runner for server reuse - Capture host, health, socket-close, and RPC hints - Add settings debug report copy for failed connections
1 parent 1627e83 commit 7ea8971

5 files changed

Lines changed: 1008 additions & 207 deletions

File tree

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import { afterEach, describe, expect, it } from "vitest";
2+
import { WebSocketServer, type WebSocket } from "ws";
3+
4+
import { OpenclawGatewayTestInternals, runOpenclawGatewayTest } from "./openclawGatewayTest.ts";
5+
6+
const servers = new Set<WebSocketServer>();
7+
8+
afterEach(async () => {
9+
await Promise.all(
10+
[...servers].map(
11+
(server) =>
12+
new Promise<void>((resolve) => {
13+
for (const client of server.clients) {
14+
client.terminate();
15+
}
16+
server.close(() => resolve());
17+
}),
18+
),
19+
);
20+
servers.clear();
21+
});
22+
23+
async function createGatewayServer(
24+
onConnection: (socket: WebSocket) => void,
25+
): Promise<{ url: string }> {
26+
const server = new WebSocketServer({ host: "127.0.0.1", port: 0 });
27+
servers.add(server);
28+
await new Promise<void>((resolve) => {
29+
server.once("listening", () => resolve());
30+
});
31+
server.on("connection", onConnection);
32+
const address = server.address();
33+
if (!address || typeof address === "string") {
34+
throw new Error("Expected a TCP address for the test websocket server.");
35+
}
36+
return { url: `ws://127.0.0.1:${address.port}` };
37+
}
38+
39+
describe("runOpenclawGatewayTest", () => {
40+
it("captures Tailscale-oriented hints for auth timeouts", () => {
41+
const hostKind = OpenclawGatewayTestInternals.classifyGatewayHost("vals-mini.example.ts.net", [
42+
"100.90.12.34",
43+
]);
44+
45+
expect(hostKind).toBe("tailscale");
46+
47+
const hints = OpenclawGatewayTestInternals.buildHints(
48+
new URL("wss://vals-mini.example.ts.net"),
49+
{
50+
resolvedAddresses: ["100.90.12.34"],
51+
hostKind,
52+
healthStatus: "skip",
53+
observedNotifications: [],
54+
hints: [],
55+
},
56+
"Authentication",
57+
"RPC 'auth.authenticate' timed out after 10000ms.",
58+
true,
59+
);
60+
61+
expect(hints.some((hint) => hint.includes("Tailscale"))).toBe(true);
62+
expect(hints.some((hint) => hint.includes("actual OpenClaw JSON-RPC gateway endpoint"))).toBe(
63+
true,
64+
);
65+
expect(hints.some((hint) => hint.includes("reverse proxy"))).toBe(true);
66+
});
67+
68+
it("reports socket-close details when auth fails mid-handshake", async () => {
69+
const gateway = await createGatewayServer((socket) => {
70+
socket.on("message", (data) => {
71+
const message = JSON.parse(data.toString()) as { method?: string };
72+
if (message.method === "auth.authenticate") {
73+
socket.close(4401, "gateway auth unavailable");
74+
}
75+
});
76+
});
77+
78+
const result = await runOpenclawGatewayTest({
79+
gatewayUrl: gateway.url,
80+
password: "topsecret",
81+
});
82+
83+
expect(result.success).toBe(false);
84+
expect(result.steps.find((step) => step.name === "WebSocket connect")?.status).toBe("pass");
85+
86+
const authStep = result.steps.find((step) => step.name === "Authentication");
87+
expect(authStep?.status).toBe("fail");
88+
expect(authStep?.detail).toContain("WebSocket closed before RPC 'auth.authenticate' completed");
89+
90+
expect(result.diagnostics?.socketCloseCode).toBe(4401);
91+
expect(result.diagnostics?.socketCloseReason).toBe("gateway auth unavailable");
92+
expect(result.diagnostics?.hints.some((hint) => hint.includes("loopback-only"))).toBe(true);
93+
});
94+
});

0 commit comments

Comments
 (0)