diff --git a/src/gateway/exec-approval-ios-push.test.ts b/src/gateway/exec-approval-ios-push.test.ts index 4644e34d401..4ae4bd9c006 100644 --- a/src/gateway/exec-approval-ios-push.test.ts +++ b/src/gateway/exec-approval-ios-push.test.ts @@ -1,4 +1,5 @@ import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import type { ExecApprovalRequest, ExecApprovalResolved } from "../infra/exec-approvals.js"; const listDevicePairingMock = vi.fn(); const loadApnsRegistrationMock = vi.fn(); @@ -28,31 +29,93 @@ function createDeferred(): Deferred { return { promise, resolve, reject }; } -function mockPairedIosOperator(scopes: string[]) { +function apnsRegistration(nodeId = "ios-device-1") { + return { + nodeId, + transport: "direct", + token: "apns-token", + topic: "ai.openclaw.ios.test", + environment: "sandbox", + updatedAtMs: 1, + }; +} + +function successfulApnsPushResult() { + return { + ok: true, + status: 200, + environment: "sandbox", + topic: "ai.openclaw.ios.test", + tokenSuffix: "token", + transport: "direct", + }; +} + +function resolvedApnsAuthConfig() { + return { + ok: true, + value: { teamId: "team", keyId: "key", privateKey: "private-key" }, + }; +} + +function approvalRequest(id: string): ExecApprovalRequest { + return { + id, + request: { command: "echo ok", host: "gateway", allowedDecisions: ["allow-once"] }, + createdAtMs: 1, + expiresAtMs: 2, + }; +} + +function approvalResolved(id: string): ExecApprovalResolved { + return { + id, + decision: "allow-once", + ts: 1, + }; +} + +function pairedIosOperator(options: { + deviceId?: string; + publicKey?: string; + platform?: string; + approvedAtMs?: number; + scopes: string[]; + approvedScopes?: string[]; + token?: string; +}) { + const deviceId = options.deviceId ?? "ios-device-1"; + return { + deviceId, + publicKey: options.publicKey ?? "pub", + platform: options.platform ?? "iOS 18", + role: "operator", + roles: ["operator"], + approvedScopes: options.approvedScopes, + createdAtMs: 1, + approvedAtMs: options.approvedAtMs ?? 1, + tokens: { + operator: { + token: options.token ?? "operator-token", + role: "operator", + scopes: options.scopes, + createdAtMs: 1, + }, + }, + }; +} + +function mockPairedIosOperators(...paired: ReturnType[]) { listDevicePairingMock.mockResolvedValue({ pending: [], - paired: [ - { - deviceId: "ios-device-1", - publicKey: "pub", - platform: "iOS 18", - role: "operator", - roles: ["operator"], - createdAtMs: 1, - approvedAtMs: 1, - tokens: { - operator: { - token: "operator-token", - role: "operator", - scopes, - createdAtMs: 1, - }, - }, - }, - ], + paired, }); } +function mockPairedIosOperator(scopes: string[]) { + mockPairedIosOperators(pairedIosOperator({ scopes })); +} + vi.mock("../config/config.js", () => ({ getRuntimeConfig: () => ({ gateway: {} }), })); @@ -86,14 +149,7 @@ describe("createExecApprovalIosPushDelivery", () => { beforeEach(() => { vi.clearAllMocks(); listDevicePairingMock.mockResolvedValue({ pending: [], paired: [] }); - loadApnsRegistrationMock.mockResolvedValue({ - nodeId: "ios-device-1", - transport: "direct", - token: "apns-token", - topic: "ai.openclaw.ios.test", - environment: "sandbox", - updatedAtMs: 1, - }); + loadApnsRegistrationMock.mockResolvedValue(apnsRegistration()); loadApnsRegistrationsMock.mockImplementation(async (nodeIds: readonly string[]) => { const registrations = []; for (const nodeId of nodeIds) { @@ -104,62 +160,23 @@ describe("createExecApprovalIosPushDelivery", () => { } return registrations; }); - resolveApnsAuthConfigFromEnvMock.mockResolvedValue({ - ok: true, - value: { teamId: "team", keyId: "key", privateKey: "private-key" }, - }); + resolveApnsAuthConfigFromEnvMock.mockResolvedValue(resolvedApnsAuthConfig()); resolveApnsRelayConfigFromEnvMock.mockReturnValue({ ok: false, error: "unused" }); - sendApnsExecApprovalAlertMock.mockResolvedValue({ - ok: true, - status: 200, - environment: "sandbox", - topic: "ai.openclaw.ios.test", - tokenSuffix: "token", - transport: "direct", - }); - sendApnsExecApprovalResolvedWakeMock.mockResolvedValue({ - ok: true, - status: 200, - environment: "sandbox", - topic: "ai.openclaw.ios.test", - tokenSuffix: "token", - transport: "direct", - }); + sendApnsExecApprovalAlertMock.mockResolvedValue(successfulApnsPushResult()); + sendApnsExecApprovalResolvedWakeMock.mockResolvedValue(successfulApnsPushResult()); }); it("does not target iOS devices whose active operator token lacks operator.approvals", async () => { - listDevicePairingMock.mockResolvedValue({ - pending: [], - paired: [ - { - deviceId: "ios-device-1", - publicKey: "pub", - platform: "iOS 18", - role: "operator", - roles: ["operator"], - approvedScopes: ["operator.approvals"], - createdAtMs: 1, - approvedAtMs: 1, - tokens: { - operator: { - token: "operator-token", - role: "operator", - scopes: ["operator.read"], - createdAtMs: 1, - }, - }, - }, - ], - }); + mockPairedIosOperators( + pairedIosOperator({ + scopes: ["operator.read"], + approvedScopes: ["operator.approvals"], + }), + ); const delivery = createExecApprovalIosPushDelivery({ log: {} }); - const accepted = await delivery.handleRequested({ - id: "approval-1", - request: { command: "echo ok", host: "gateway", allowedDecisions: ["allow-once"] }, - createdAtMs: 1, - expiresAtMs: 2, - }); + const accepted = await delivery.handleRequested(approvalRequest("approval-1")); expect(accepted).toBe(false); expect(loadApnsRegistrationsMock).not.toHaveBeenCalled(); @@ -171,12 +188,7 @@ describe("createExecApprovalIosPushDelivery", () => { const delivery = createExecApprovalIosPushDelivery({ log: {} }); - const accepted = await delivery.handleRequested({ - id: "approval-2", - request: { command: "echo ok", host: "gateway", allowedDecisions: ["allow-once"] }, - createdAtMs: 1, - expiresAtMs: 2, - }); + const accepted = await delivery.handleRequested(approvalRequest("approval-2")); expect(accepted).toBe(true); expect(loadApnsRegistrationsMock).toHaveBeenCalledWith(["ios-device-1"]); @@ -184,54 +196,26 @@ describe("createExecApprovalIosPushDelivery", () => { }); it("loads APNs registrations in one bulk read for all visible iOS operators", async () => { - listDevicePairingMock.mockResolvedValue({ - pending: [], - paired: [ - { - deviceId: "ios-device-1", - publicKey: "pub-1", - platform: "iOS 18", - role: "operator", - roles: ["operator"], - createdAtMs: 1, - approvedAtMs: 1, - tokens: { - operator: { - token: "operator-token-1", - role: "operator", - scopes: ["operator.approvals"], - createdAtMs: 1, - }, - }, - }, - { - deviceId: "ios-device-2", - publicKey: "pub-2", - platform: "iPadOS 18", - role: "operator", - roles: ["operator"], - createdAtMs: 1, - approvedAtMs: 2, - tokens: { - operator: { - token: "operator-token-2", - role: "operator", - scopes: ["operator.approvals"], - createdAtMs: 1, - }, - }, - }, - ], - }); + mockPairedIosOperators( + pairedIosOperator({ + deviceId: "ios-device-1", + publicKey: "pub-1", + scopes: ["operator.approvals"], + token: "operator-token-1", + }), + pairedIosOperator({ + deviceId: "ios-device-2", + publicKey: "pub-2", + platform: "iPadOS 18", + approvedAtMs: 2, + scopes: ["operator.approvals"], + token: "operator-token-2", + }), + ); const delivery = createExecApprovalIosPushDelivery({ log: {} }); - await delivery.handleRequested({ - id: "approval-bulk-load", - request: { command: "echo ok", host: "gateway", allowedDecisions: ["allow-once"] }, - createdAtMs: 1, - expiresAtMs: 2, - }); + await delivery.handleRequested(approvalRequest("approval-bulk-load")); expect(loadApnsRegistrationsMock).toHaveBeenCalledTimes(1); expect(loadApnsRegistrationsMock).toHaveBeenCalledWith(["ios-device-1", "ios-device-2"]); @@ -243,15 +227,9 @@ describe("createExecApprovalIosPushDelivery", () => { const delivery = createExecApprovalIosPushDelivery({ log: {} }); - const accepted = await delivery.handleRequested( - { - id: "approval-filtered", - request: { command: "echo ok", host: "gateway", allowedDecisions: ["allow-once"] }, - createdAtMs: 1, - expiresAtMs: 2, - }, - { isTargetVisible }, - ); + const accepted = await delivery.handleRequested(approvalRequest("approval-filtered"), { + isTargetVisible, + }); expect(accepted).toBe(false); expect(isTargetVisible).toHaveBeenCalledWith({ @@ -277,12 +255,7 @@ describe("createExecApprovalIosPushDelivery", () => { const delivery = createExecApprovalIosPushDelivery({ log: { warn } }); - const accepted = await delivery.handleRequested({ - id: "approval-dead-route", - request: { command: "echo ok", host: "gateway", allowedDecisions: ["allow-once"] }, - createdAtMs: 1, - expiresAtMs: 2, - }); + const accepted = await delivery.handleRequested(approvalRequest("approval-dead-route")); expect(accepted).toBe(false); expect(sendApnsExecApprovalAlertMock).toHaveBeenCalledTimes(1); @@ -308,29 +281,13 @@ describe("createExecApprovalIosPushDelivery", () => { const delivery = createExecApprovalIosPushDelivery({ log: {} }); - const requested = delivery.handleRequested({ - id: "approval-ordered-cleanup", - request: { command: "echo ok", host: "gateway", allowedDecisions: ["allow-once"] }, - createdAtMs: 1, - expiresAtMs: 2, - }); - const resolved = delivery.handleResolved({ - id: "approval-ordered-cleanup", - decision: "allow-once", - ts: 1, - }); + const requested = delivery.handleRequested(approvalRequest("approval-ordered-cleanup")); + const resolved = delivery.handleResolved(approvalResolved("approval-ordered-cleanup")); await Promise.resolve(); expect(sendApnsExecApprovalResolvedWakeMock).not.toHaveBeenCalled(); - requestedPush.resolve({ - ok: true, - status: 200, - environment: "sandbox", - topic: "ai.openclaw.ios.test", - tokenSuffix: "token", - transport: "direct", - }); + requestedPush.resolve(successfulApnsPushResult()); await requested; await resolved; @@ -341,11 +298,7 @@ describe("createExecApprovalIosPushDelivery", () => { const debug = vi.fn(); const delivery = createExecApprovalIosPushDelivery({ log: { debug } }); - await delivery.handleResolved({ - id: "approval-missing-targets", - decision: "allow-once", - ts: 1, - }); + await delivery.handleResolved(approvalResolved("approval-missing-targets")); expect(debug).toHaveBeenCalledWith( "exec approvals: iOS cleanup push skipped approvalId=approval-missing-targets reason=missing-targets", @@ -360,31 +313,12 @@ describe("createExecApprovalIosPushDelivery", () => { const delivery = createExecApprovalIosPushDelivery({ log: {} }); - await delivery.handleRequested({ - id: "approval-cleanup", - request: { command: "echo ok", host: "gateway", allowedDecisions: ["allow-once"] }, - createdAtMs: 1, - expiresAtMs: 2, - }); + await delivery.handleRequested(approvalRequest("approval-cleanup")); vi.clearAllMocks(); - loadApnsRegistrationMock.mockResolvedValue({ - nodeId: "ios-device-1", - transport: "direct", - token: "apns-token", - topic: "ai.openclaw.ios.test", - environment: "sandbox", - updatedAtMs: 1, - }); - resolveApnsAuthConfigFromEnvMock.mockResolvedValue({ - ok: true, - value: { teamId: "team", keyId: "key", privateKey: "private-key" }, - }); + loadApnsRegistrationMock.mockResolvedValue(apnsRegistration()); + resolveApnsAuthConfigFromEnvMock.mockResolvedValue(resolvedApnsAuthConfig()); - await delivery.handleResolved({ - id: "approval-cleanup", - decision: "allow-once", - ts: 1, - }); + await delivery.handleResolved(approvalResolved("approval-cleanup")); expect(listDevicePairingMock).not.toHaveBeenCalled(); expect(loadApnsRegistrationsMock).toHaveBeenCalledWith(["ios-device-1"]);