From 177654f526b27183b551238b3c710de587e0f6c8 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 19 Feb 2026 13:45:34 +0000 Subject: [PATCH] refactor: dedupe APNs push send flow and add wake default test --- src/infra/push-apns.test.ts | 34 +++++++++++++ src/infra/push-apns.ts | 99 ++++++++++++++++++++----------------- 2 files changed, 88 insertions(+), 45 deletions(-) diff --git a/src/infra/push-apns.test.ts b/src/infra/push-apns.test.ts index 7265a521573..1e72a3f2439 100644 --- a/src/infra/push-apns.test.ts +++ b/src/infra/push-apns.test.ts @@ -190,4 +190,38 @@ describe("push APNs send semantics", () => { expect(result.ok).toBe(true); expect(result.environment).toBe("production"); }); + + it("defaults background wake reason when not provided", async () => { + const send = vi.fn().mockResolvedValue({ + status: 200, + apnsId: "apns-wake-default-reason-id", + body: "", + }); + + await sendApnsBackgroundWake({ + auth: { + teamId: "TEAM123", + keyId: "KEY123", + privateKey: testAuthPrivateKey, + }, + registration: { + nodeId: "ios-node-wake-default-reason", + token: "ABCD1234ABCD1234ABCD1234ABCD1234", + topic: "ai.openclaw.ios", + environment: "sandbox", + updatedAtMs: 1, + }, + nodeId: "ios-node-wake-default-reason", + requestSender: send, + }); + + const sent = send.mock.calls[0]?.[0]; + expect(sent?.payload).toMatchObject({ + openclaw: { + kind: "node.wake", + reason: "node.invoke", + nodeId: "ios-node-wake-default-reason", + }, + }); + }); }); diff --git a/src/infra/push-apns.ts b/src/infra/push-apns.ts index 607458c3bd1..0da3e1f429b 100644 --- a/src/infra/push-apns.ts +++ b/src/infra/push-apns.ts @@ -425,6 +425,46 @@ function toApnsPushResult(params: { }; } +function createOpenClawPushMetadata(params: { + kind: "push.test" | "node.wake"; + nodeId: string; + reason?: string; +}): { kind: "push.test" | "node.wake"; nodeId: string; ts: number; reason?: string } { + return { + kind: params.kind, + nodeId: params.nodeId, + ts: Date.now(), + ...(params.reason ? { reason: params.reason } : {}), + }; +} + +async function sendApnsPush(params: { + auth: ApnsAuthConfig; + registration: ApnsRegistration; + payload: object; + timeoutMs?: number; + requestSender?: ApnsRequestSender; + pushType: ApnsPushType; + priority: "10" | "5"; +}): Promise { + const { token, topic, environment, bearerToken } = resolveApnsSendContext({ + auth: params.auth, + registration: params.registration, + }); + const sender = params.requestSender ?? sendApnsRequest; + const response = await sender({ + token, + topic, + environment, + bearerToken, + payload: params.payload, + timeoutMs: resolveApnsTimeoutMs(params.timeoutMs), + pushType: params.pushType, + priority: params.priority, + }); + return toApnsPushResult({ response, token, topic, environment }); +} + export async function sendApnsAlert(params: { auth: ApnsAuthConfig; registration: ApnsRegistration; @@ -434,11 +474,6 @@ export async function sendApnsAlert(params: { timeoutMs?: number; requestSender?: ApnsRequestSender; }): Promise { - const { token, topic, environment, bearerToken } = resolveApnsSendContext({ - auth: params.auth, - registration: params.registration, - }); - const payload = { aps: { alert: { @@ -447,31 +482,21 @@ export async function sendApnsAlert(params: { }, sound: "default", }, - openclaw: { + openclaw: createOpenClawPushMetadata({ kind: "push.test", nodeId: params.nodeId, - ts: Date.now(), - }, + }), }; - const sender = params.requestSender ?? sendApnsRequest; - const response = await sender({ - token, - topic, - environment, - bearerToken, + return await sendApnsPush({ + auth: params.auth, + registration: params.registration, payload, - timeoutMs: resolveApnsTimeoutMs(params.timeoutMs), + timeoutMs: params.timeoutMs, + requestSender: params.requestSender, pushType: "alert", priority: "10", }); - - return toApnsPushResult({ - response, - token, - topic, - environment, - }); } export async function sendApnsBackgroundWake(params: { @@ -482,39 +507,23 @@ export async function sendApnsBackgroundWake(params: { timeoutMs?: number; requestSender?: ApnsRequestSender; }): Promise { - const { token, topic, environment, bearerToken } = resolveApnsSendContext({ - auth: params.auth, - registration: params.registration, - }); - const payload = { aps: { "content-available": 1, }, - openclaw: { + openclaw: createOpenClawPushMetadata({ kind: "node.wake", reason: params.wakeReason ?? "node.invoke", nodeId: params.nodeId, - ts: Date.now(), - }, + }), }; - - const sender = params.requestSender ?? sendApnsRequest; - const response = await sender({ - token, - topic, - environment, - bearerToken, + return await sendApnsPush({ + auth: params.auth, + registration: params.registration, payload, - timeoutMs: resolveApnsTimeoutMs(params.timeoutMs), + timeoutMs: params.timeoutMs, + requestSender: params.requestSender, pushType: "background", priority: "5", }); - - return toApnsPushResult({ - response, - token, - topic, - environment, - }); }