mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-19 13:08:38 +00:00
CLI: approve latest pending device request
This commit is contained in:
@@ -21,12 +21,15 @@ openclaw devices list
|
|||||||
openclaw devices list --json
|
openclaw devices list --json
|
||||||
```
|
```
|
||||||
|
|
||||||
### `openclaw devices approve <requestId>`
|
### `openclaw devices approve [requestId] [--latest]`
|
||||||
|
|
||||||
Approve a pending device pairing request.
|
Approve a pending device pairing request. If `requestId` is omitted, OpenClaw
|
||||||
|
automatically approves the most recent pending request.
|
||||||
|
|
||||||
```
|
```
|
||||||
|
openclaw devices approve
|
||||||
openclaw devices approve <requestId>
|
openclaw devices approve <requestId>
|
||||||
|
openclaw devices approve --latest
|
||||||
```
|
```
|
||||||
|
|
||||||
### `openclaw devices reject <requestId>`
|
### `openclaw devices reject <requestId>`
|
||||||
|
|||||||
115
src/cli/devices-cli.test.ts
Normal file
115
src/cli/devices-cli.test.ts
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
import { Command } from "commander";
|
||||||
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
const callGateway = vi.fn();
|
||||||
|
const withProgress = vi.fn(async (_opts: unknown, fn: () => Promise<unknown>) => await fn());
|
||||||
|
const runtime = {
|
||||||
|
log: vi.fn(),
|
||||||
|
error: vi.fn(),
|
||||||
|
exit: vi.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
vi.mock("../gateway/call.js", () => ({
|
||||||
|
callGateway,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("./progress.js", () => ({
|
||||||
|
withProgress,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("../runtime.js", () => ({
|
||||||
|
defaultRuntime: runtime,
|
||||||
|
}));
|
||||||
|
|
||||||
|
async function runDevicesApprove(argv: string[]) {
|
||||||
|
const { registerDevicesCli } = await import("./devices-cli.js");
|
||||||
|
const program = new Command();
|
||||||
|
registerDevicesCli(program);
|
||||||
|
await program.parseAsync(["devices", "approve", ...argv], { from: "user" });
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("devices cli approve", () => {
|
||||||
|
afterEach(() => {
|
||||||
|
callGateway.mockReset();
|
||||||
|
withProgress.mockClear();
|
||||||
|
runtime.log.mockReset();
|
||||||
|
runtime.error.mockReset();
|
||||||
|
runtime.exit.mockReset();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("approves an explicit request id without listing", async () => {
|
||||||
|
callGateway.mockResolvedValueOnce({ device: { deviceId: "device-1" } });
|
||||||
|
|
||||||
|
await runDevicesApprove(["req-123"]);
|
||||||
|
|
||||||
|
expect(callGateway).toHaveBeenCalledTimes(1);
|
||||||
|
expect(callGateway).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
method: "device.pair.approve",
|
||||||
|
params: { requestId: "req-123" },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("auto-approves the latest pending request when id is omitted", async () => {
|
||||||
|
callGateway
|
||||||
|
.mockResolvedValueOnce({
|
||||||
|
pending: [
|
||||||
|
{ requestId: "req-1", ts: 1000 },
|
||||||
|
{ requestId: "req-2", ts: 2000 },
|
||||||
|
],
|
||||||
|
})
|
||||||
|
.mockResolvedValueOnce({ device: { deviceId: "device-2" } });
|
||||||
|
|
||||||
|
await runDevicesApprove([]);
|
||||||
|
|
||||||
|
expect(callGateway).toHaveBeenNthCalledWith(
|
||||||
|
1,
|
||||||
|
expect.objectContaining({ method: "device.pair.list" }),
|
||||||
|
);
|
||||||
|
expect(callGateway).toHaveBeenNthCalledWith(
|
||||||
|
2,
|
||||||
|
expect.objectContaining({
|
||||||
|
method: "device.pair.approve",
|
||||||
|
params: { requestId: "req-2" },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses latest pending request when --latest is passed", async () => {
|
||||||
|
callGateway
|
||||||
|
.mockResolvedValueOnce({
|
||||||
|
pending: [
|
||||||
|
{ requestId: "req-2", ts: 2000 },
|
||||||
|
{ requestId: "req-3", ts: 3000 },
|
||||||
|
],
|
||||||
|
})
|
||||||
|
.mockResolvedValueOnce({ device: { deviceId: "device-3" } });
|
||||||
|
|
||||||
|
await runDevicesApprove(["req-old", "--latest"]);
|
||||||
|
|
||||||
|
expect(callGateway).toHaveBeenNthCalledWith(
|
||||||
|
2,
|
||||||
|
expect.objectContaining({
|
||||||
|
method: "device.pair.approve",
|
||||||
|
params: { requestId: "req-3" },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("prints an error and exits when no pending requests are available", async () => {
|
||||||
|
callGateway.mockResolvedValueOnce({ pending: [] });
|
||||||
|
|
||||||
|
await runDevicesApprove([]);
|
||||||
|
|
||||||
|
expect(callGateway).toHaveBeenCalledTimes(1);
|
||||||
|
expect(callGateway).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({ method: "device.pair.list" }),
|
||||||
|
);
|
||||||
|
expect(runtime.error).toHaveBeenCalledWith("No pending device pairing requests to approve");
|
||||||
|
expect(runtime.exit).toHaveBeenCalledWith(1);
|
||||||
|
expect(callGateway).not.toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({ method: "device.pair.approve" }),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -13,6 +13,7 @@ type DevicesRpcOpts = {
|
|||||||
password?: string;
|
password?: string;
|
||||||
timeout?: string;
|
timeout?: string;
|
||||||
json?: boolean;
|
json?: boolean;
|
||||||
|
latest?: boolean;
|
||||||
device?: string;
|
device?: string;
|
||||||
role?: string;
|
role?: string;
|
||||||
scope?: string[];
|
scope?: string[];
|
||||||
@@ -86,6 +87,17 @@ function parseDevicePairingList(value: unknown): DevicePairingList {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function selectLatestPendingRequest(pending: PendingDevice[] | undefined) {
|
||||||
|
if (!pending?.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return pending.reduce((latest, current) => {
|
||||||
|
const latestTs = typeof latest.ts === "number" ? latest.ts : 0;
|
||||||
|
const currentTs = typeof current.ts === "number" ? current.ts : 0;
|
||||||
|
return currentTs > latestTs ? current : latest;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function formatTokenSummary(tokens: DeviceTokenSummary[] | undefined) {
|
function formatTokenSummary(tokens: DeviceTokenSummary[] | undefined) {
|
||||||
if (!tokens || tokens.length === 0) {
|
if (!tokens || tokens.length === 0) {
|
||||||
return "none";
|
return "none";
|
||||||
@@ -172,15 +184,31 @@ export function registerDevicesCli(program: Command) {
|
|||||||
devices
|
devices
|
||||||
.command("approve")
|
.command("approve")
|
||||||
.description("Approve a pending device pairing request")
|
.description("Approve a pending device pairing request")
|
||||||
.argument("<requestId>", "Pending request id")
|
.argument("[requestId]", "Pending request id")
|
||||||
.action(async (requestId: string, opts: DevicesRpcOpts) => {
|
.option("--latest", "Approve the most recent pending request", false)
|
||||||
const result = await callGatewayCli("device.pair.approve", opts, { requestId });
|
.action(async (requestId: string | undefined, opts: DevicesRpcOpts) => {
|
||||||
|
let resolvedRequestId = requestId?.trim();
|
||||||
|
if (!resolvedRequestId || opts.latest) {
|
||||||
|
const listResult = await callGatewayCli("device.pair.list", opts, {});
|
||||||
|
const latest = selectLatestPendingRequest(parseDevicePairingList(listResult).pending);
|
||||||
|
resolvedRequestId = latest?.requestId?.trim();
|
||||||
|
}
|
||||||
|
if (!resolvedRequestId) {
|
||||||
|
defaultRuntime.error("No pending device pairing requests to approve");
|
||||||
|
defaultRuntime.exit(1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const result = await callGatewayCli("device.pair.approve", opts, {
|
||||||
|
requestId: resolvedRequestId,
|
||||||
|
});
|
||||||
if (opts.json) {
|
if (opts.json) {
|
||||||
defaultRuntime.log(JSON.stringify(result, null, 2));
|
defaultRuntime.log(JSON.stringify(result, null, 2));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const deviceId = (result as { device?: { deviceId?: string } })?.device?.deviceId;
|
const deviceId = (result as { device?: { deviceId?: string } })?.device?.deviceId;
|
||||||
defaultRuntime.log(`${theme.success("Approved")} ${theme.command(deviceId ?? "ok")}`);
|
defaultRuntime.log(
|
||||||
|
`${theme.success("Approved")} ${theme.command(deviceId ?? "ok")} ${theme.muted(`(${resolvedRequestId})`)}`,
|
||||||
|
);
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user