mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-16 22:20:42 +00:00
fix(gateway): split probe capability from reachability
This commit is contained in:
@@ -19,7 +19,14 @@ vi.mock("../progress.js", () => ({
|
||||
describe("probeGatewayStatus", () => {
|
||||
it("uses lightweight token-only probing for daemon status", async () => {
|
||||
callGatewayMock.mockReset();
|
||||
probeGatewayMock.mockResolvedValueOnce({ ok: true });
|
||||
probeGatewayMock.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
auth: {
|
||||
role: "operator",
|
||||
scopes: ["operator.write"],
|
||||
capability: "write_capable",
|
||||
},
|
||||
});
|
||||
|
||||
const result = await probeGatewayStatus({
|
||||
url: "ws://127.0.0.1:19191",
|
||||
@@ -29,7 +36,16 @@ describe("probeGatewayStatus", () => {
|
||||
json: true,
|
||||
});
|
||||
|
||||
expect(result).toEqual({ ok: true });
|
||||
expect(result).toEqual({
|
||||
ok: true,
|
||||
kind: "connect",
|
||||
capability: "write_capable",
|
||||
auth: {
|
||||
role: "operator",
|
||||
scopes: ["operator.write"],
|
||||
capability: "write_capable",
|
||||
},
|
||||
});
|
||||
expect(callGatewayMock).not.toHaveBeenCalled();
|
||||
expect(probeGatewayMock).toHaveBeenCalledWith({
|
||||
url: "ws://127.0.0.1:19191",
|
||||
@@ -58,7 +74,12 @@ describe("probeGatewayStatus", () => {
|
||||
configPath: "/tmp/openclaw-daemon/openclaw.json",
|
||||
});
|
||||
|
||||
expect(result).toEqual({ ok: true });
|
||||
expect(result).toEqual({
|
||||
ok: true,
|
||||
kind: "read",
|
||||
capability: "read_only",
|
||||
auth: undefined,
|
||||
});
|
||||
expect(probeGatewayMock).not.toHaveBeenCalled();
|
||||
expect(callGatewayMock).toHaveBeenCalledWith({
|
||||
url: "ws://127.0.0.1:19191",
|
||||
@@ -78,6 +99,11 @@ describe("probeGatewayStatus", () => {
|
||||
ok: false,
|
||||
error: null,
|
||||
close: { code: 1008, reason: "pairing required" },
|
||||
auth: {
|
||||
role: null,
|
||||
scopes: [],
|
||||
capability: "pairing_pending",
|
||||
},
|
||||
});
|
||||
|
||||
const result = await probeGatewayStatus({
|
||||
@@ -87,6 +113,13 @@ describe("probeGatewayStatus", () => {
|
||||
|
||||
expect(result).toEqual({
|
||||
ok: false,
|
||||
kind: "connect",
|
||||
capability: "pairing_pending",
|
||||
auth: {
|
||||
role: null,
|
||||
scopes: [],
|
||||
capability: "pairing_pending",
|
||||
},
|
||||
error: "gateway closed (1008): pairing required",
|
||||
});
|
||||
});
|
||||
@@ -98,6 +131,11 @@ describe("probeGatewayStatus", () => {
|
||||
ok: false,
|
||||
error: "timeout",
|
||||
close: { code: 1008, reason: "pairing required" },
|
||||
auth: {
|
||||
role: null,
|
||||
scopes: [],
|
||||
capability: "pairing_pending",
|
||||
},
|
||||
});
|
||||
|
||||
const result = await probeGatewayStatus({
|
||||
@@ -107,6 +145,13 @@ describe("probeGatewayStatus", () => {
|
||||
|
||||
expect(result).toEqual({
|
||||
ok: false,
|
||||
kind: "connect",
|
||||
capability: "pairing_pending",
|
||||
auth: {
|
||||
role: null,
|
||||
scopes: [],
|
||||
capability: "pairing_pending",
|
||||
},
|
||||
error: "gateway closed (1008): pairing required",
|
||||
});
|
||||
});
|
||||
@@ -125,6 +170,7 @@ describe("probeGatewayStatus", () => {
|
||||
|
||||
expect(result).toEqual({
|
||||
ok: false,
|
||||
kind: "read",
|
||||
error: "missing scope: operator.admin",
|
||||
});
|
||||
expect(probeGatewayMock).not.toHaveBeenCalled();
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { formatErrorMessage } from "../../infra/errors.js";
|
||||
import { withProgress } from "../progress.js";
|
||||
|
||||
type GatewayStatusProbeKind = "connect" | "read";
|
||||
|
||||
function resolveProbeFailureMessage(result: {
|
||||
error?: string | null;
|
||||
close?: { code: number; reason: string } | null;
|
||||
@@ -24,6 +26,7 @@ export async function probeGatewayStatus(opts: {
|
||||
requireRpc?: boolean;
|
||||
configPath?: string;
|
||||
}) {
|
||||
const kind = (opts.requireRpc ? "read" : "connect") satisfies GatewayStatusProbeKind;
|
||||
try {
|
||||
const result = await withProgress(
|
||||
{
|
||||
@@ -58,16 +61,26 @@ export async function probeGatewayStatus(opts: {
|
||||
});
|
||||
},
|
||||
);
|
||||
const auth = "auth" in result ? result.auth : undefined;
|
||||
if (result.ok) {
|
||||
return { ok: true } as const;
|
||||
return {
|
||||
ok: true,
|
||||
kind,
|
||||
capability: opts.requireRpc ? "read_only" : auth?.capability,
|
||||
auth,
|
||||
} as const;
|
||||
}
|
||||
return {
|
||||
ok: false,
|
||||
kind,
|
||||
capability: auth?.capability,
|
||||
auth,
|
||||
error: resolveProbeFailureMessage(result),
|
||||
} as const;
|
||||
} catch (err) {
|
||||
return {
|
||||
ok: false,
|
||||
kind,
|
||||
error: formatErrorMessage(err),
|
||||
} as const;
|
||||
}
|
||||
|
||||
@@ -49,7 +49,9 @@ function resolveRpcOptions(cmdOpts: GatewayRpcOpts, command?: Command): GatewayR
|
||||
export function addGatewayServiceCommands(parent: Command, opts?: { statusDescription?: string }) {
|
||||
parent
|
||||
.command("status")
|
||||
.description(opts?.statusDescription ?? "Show gateway service status + probe the Gateway")
|
||||
.description(
|
||||
opts?.statusDescription ?? "Show gateway service status + probe connectivity/capability",
|
||||
)
|
||||
.option("--url <url>", "Gateway WebSocket URL (defaults to config/remote/local)")
|
||||
.option("--token <token>", "Gateway token (if required)")
|
||||
.option("--password <password>", "Gateway password (password auth)")
|
||||
|
||||
@@ -14,6 +14,6 @@ export function registerDaemonCli(program: Command) {
|
||||
);
|
||||
|
||||
addGatewayServiceCommands(daemon, {
|
||||
statusDescription: "Show service install status + probe the Gateway",
|
||||
statusDescription: "Show service install status + probe connectivity/capability",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -165,6 +165,13 @@ export type DaemonStatus = {
|
||||
lastError?: string;
|
||||
rpc?: {
|
||||
ok: boolean;
|
||||
kind?: "connect" | "read";
|
||||
capability?: string;
|
||||
auth?: {
|
||||
role?: string | null;
|
||||
scopes?: string[];
|
||||
capability?: string;
|
||||
};
|
||||
error?: string;
|
||||
url?: string;
|
||||
authWarning?: string;
|
||||
|
||||
@@ -120,4 +120,36 @@ describe("printDaemonStatus", () => {
|
||||
expect.stringContaining(formatCliCommand("openclaw gateway restart")),
|
||||
);
|
||||
});
|
||||
|
||||
it("prints probe kind and capability separately", () => {
|
||||
printDaemonStatus(
|
||||
{
|
||||
service: {
|
||||
label: "LaunchAgent",
|
||||
loaded: true,
|
||||
loadedText: "loaded",
|
||||
notLoadedText: "not loaded",
|
||||
runtime: { status: "running", pid: 8000 },
|
||||
},
|
||||
gateway: {
|
||||
bindMode: "loopback",
|
||||
bindHost: "127.0.0.1",
|
||||
port: 18789,
|
||||
portSource: "env/config",
|
||||
probeUrl: "ws://127.0.0.1:18789",
|
||||
},
|
||||
rpc: {
|
||||
ok: true,
|
||||
kind: "connect",
|
||||
capability: "write_capable",
|
||||
url: "ws://127.0.0.1:18789",
|
||||
},
|
||||
extraServices: [],
|
||||
},
|
||||
{ json: false },
|
||||
);
|
||||
|
||||
expect(runtime.log).toHaveBeenCalledWith(expect.stringContaining("Connectivity probe: ok"));
|
||||
expect(runtime.log).toHaveBeenCalledWith(expect.stringContaining("Capability: write-capable"));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -50,6 +50,17 @@ function sanitizeDaemonStatusForJson(status: DaemonStatus): DaemonStatus {
|
||||
};
|
||||
}
|
||||
|
||||
function formatProbeKindLabel(kind?: "connect" | "read") {
|
||||
return kind === "read" ? "Read probe:" : "Connectivity probe:";
|
||||
}
|
||||
|
||||
function formatCapabilityLabel(capability?: string) {
|
||||
if (!capability) {
|
||||
return null;
|
||||
}
|
||||
return capability.replaceAll("_", "-");
|
||||
}
|
||||
|
||||
export function printDaemonStatus(status: DaemonStatus, opts: { json: boolean }) {
|
||||
if (opts.json) {
|
||||
const sanitized = sanitizeDaemonStatusForJson(status);
|
||||
@@ -175,21 +186,26 @@ export function printDaemonStatus(status: DaemonStatus, opts: { json: boolean })
|
||||
);
|
||||
}
|
||||
if (rpc) {
|
||||
const probeLabel = formatProbeKindLabel(rpc.kind);
|
||||
if (rpc.ok) {
|
||||
defaultRuntime.log(`${label("RPC probe:")} ${okText("ok")}`);
|
||||
defaultRuntime.log(`${label(probeLabel)} ${okText("ok")}`);
|
||||
} else {
|
||||
defaultRuntime.error(`${label("RPC probe:")} ${errorText("failed")}`);
|
||||
defaultRuntime.error(`${label(probeLabel)} ${errorText("failed")}`);
|
||||
if (rpc.authWarning) {
|
||||
defaultRuntime.error(`${label("RPC auth:")} ${warnText(rpc.authWarning)}`);
|
||||
defaultRuntime.error(`${label("Probe auth:")} ${warnText(rpc.authWarning)}`);
|
||||
}
|
||||
if (rpc.url) {
|
||||
defaultRuntime.error(`${label("RPC target:")} ${rpc.url}`);
|
||||
defaultRuntime.error(`${label("Probe target:")} ${rpc.url}`);
|
||||
}
|
||||
const lines = (rpc.error ?? "unknown").split(/\r?\n/).filter(Boolean);
|
||||
for (const line of lines.slice(0, 12)) {
|
||||
defaultRuntime.error(` ${errorText(line)}`);
|
||||
}
|
||||
}
|
||||
const capability = formatCapabilityLabel(rpc.capability);
|
||||
if (capability) {
|
||||
defaultRuntime.log(`${label("Capability:")} ${infoText(capability)}`);
|
||||
}
|
||||
spacer();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user