diff --git a/src/cli/devices-cli.test.ts b/src/cli/devices-cli.test.ts index f70cc638539..8acd1336a99 100644 --- a/src/cli/devices-cli.test.ts +++ b/src/cli/devices-cli.test.ts @@ -439,6 +439,22 @@ describe("devices cli local fallback", () => { expect(runtime.log).toHaveBeenCalledWith(expect.stringContaining("Approved")); }); + it("falls back to local pairing list when gateway returns a scope upgrade message on loopback", async () => { + callGateway.mockRejectedValueOnce( + new Error("scope upgrade pending approval (requestId: req-123)"), + ); + listDevicePairing.mockResolvedValueOnce({ + pending: [{ requestId: "req-1", deviceId: "device-1", publicKey: "pk", ts: 1 }], + paired: [], + }); + summarizeDeviceTokens.mockReturnValue(undefined); + + await runDevicesCommand(["list"]); + + expect(listDevicePairing).toHaveBeenCalledTimes(1); + expect(runtime.log).toHaveBeenCalledWith(expect.stringContaining(fallbackNotice)); + }); + it("does not use local fallback when an explicit --url is provided", async () => { callGateway.mockRejectedValueOnce(new Error("gateway closed (1008): pairing required")); diff --git a/src/cli/devices-cli.ts b/src/cli/devices-cli.ts index 6419f9948c2..71c1a95f475 100644 --- a/src/cli/devices-cli.ts +++ b/src/cli/devices-cli.ts @@ -2,6 +2,7 @@ import type { Command } from "commander"; import { buildGatewayConnectionDetails, callGateway } from "../gateway/call.js"; import { isLoopbackHost } from "../gateway/net.js"; import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../gateway/protocol/client-info.js"; +import { readConnectPairingRequiredMessage } from "../gateway/protocol/connect-error-details.js"; import { approveDevicePairing, formatDevicePairingForbiddenMessage, @@ -120,7 +121,7 @@ function normalizeErrorMessage(error: unknown): string { function shouldUseLocalPairingFallback(opts: DevicesRpcOpts, error: unknown): boolean { const message = normalizeLowercaseStringOrEmpty(normalizeErrorMessage(error)); - if (!message.includes("pairing required")) { + if (!readConnectPairingRequiredMessage(message)) { return false; } if (typeof opts.url === "string" && opts.url.trim().length > 0) { diff --git a/src/cli/logs-cli.test.ts b/src/cli/logs-cli.test.ts index ba7c1e12193..e383a16850a 100644 --- a/src/cli/logs-cli.test.ts +++ b/src/cli/logs-cli.test.ts @@ -158,6 +158,29 @@ describe("logs cli", () => { expect(stderrWrites.join("")).toContain("reading local log file instead"); }); + it("falls back to the local log file on loopback scope-upgrade errors", async () => { + callGatewayFromCli.mockRejectedValueOnce( + new Error("scope upgrade pending approval (requestId: req-123)"), + ); + readConfiguredLogTail.mockResolvedValueOnce({ + file: "/tmp/openclaw.log", + cursor: 5, + size: 5, + lines: ["local fallback line"], + truncated: false, + reset: false, + }); + + const stdoutWrites = captureStdoutWrites(); + const stderrWrites = captureStderrWrites(); + + await runLogsCli(["logs"]); + + expect(readConfiguredLogTail).toHaveBeenCalledTimes(1); + expect(stdoutWrites.join("")).toContain("local fallback line"); + expect(stderrWrites.join("")).toContain("reading local log file instead"); + }); + describe("formatLogTimestamp", () => { it("formats UTC timestamp in plain mode by default", () => { const result = formatLogTimestamp("2025-01-01T12:00:00.000Z"); diff --git a/src/cli/logs-cli.ts b/src/cli/logs-cli.ts index d51541fc650..19ceeb1d94f 100644 --- a/src/cli/logs-cli.ts +++ b/src/cli/logs-cli.ts @@ -2,6 +2,7 @@ import { setTimeout as delay } from "node:timers/promises"; import type { Command } from "commander"; import { buildGatewayConnectionDetails } from "../gateway/call.js"; import { isLoopbackHost } from "../gateway/net.js"; +import { readConnectPairingRequiredMessage } from "../gateway/protocol/connect-error-details.js"; import { formatErrorMessage } from "../infra/errors.js"; import { readConfiguredLogTail } from "../logging/log-tail.js"; import { parseLogLine } from "../logging/parse-log-line.js"; @@ -96,7 +97,7 @@ function normalizeErrorMessage(error: unknown): string { function shouldUseLocalLogsFallback(opts: LogsCliOptions, error: unknown): boolean { const message = normalizeLowercaseStringOrEmpty(normalizeErrorMessage(error)); - if (!message.includes("pairing required")) { + if (!readConnectPairingRequiredMessage(message)) { return false; } if (typeof opts.url === "string" && opts.url.trim().length > 0) {