mirror of
https://github.com/moltbot/moltbot.git
synced 2026-03-07 22:44:16 +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 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 --latest
|
||||
```
|
||||
|
||||
### `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;
|
||||
timeout?: string;
|
||||
json?: boolean;
|
||||
latest?: boolean;
|
||||
device?: string;
|
||||
role?: 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) {
|
||||
if (!tokens || tokens.length === 0) {
|
||||
return "none";
|
||||
@@ -172,15 +184,31 @@ export function registerDevicesCli(program: Command) {
|
||||
devices
|
||||
.command("approve")
|
||||
.description("Approve a pending device pairing request")
|
||||
.argument("<requestId>", "Pending request id")
|
||||
.action(async (requestId: string, opts: DevicesRpcOpts) => {
|
||||
const result = await callGatewayCli("device.pair.approve", opts, { requestId });
|
||||
.argument("[requestId]", "Pending request id")
|
||||
.option("--latest", "Approve the most recent pending request", false)
|
||||
.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) {
|
||||
defaultRuntime.log(JSON.stringify(result, null, 2));
|
||||
return;
|
||||
}
|
||||
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