diff --git a/src/cli/devices-cli.test.ts b/src/cli/devices-cli.test.ts index 4ec0d22a1f4..cd092b22e1a 100644 --- a/src/cli/devices-cli.test.ts +++ b/src/cli/devices-cli.test.ts @@ -118,6 +118,42 @@ function mockLocalPairingFallback(message?: string) { summarizeDeviceTokens.mockReturnValue(undefined); } +function requireRecord(value: unknown, label: string): Record { + expect(typeof value).toBe("object"); + expect(value).not.toBeNull(); + if (typeof value !== "object" || value === null) { + throw new Error(`${label} was not an object`); + } + return value as Record; +} + +function expectRecordFields(record: Record, fields: Record) { + for (const [key, value] of Object.entries(fields)) { + expect(record[key]).toEqual(value); + } +} + +function requireGatewayCall(index: number): Record { + const call = (callGateway.mock.calls as unknown[][])[index]?.[0]; + return requireRecord(call, `gateway call ${index + 1}`); +} + +function expectGatewayCall(index: number, fields: Record) { + expectRecordFields(requireGatewayCall(index), fields); +} + +function hasGatewayMethod(method: string): boolean { + return (callGateway.mock.calls as unknown[][]).some((call) => { + const params = call[0]; + return ( + typeof params === "object" && + params !== null && + "method" in params && + params.method === method + ); + }); +} + describe("devices cli approve", () => { it("uses admin scope when approving an admin-scope request", async () => { callGateway @@ -130,20 +166,12 @@ describe("devices cli approve", () => { await runDevicesApprove(["req-123"]); expect(callGateway).toHaveBeenCalledTimes(2); - expect(callGateway).toHaveBeenNthCalledWith( - 1, - expect.objectContaining({ - method: "device.pair.list", - }), - ); - expect(callGateway).toHaveBeenNthCalledWith( - 2, - expect.objectContaining({ - method: "device.pair.approve", - params: { requestId: "req-123" }, - scopes: ["operator.admin"], - }), - ); + expectGatewayCall(0, { method: "device.pair.list" }); + expectGatewayCall(1, { + method: "device.pair.approve", + params: { requestId: "req-123" }, + scopes: ["operator.admin"], + }); }); it("keeps pairing scope for non-admin device approvals", async () => { @@ -161,14 +189,11 @@ describe("devices cli approve", () => { await runDevicesApprove(["req-pairing"]); - expect(callGateway).toHaveBeenNthCalledWith( - 2, - expect.objectContaining({ - method: "device.pair.approve", - params: { requestId: "req-pairing" }, - scopes: ["operator.pairing"], - }), - ); + expectGatewayCall(1, { + method: "device.pair.approve", + params: { requestId: "req-pairing" }, + scopes: ["operator.pairing"], + }); }); it("retries explicit approval with admin scope when a paired-device session is ownership-denied", async () => { @@ -183,22 +208,16 @@ describe("devices cli approve", () => { await runDevicesApprove(["req-cross-device"]); expect(callGateway).toHaveBeenCalledTimes(3); - expect(callGateway).toHaveBeenNthCalledWith( - 2, - expect.objectContaining({ - method: "device.pair.approve", - params: { requestId: "req-cross-device" }, - scopes: undefined, - }), - ); - expect(callGateway).toHaveBeenNthCalledWith( - 3, - expect.objectContaining({ - method: "device.pair.approve", - params: { requestId: "req-cross-device" }, - scopes: ["operator.admin"], - }), - ); + expectGatewayCall(1, { + method: "device.pair.approve", + params: { requestId: "req-cross-device" }, + scopes: undefined, + }); + expectGatewayCall(2, { + method: "device.pair.approve", + params: { requestId: "req-cross-device" }, + scopes: ["operator.admin"], + }); }); it("uses admin scope when a repair approval would inherit an admin token", async () => { @@ -220,14 +239,11 @@ describe("devices cli approve", () => { await runDevicesApprove(["req-repair"]); - expect(callGateway).toHaveBeenNthCalledWith( - 2, - expect.objectContaining({ - method: "device.pair.approve", - params: { requestId: "req-repair" }, - scopes: ["operator.admin"], - }), - ); + expectGatewayCall(1, { + method: "device.pair.approve", + params: { requestId: "req-repair" }, + scopes: ["operator.admin"], + }); }); it("prints selected details and exits when implicit approval is used", async () => { @@ -256,9 +272,7 @@ describe("devices cli approve", () => { await runDevicesApprove([]); expect(callGateway).toHaveBeenCalledTimes(1); - expect(callGateway).toHaveBeenCalledWith( - expect.objectContaining({ method: "device.pair.list" }), - ); + expectGatewayCall(0, { method: "device.pair.list" }); const logOutput = runtime.log.mock.calls.map((c) => readRuntimeCallText(c)).join("\n"); expect(logOutput).toContain("req-abc"); expect(logOutput).toContain("Device Nine"); @@ -268,9 +282,7 @@ describe("devices cli approve", () => { expect.stringContaining("openclaw devices approve req-abc"), ); expect(runtime.exit).toHaveBeenCalledWith(1); - expect(callGateway).not.toHaveBeenCalledWith( - expect.objectContaining({ method: "device.pair.approve" }), - ); + expect(hasGatewayMethod("device.pair.approve")).toBe(false); }); it("sanitizes preview ip output for implicit approval", async () => { @@ -329,13 +341,8 @@ describe("devices cli approve", () => { await runDevicesApprove(args); - expect(callGateway).toHaveBeenNthCalledWith( - 1, - expect.objectContaining({ method: "device.pair.list" }), - ); - expect(callGateway).not.toHaveBeenCalledWith( - expect.objectContaining({ method: "device.pair.approve" }), - ); + expectGatewayCall(0, { method: "device.pair.list" }); + expect(hasGatewayMethod("device.pair.approve")).toBe(false); expect(runtime.error).toHaveBeenCalledWith( expect.stringContaining(`openclaw devices approve ${expectedRequestId}`), ); @@ -360,9 +367,7 @@ describe("devices cli approve", () => { expect(runtime.error).toHaveBeenCalledWith( expect.stringContaining("openclaw devices approve req-blank"), ); - expect(callGateway).not.toHaveBeenCalledWith( - expect.objectContaining({ method: "device.pair.approve" }), - ); + expect(hasGatewayMethod("device.pair.approve")).toBe(false); }); it("includes explicit gateway flags in the rerun approval command", async () => { @@ -386,9 +391,7 @@ describe("devices cli approve", () => { ); expect(errorOutput).toContain("Reuse the same --token option when rerunning."); expect(errorOutput).not.toContain("secret-token"); - expect(callGateway).not.toHaveBeenCalledWith( - expect.objectContaining({ method: "device.pair.approve" }), - ); + expect(hasGatewayMethod("device.pair.approve")).toBe(false); }); it("returns JSON for implicit approval preview in JSON mode", async () => { @@ -415,9 +418,7 @@ describe("devices cli approve", () => { }, }); expect(runtime.exit).toHaveBeenCalledWith(1); - expect(callGateway).not.toHaveBeenCalledWith( - expect.objectContaining({ method: "device.pair.approve" }), - ); + expect(hasGatewayMethod("device.pair.approve")).toBe(false); }); it("prints an error and exits when no pending requests are available", async () => { @@ -426,14 +427,10 @@ describe("devices cli approve", () => { await runDevicesApprove([]); expect(callGateway).toHaveBeenCalledTimes(1); - expect(callGateway).toHaveBeenCalledWith( - expect.objectContaining({ method: "device.pair.list" }), - ); + expectGatewayCall(0, { 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" }), - ); + expect(hasGatewayMethod("device.pair.approve")).toBe(false); }); }); @@ -444,12 +441,10 @@ describe("devices cli remove", () => { await runDevicesCommand(["remove", "device-1"]); expect(callGateway).toHaveBeenCalledTimes(1); - expect(callGateway).toHaveBeenCalledWith( - expect.objectContaining({ - method: "device.pair.remove", - params: { deviceId: "device-1" }, - }), - ); + expectGatewayCall(0, { + method: "device.pair.remove", + params: { deviceId: "device-1" }, + }); }); }); @@ -474,22 +469,10 @@ describe("devices cli clear", () => { await runDevicesCommand(["clear", "--yes", "--pending"]); - expect(callGateway).toHaveBeenNthCalledWith( - 1, - expect.objectContaining({ method: "device.pair.list" }), - ); - expect(callGateway).toHaveBeenNthCalledWith( - 2, - expect.objectContaining({ method: "device.pair.remove", params: { deviceId: "device-1" } }), - ); - expect(callGateway).toHaveBeenNthCalledWith( - 3, - expect.objectContaining({ method: "device.pair.remove", params: { deviceId: "device-2" } }), - ); - expect(callGateway).toHaveBeenNthCalledWith( - 4, - expect.objectContaining({ method: "device.pair.reject", params: { requestId: "req-1" } }), - ); + expectGatewayCall(0, { method: "device.pair.list" }); + expectGatewayCall(1, { method: "device.pair.remove", params: { deviceId: "device-1" } }); + expectGatewayCall(2, { method: "device.pair.remove", params: { deviceId: "device-2" } }); + expectGatewayCall(3, { method: "device.pair.reject", params: { requestId: "req-1" } }); }); }); @@ -531,7 +514,7 @@ describe("devices cli tokens", () => { ])("$label", async ({ argv, expectedCall }) => { callGateway.mockResolvedValueOnce({ ok: true }); await runDevicesCommand(argv); - expect(callGateway).toHaveBeenCalledWith(expect.objectContaining(expectedCall)); + expectGatewayCall(0, expectedCall); }); it("rejects blank device or role values", async () => { @@ -553,9 +536,7 @@ describe("devices cli local fallback", () => { await runDevicesCommand(["list"]); - expect(callGateway).toHaveBeenCalledWith( - expect.objectContaining({ method: "device.pair.list" }), - ); + expectGatewayCall(0, { method: "device.pair.list" }); expect(listDevicePairing).toHaveBeenCalledTimes(1); expect(runtime.log).toHaveBeenCalledWith(expect.stringContaining(fallbackNotice)); });