fix(cli): preserve local pairing fallback for upgrades

This commit is contained in:
Ayaan Zaidi
2026-04-20 12:08:53 +05:30
parent 66c1190bcc
commit 84f535c315
4 changed files with 43 additions and 2 deletions

View File

@@ -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"));

View File

@@ -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) {

View File

@@ -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");

View File

@@ -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) {