mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-10 04:42:44 +00:00
fix(status): show pairing approval recovery hints
This commit is contained in:
committed by
Peter Steinberger
parent
6c1ed9493c
commit
b902d5ade0
@@ -37,6 +37,21 @@ import {
|
|||||||
resolveUpdateAvailability,
|
resolveUpdateAvailability,
|
||||||
} from "./status.update.js";
|
} from "./status.update.js";
|
||||||
|
|
||||||
|
function resolvePairingRecoveryContext(params: {
|
||||||
|
error?: string | null;
|
||||||
|
closeReason?: string | null;
|
||||||
|
}): { requestId: string | null } | null {
|
||||||
|
const source = [params.error, params.closeReason]
|
||||||
|
.filter((part) => typeof part === "string" && part.trim().length > 0)
|
||||||
|
.join(" ");
|
||||||
|
if (!source || !/pairing required/i.test(source)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const requestIdMatch = source.match(/requestId:\s*([^\s)]+)/i);
|
||||||
|
const requestId = requestIdMatch && requestIdMatch[1] ? requestIdMatch[1].trim() : "";
|
||||||
|
return { requestId: requestId || null };
|
||||||
|
}
|
||||||
|
|
||||||
export async function statusCommand(
|
export async function statusCommand(
|
||||||
opts: {
|
opts: {
|
||||||
json?: boolean;
|
json?: boolean;
|
||||||
@@ -230,6 +245,10 @@ export async function statusCommand(
|
|||||||
const suffix = self ? ` · ${self}` : "";
|
const suffix = self ? ` · ${self}` : "";
|
||||||
return `${gatewayMode} · ${target} · ${reach}${auth}${suffix}`;
|
return `${gatewayMode} · ${target} · ${reach}${auth}${suffix}`;
|
||||||
})();
|
})();
|
||||||
|
const pairingRecovery = resolvePairingRecoveryContext({
|
||||||
|
error: gatewayProbe?.error ?? null,
|
||||||
|
closeReason: gatewayProbe?.close?.reason ?? null,
|
||||||
|
});
|
||||||
|
|
||||||
const agentsValue = (() => {
|
const agentsValue = (() => {
|
||||||
const pending =
|
const pending =
|
||||||
@@ -399,6 +418,20 @@ export async function statusCommand(
|
|||||||
}).trimEnd(),
|
}).trimEnd(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (pairingRecovery) {
|
||||||
|
runtime.log("");
|
||||||
|
runtime.log(theme.warn("Gateway pairing approval required."));
|
||||||
|
if (pairingRecovery.requestId) {
|
||||||
|
runtime.log(
|
||||||
|
theme.muted(
|
||||||
|
`Recovery: ${formatCliCommand(`openclaw devices approve ${pairingRecovery.requestId}`)}`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
runtime.log(theme.muted(`Fallback: ${formatCliCommand("openclaw devices approve --latest")}`));
|
||||||
|
runtime.log(theme.muted(`Inspect: ${formatCliCommand("openclaw devices list")}`));
|
||||||
|
}
|
||||||
|
|
||||||
runtime.log("");
|
runtime.log("");
|
||||||
runtime.log(theme.heading("Security audit"));
|
runtime.log(theme.heading("Security audit"));
|
||||||
const fmtSummary = (value: { critical: number; warn: number; info: number }) => {
|
const fmtSummary = (value: { critical: number; warn: number; info: number }) => {
|
||||||
|
|||||||
@@ -479,6 +479,52 @@ describe("statusCommand", () => {
|
|||||||
expect(logs.join("\n")).toMatch(/WARN/);
|
expect(logs.join("\n")).toMatch(/WARN/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("prints requestId-aware recovery guidance when gateway pairing is required", async () => {
|
||||||
|
mocks.probeGateway.mockResolvedValueOnce({
|
||||||
|
ok: false,
|
||||||
|
url: "ws://127.0.0.1:18789",
|
||||||
|
connectLatencyMs: null,
|
||||||
|
error: "connect failed: pairing required (requestId: req-123)",
|
||||||
|
close: { code: 1008, reason: "pairing required (requestId: req-123)" },
|
||||||
|
health: null,
|
||||||
|
status: null,
|
||||||
|
presence: null,
|
||||||
|
configSnapshot: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
runtimeLogMock.mockClear();
|
||||||
|
await statusCommand({}, runtime as never);
|
||||||
|
const logs = runtimeLogMock.mock.calls.map((c: unknown[]) => String(c[0]));
|
||||||
|
const joined = logs.join("\n");
|
||||||
|
expect(joined).toContain("Gateway pairing approval required.");
|
||||||
|
expect(joined).toContain("devices approve req-123");
|
||||||
|
expect(joined).toContain("devices approve --latest");
|
||||||
|
expect(joined).toContain("devices list");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("prints fallback recovery guidance when pairing requestId is unavailable", async () => {
|
||||||
|
mocks.probeGateway.mockResolvedValueOnce({
|
||||||
|
ok: false,
|
||||||
|
url: "ws://127.0.0.1:18789",
|
||||||
|
connectLatencyMs: null,
|
||||||
|
error: "connect failed: pairing required",
|
||||||
|
close: { code: 1008, reason: "connect failed" },
|
||||||
|
health: null,
|
||||||
|
status: null,
|
||||||
|
presence: null,
|
||||||
|
configSnapshot: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
runtimeLogMock.mockClear();
|
||||||
|
await statusCommand({}, runtime as never);
|
||||||
|
const logs = runtimeLogMock.mock.calls.map((c: unknown[]) => String(c[0]));
|
||||||
|
const joined = logs.join("\n");
|
||||||
|
expect(joined).toContain("Gateway pairing approval required.");
|
||||||
|
expect(joined).not.toContain("devices approve req-");
|
||||||
|
expect(joined).toContain("devices approve --latest");
|
||||||
|
expect(joined).toContain("devices list");
|
||||||
|
});
|
||||||
|
|
||||||
it("includes sessions across agents in JSON output", async () => {
|
it("includes sessions across agents in JSON output", async () => {
|
||||||
const originalAgents = mocks.listAgentsForGateway.getMockImplementation();
|
const originalAgents = mocks.listAgentsForGateway.getMockImplementation();
|
||||||
const originalResolveStorePath = mocks.resolveStorePath.getMockImplementation();
|
const originalResolveStorePath = mocks.resolveStorePath.getMockImplementation();
|
||||||
|
|||||||
Reference in New Issue
Block a user