mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-02 08:51:38 +00:00
fix(status): honor explicit local RPC fallback timeouts
This commit is contained in:
@@ -1,8 +1,19 @@
|
||||
// Status scan shared tests cover gateway probe snapshots, Tailscale URLs, and shared scan helpers.
|
||||
import { once } from "node:events";
|
||||
import type { AddressInfo } from "node:net";
|
||||
import path from "node:path";
|
||||
import { DatabaseSync } from "node:sqlite";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { WebSocketServer } from "ws";
|
||||
import { cleanupTempDirs, makeTempDir } from "../../test/helpers/temp-dir.js";
|
||||
import { parseStatusRouteArgs } from "../cli/program/route-args.js";
|
||||
import {
|
||||
buildMinimalGatewayHelloOkPayload,
|
||||
closeMinimalGatewayServer,
|
||||
parseMinimalGatewayRequestFrame,
|
||||
sendMinimalGatewayConnectChallenge,
|
||||
sendMinimalGatewayResponse,
|
||||
} from "../gateway/minimal-gateway.test-helpers.js";
|
||||
import {
|
||||
buildTailscaleHttpsUrl,
|
||||
resolveGatewayProbeSnapshot,
|
||||
@@ -325,39 +336,118 @@ describe("resolveGatewayProbeSnapshot", () => {
|
||||
expect(gatewayCall.timeoutMs).toBe(2000);
|
||||
});
|
||||
|
||||
it("does not raise an explicit local status RPC fallback timeout", async () => {
|
||||
it.each([1, 50, 999, 1000, 2000, 8000])(
|
||||
"does not raise an explicit local status RPC fallback timeout (%i ms)",
|
||||
async (timeoutMs) => {
|
||||
mocks.resolveGatewayProbeTarget.mockReturnValue({
|
||||
mode: "local",
|
||||
gatewayMode: "local",
|
||||
remoteUrlMissing: false,
|
||||
});
|
||||
mocks.probeGateway.mockResolvedValue({
|
||||
ok: false,
|
||||
url: "ws://127.0.0.1:18789",
|
||||
connectLatencyMs: null,
|
||||
error: "timeout",
|
||||
close: null,
|
||||
auth: {
|
||||
role: null,
|
||||
scopes: [],
|
||||
capability: "unknown",
|
||||
},
|
||||
health: null,
|
||||
status: null,
|
||||
presence: null,
|
||||
configSnapshot: null,
|
||||
});
|
||||
mocks.callGateway.mockResolvedValue({ sessions: 1 });
|
||||
|
||||
await resolveGatewayProbeSnapshot({
|
||||
cfg: {},
|
||||
opts: { timeoutMs },
|
||||
});
|
||||
|
||||
const probeCall = readProbeCall();
|
||||
expect(probeCall).not.toHaveProperty("preauthHandshakeTimeoutMs");
|
||||
expect(probeCall.timeoutMs).toBe(timeoutMs);
|
||||
expect(readGatewayCall().timeoutMs).toBe(Math.min(2000, timeoutMs));
|
||||
},
|
||||
);
|
||||
|
||||
it("enforces an explicit CLI timeout against a real local fallback status RPC", async () => {
|
||||
const gateway = new WebSocketServer({ host: "127.0.0.1", port: 0 });
|
||||
await once(gateway, "listening");
|
||||
const address = gateway.address() as AddressInfo;
|
||||
const url = `ws://127.0.0.1:${address.port}`;
|
||||
const observedMethods: string[] = [];
|
||||
gateway.on("connection", (socket) => {
|
||||
sendMinimalGatewayConnectChallenge(socket);
|
||||
socket.on("message", (data) => {
|
||||
const frame = parseMinimalGatewayRequestFrame(data);
|
||||
if (frame.type !== "req" || !frame.id || !frame.method) {
|
||||
return;
|
||||
}
|
||||
const requestId = frame.id;
|
||||
if (frame.method === "connect") {
|
||||
sendMinimalGatewayResponse(
|
||||
socket,
|
||||
requestId,
|
||||
buildMinimalGatewayHelloOkPayload({
|
||||
methods: ["system-presence", "status"],
|
||||
auth: { role: "operator", scopes: ["operator.read"] },
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
observedMethods.push(frame.method);
|
||||
if (frame.method === "status") {
|
||||
const responseTimer = setTimeout(() => {
|
||||
if (socket.readyState === socket.OPEN) {
|
||||
sendMinimalGatewayResponse(socket, requestId, { sessions: 1 });
|
||||
}
|
||||
}, 400);
|
||||
responseTimer.unref();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
mocks.buildGatewayConnectionDetailsWithResolvers.mockReturnValue({
|
||||
url,
|
||||
urlSource: "local loopback",
|
||||
message: `Gateway target: ${url}`,
|
||||
});
|
||||
mocks.resolveGatewayProbeTarget.mockReturnValue({
|
||||
mode: "local",
|
||||
gatewayMode: "local",
|
||||
remoteUrlMissing: false,
|
||||
});
|
||||
mocks.probeGateway.mockResolvedValue({
|
||||
ok: false,
|
||||
url: "ws://127.0.0.1:18789",
|
||||
connectLatencyMs: null,
|
||||
error: "timeout",
|
||||
close: null,
|
||||
auth: {
|
||||
role: null,
|
||||
scopes: [],
|
||||
capability: "unknown",
|
||||
},
|
||||
health: null,
|
||||
status: null,
|
||||
presence: null,
|
||||
configSnapshot: null,
|
||||
mocks.probeGateway.mockImplementation(async (...args: unknown[]) => {
|
||||
const { probeGateway } =
|
||||
await vi.importActual<typeof import("../gateway/probe.js")>("../gateway/probe.js");
|
||||
return await probeGateway(...(args as Parameters<typeof probeGateway>));
|
||||
});
|
||||
mocks.callGateway.mockResolvedValue({ sessions: 1 });
|
||||
|
||||
await resolveGatewayProbeSnapshot({
|
||||
cfg: {},
|
||||
opts: { timeoutMs: 1000 },
|
||||
mocks.callGateway.mockImplementation(async (...args: unknown[]) => {
|
||||
const { callGateway } =
|
||||
await vi.importActual<typeof import("../gateway/call.js")>("../gateway/call.js");
|
||||
return await callGateway(...(args as Parameters<typeof callGateway>));
|
||||
});
|
||||
const parsed = parseStatusRouteArgs(["node", "openclaw", "status", "--timeout", "250"]);
|
||||
expect(parsed?.timeoutMs).toBe(250);
|
||||
|
||||
const probeCall = readProbeCall();
|
||||
expect(probeCall).not.toHaveProperty("preauthHandshakeTimeoutMs");
|
||||
expect(probeCall.timeoutMs).toBe(1000);
|
||||
expect(readGatewayCall().timeoutMs).toBe(1000);
|
||||
try {
|
||||
const result = await resolveGatewayProbeSnapshot({
|
||||
cfg: { gateway: { auth: { mode: "none" } } },
|
||||
opts: { timeoutMs: parsed?.timeoutMs },
|
||||
});
|
||||
|
||||
expect(readProbeCall().timeoutMs).toBe(250);
|
||||
expect(readGatewayCall().timeoutMs).toBe(250);
|
||||
expect(observedMethods).toEqual(["system-presence", "status"]);
|
||||
expect(result.gatewayProbe?.ok).toBe(false);
|
||||
expect(result.gatewayProbe?.error).toContain("timeout");
|
||||
} finally {
|
||||
await closeMinimalGatewayServer(gateway);
|
||||
}
|
||||
});
|
||||
|
||||
it("lets callGateway reuse paired-device auth for local status RPC fallback", async () => {
|
||||
|
||||
@@ -208,7 +208,11 @@ async function applyLocalStatusRpcFallback(params: {
|
||||
if (!shouldTryLocalStatusRpcFallback(params)) {
|
||||
return params.gatewayProbe;
|
||||
}
|
||||
const boundedFallbackTimeoutMs = Math.min(2000, Math.max(1000, params.timeoutMs));
|
||||
// Explicit probe budgets are operator-owned; only implicit fallback defaults get a floor.
|
||||
const boundedFallbackTimeoutMs = Math.min(
|
||||
2000,
|
||||
params.timeoutMsExplicit ? params.timeoutMs : Math.max(1000, params.timeoutMs),
|
||||
);
|
||||
// The fallback uses the gateway status RPC because it can succeed after probe handshake ambiguity.
|
||||
const status = await loadGatewayCallModule()
|
||||
.then(({ callGateway }) =>
|
||||
|
||||
Reference in New Issue
Block a user