From e351a86290f7552a09b21a3dff3462fdd44b166f Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 13 Mar 2026 16:45:41 +0000 Subject: [PATCH] refactor: share node wake test apns fixtures --- .../server-methods/nodes.invoke-wake.test.ts | 219 ++++++++---------- 1 file changed, 97 insertions(+), 122 deletions(-) diff --git a/src/gateway/server-methods/nodes.invoke-wake.test.ts b/src/gateway/server-methods/nodes.invoke-wake.test.ts index 36d19a9a014..23976d71db0 100644 --- a/src/gateway/server-methods/nodes.invoke-wake.test.ts +++ b/src/gateway/server-methods/nodes.invoke-wake.test.ts @@ -59,6 +59,92 @@ type TestNodeSession = { }; const WAKE_WAIT_TIMEOUT_MS = 3_001; +const DEFAULT_RELAY_CONFIG = { + baseUrl: "https://relay.example.com", + timeoutMs: 1000, +} as const; +type WakeResultOverrides = Partial<{ + ok: boolean; + status: number; + reason: string; + tokenSuffix: string; + topic: string; + environment: "sandbox" | "production"; + transport: "direct" | "relay"; +}>; + +function directRegistration(nodeId: string) { + return { + nodeId, + transport: "direct" as const, + token: "abcd1234abcd1234abcd1234abcd1234", + topic: "ai.openclaw.ios", + environment: "sandbox" as const, + updatedAtMs: 1, + }; +} + +function relayRegistration(nodeId: string) { + return { + nodeId, + transport: "relay" as const, + relayHandle: "relay-handle-123", + sendGrant: "send-grant-123", + installationId: "install-123", + topic: "ai.openclaw.ios", + environment: "production" as const, + distribution: "official" as const, + updatedAtMs: 1, + tokenDebugSuffix: "abcd1234", + }; +} + +function mockDirectWakeConfig(nodeId: string, overrides: WakeResultOverrides = {}) { + mocks.loadApnsRegistration.mockResolvedValue(directRegistration(nodeId)); + mocks.resolveApnsAuthConfigFromEnv.mockResolvedValue({ + ok: true, + value: { + teamId: "TEAM123", + keyId: "KEY123", + privateKey: "-----BEGIN PRIVATE KEY-----\nabc\n-----END PRIVATE KEY-----", // pragma: allowlist secret + }, + }); + mocks.sendApnsBackgroundWake.mockResolvedValue({ + ok: true, + status: 200, + tokenSuffix: "1234abcd", + topic: "ai.openclaw.ios", + environment: "sandbox", + transport: "direct", + ...overrides, + }); +} + +function mockRelayWakeConfig(nodeId: string, overrides: WakeResultOverrides = {}) { + mocks.loadConfig.mockReturnValue({ + gateway: { + push: { + apns: { + relay: DEFAULT_RELAY_CONFIG, + }, + }, + }, + }); + mocks.loadApnsRegistration.mockResolvedValue(relayRegistration(nodeId)); + mocks.resolveApnsRelayConfigFromEnv.mockReturnValue({ + ok: true, + value: DEFAULT_RELAY_CONFIG, + }); + mocks.sendApnsBackgroundWake.mockResolvedValue({ + ok: true, + status: 200, + tokenSuffix: "abcd1234", + topic: "ai.openclaw.ios", + environment: "production", + transport: "relay", + ...overrides, + }); +} function makeNodeInvokeParams(overrides?: Partial>) { return { @@ -157,33 +243,6 @@ async function ackPending(nodeId: string, ids: string[]) { return respond; } -function mockSuccessfulWakeConfig(nodeId: string) { - mocks.loadApnsRegistration.mockResolvedValue({ - nodeId, - transport: "direct", - token: "abcd1234abcd1234abcd1234abcd1234", - topic: "ai.openclaw.ios", - environment: "sandbox", - updatedAtMs: 1, - }); - mocks.resolveApnsAuthConfigFromEnv.mockResolvedValue({ - ok: true, - value: { - teamId: "TEAM123", - keyId: "KEY123", - privateKey: "-----BEGIN PRIVATE KEY-----\nabc\n-----END PRIVATE KEY-----", // pragma: allowlist secret - }, - }); - mocks.sendApnsBackgroundWake.mockResolvedValue({ - ok: true, - status: 200, - tokenSuffix: "1234abcd", - topic: "ai.openclaw.ios", - environment: "sandbox", - transport: "direct", - }); -} - describe("node.invoke APNs wake path", () => { beforeEach(() => { mocks.loadConfig.mockClear(); @@ -227,18 +286,7 @@ describe("node.invoke APNs wake path", () => { }); it("does not throttle repeated relay wake attempts when relay config is missing", async () => { - mocks.loadApnsRegistration.mockResolvedValue({ - nodeId: "ios-node-relay-no-auth", - transport: "relay", - relayHandle: "relay-handle-123", - sendGrant: "send-grant-123", - installationId: "install-123", - topic: "ai.openclaw.ios", - environment: "production", - distribution: "official", - updatedAtMs: 1, - tokenDebugSuffix: "abcd1234", - }); + mocks.loadApnsRegistration.mockResolvedValue(relayRegistration("ios-node-relay-no-auth")); mocks.resolveApnsRelayConfigFromEnv.mockReturnValue({ ok: false, error: "relay config missing", @@ -265,7 +313,7 @@ describe("node.invoke APNs wake path", () => { it("wakes and retries invoke after the node reconnects", async () => { vi.useFakeTimers(); - mockSuccessfulWakeConfig("ios-node-reconnect"); + mockDirectWakeConfig("ios-node-reconnect"); let connected = false; const session: TestNodeSession = { nodeId: "ios-node-reconnect", commands: ["camera.capture"] }; @@ -308,30 +356,12 @@ describe("node.invoke APNs wake path", () => { }); it("clears stale registrations after an invalid device token wake failure", async () => { - mocks.loadApnsRegistration.mockResolvedValue({ - nodeId: "ios-node-stale", - transport: "direct", - token: "abcd1234abcd1234abcd1234abcd1234", - topic: "ai.openclaw.ios", - environment: "sandbox", - updatedAtMs: 1, - }); - mocks.resolveApnsAuthConfigFromEnv.mockResolvedValue({ - ok: true, - value: { - teamId: "TEAM123", - keyId: "KEY123", - privateKey: "-----BEGIN PRIVATE KEY-----\nabc\n-----END PRIVATE KEY-----", // pragma: allowlist secret - }, - }); - mocks.sendApnsBackgroundWake.mockResolvedValue({ + const registration = directRegistration("ios-node-stale"); + mocks.loadApnsRegistration.mockResolvedValue(registration); + mockDirectWakeConfig("ios-node-stale", { ok: false, status: 400, reason: "BadDeviceToken", - tokenSuffix: "1234abcd", - topic: "ai.openclaw.ios", - environment: "sandbox", - transport: "direct", }); mocks.shouldClearStoredApnsRegistration.mockReturnValue(true); @@ -350,57 +380,16 @@ describe("node.invoke APNs wake path", () => { expect(call?.[2]?.message).toBe("node not connected"); expect(mocks.clearApnsRegistrationIfCurrent).toHaveBeenCalledWith({ nodeId: "ios-node-stale", - registration: { - nodeId: "ios-node-stale", - transport: "direct", - token: "abcd1234abcd1234abcd1234abcd1234", - topic: "ai.openclaw.ios", - environment: "sandbox", - updatedAtMs: 1, - }, + registration, }); }); it("does not clear relay registrations from wake failures", async () => { - mocks.loadConfig.mockReturnValue({ - gateway: { - push: { - apns: { - relay: { - baseUrl: "https://relay.example.com", - timeoutMs: 1000, - }, - }, - }, - }, - }); - mocks.loadApnsRegistration.mockResolvedValue({ - nodeId: "ios-node-relay", - transport: "relay", - relayHandle: "relay-handle-123", - sendGrant: "send-grant-123", - installationId: "install-123", - topic: "ai.openclaw.ios", - environment: "production", - distribution: "official", - updatedAtMs: 1, - tokenDebugSuffix: "abcd1234", - }); - mocks.resolveApnsRelayConfigFromEnv.mockReturnValue({ - ok: true, - value: { - baseUrl: "https://relay.example.com", - timeoutMs: 1000, - }, - }); - mocks.sendApnsBackgroundWake.mockResolvedValue({ + const registration = relayRegistration("ios-node-relay"); + mockRelayWakeConfig("ios-node-relay", { ok: false, status: 410, reason: "Unregistered", - tokenSuffix: "abcd1234", - topic: "ai.openclaw.ios", - environment: "production", - transport: "relay", }); mocks.shouldClearStoredApnsRegistration.mockReturnValue(false); @@ -420,26 +409,12 @@ describe("node.invoke APNs wake path", () => { expect(mocks.resolveApnsRelayConfigFromEnv).toHaveBeenCalledWith(process.env, { push: { apns: { - relay: { - baseUrl: "https://relay.example.com", - timeoutMs: 1000, - }, + relay: DEFAULT_RELAY_CONFIG, }, }, }); expect(mocks.shouldClearStoredApnsRegistration).toHaveBeenCalledWith({ - registration: { - nodeId: "ios-node-relay", - transport: "relay", - relayHandle: "relay-handle-123", - sendGrant: "send-grant-123", - installationId: "install-123", - topic: "ai.openclaw.ios", - environment: "production", - distribution: "official", - updatedAtMs: 1, - tokenDebugSuffix: "abcd1234", - }, + registration, result: { ok: false, status: 410, @@ -455,7 +430,7 @@ describe("node.invoke APNs wake path", () => { it("forces one retry wake when the first wake still fails to reconnect", async () => { vi.useFakeTimers(); - mockSuccessfulWakeConfig("ios-node-throttle"); + mockDirectWakeConfig("ios-node-throttle"); const nodeRegistry = { get: vi.fn(() => undefined),