From 2f92fddef088e5d8d959614863f0629d49e5284b Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Tue, 2 Jun 2026 03:02:03 +0200 Subject: [PATCH] refactor: share node invoke wake test helpers --- .../server-methods/nodes.invoke-wake.test.ts | 218 +++++++++--------- 1 file changed, 106 insertions(+), 112 deletions(-) diff --git a/src/gateway/server-methods/nodes.invoke-wake.test.ts b/src/gateway/server-methods/nodes.invoke-wake.test.ts index 0d3f7a7010d9..35533ec76ab7 100644 --- a/src/gateway/server-methods/nodes.invoke-wake.test.ts +++ b/src/gateway/server-methods/nodes.invoke-wake.test.ts @@ -187,6 +187,48 @@ function expectQueuedAction( return expectRecordFields(actions[0], "queued action", expected); } +function expectWakeSendError(wake: unknown, reason: string, status: number) { + expectRecordFields(wake, "wake result", { + available: true, + throttled: false, + path: "send-error", + apnsReason: reason, + apnsStatus: status, + }); +} + +function expectNoAuthWake(wake: unknown, label: string, reason: string) { + expectRecordFields(wake, label, { + available: false, + throttled: false, + path: "no-auth", + apnsReason: reason, + }); +} + +async function expectWakeState( + nodeId: string, + expected: Record, + label = "wake result", +) { + expectRecordFields(await maybeWakeNodeWithApns(nodeId), label, expected); +} + +async function expectNudgeState(nodeId: string, expected: Record) { + expectRecordFields(await maybeSendNodeWakeNudge(nodeId), "nudge result", expected); +} + +async function expectWakeAndNudgeSent(nodeId: string) { + await expectWakeState(nodeId, { + path: "sent", + throttled: false, + }); + await expectNudgeState(nodeId, { + sent: true, + throttled: false, + }); +} + const WAKE_WAIT_TIMEOUT_MS = 3_001; const DEFAULT_RELAY_CONFIG = { baseUrl: "https://relay.example.com", @@ -341,6 +383,34 @@ function createNodeClient(nodeId: string, commands?: string[]) { }; } +function createForegroundUnavailableNodeRegistry(params: { + nodeId: string; + commands: string[]; + platform: string; +}) { + return { + get: vi.fn(() => ({ + nodeId: params.nodeId, + commands: params.commands, + platform: params.platform, + })), + invoke: vi.fn().mockResolvedValue({ + ok: false, + error: { + code: "NODE_BACKGROUND_UNAVAILABLE", + message: "NODE_BACKGROUND_UNAVAILABLE: canvas/camera/screen commands require foreground", + }, + }), + }; +} + +function createMissingNodeRegistry() { + return { + get: vi.fn(() => undefined), + invoke: vi.fn().mockResolvedValue({ ok: true }), + }; +} + async function pullPending(nodeId: string, commands?: string[]) { const respond = vi.fn(); await nodeHandlers["node.pending.pull"]({ @@ -508,10 +578,7 @@ describe("node.invoke APNs wake path", () => { it("keeps the existing not-connected response when wake path is unavailable", async () => { mocks.loadApnsRegistration.mockResolvedValue(null); - const nodeRegistry = { - get: vi.fn(() => undefined), - invoke: vi.fn().mockResolvedValue({ ok: true }), - }; + const nodeRegistry = createMissingNodeRegistry(); const respond = await invokeNode({ nodeRegistry }); const call = firstRespondCall(respond); @@ -532,18 +599,8 @@ describe("node.invoke APNs wake path", () => { const first = await maybeWakeNodeWithApns("ios-node-relay-no-auth"); const second = await maybeWakeNodeWithApns("ios-node-relay-no-auth"); - expectRecordFields(first, "first wake result", { - available: false, - throttled: false, - path: "no-auth", - apnsReason: "relay config missing", - }); - expectRecordFields(second, "second wake result", { - available: false, - throttled: false, - path: "no-auth", - apnsReason: "relay config missing", - }); + expectNoAuthWake(first, "first wake result", "relay config missing"); + expectNoAuthWake(second, "second wake result", "relay config missing"); expect(mocks.resolveApnsRelayConfigFromEnv).toHaveBeenCalledTimes(2); expect(mocks.sendApnsBackgroundWake).not.toHaveBeenCalled(); }); @@ -559,33 +616,19 @@ describe("node.invoke APNs wake path", () => { transport: "direct", }); - expectRecordFields(await maybeWakeNodeWithApns("ios-node-clear-wake"), "wake result", { - path: "sent", - throttled: false, - }); - expectRecordFields(await maybeSendNodeWakeNudge("ios-node-clear-wake"), "nudge result", { - sent: true, - throttled: false, - }); - expectRecordFields(await maybeWakeNodeWithApns("ios-node-clear-wake"), "wake result", { + await expectWakeAndNudgeSent("ios-node-clear-wake"); + await expectWakeState("ios-node-clear-wake", { path: "throttled", throttled: true, }); - expectRecordFields(await maybeSendNodeWakeNudge("ios-node-clear-wake"), "nudge result", { + await expectNudgeState("ios-node-clear-wake", { sent: false, throttled: true, }); clearNodeWakeState("ios-node-clear-wake"); - expectRecordFields(await maybeWakeNodeWithApns("ios-node-clear-wake"), "wake result", { - path: "sent", - throttled: false, - }); - expectRecordFields(await maybeSendNodeWakeNudge("ios-node-clear-wake"), "nudge result", { - sent: true, - throttled: false, - }); + await expectWakeAndNudgeSent("ios-node-clear-wake"); expect(mocks.sendApnsBackgroundWake).toHaveBeenCalledTimes(2); expect(mocks.sendApnsAlert).toHaveBeenCalledTimes(2); }); @@ -720,13 +763,7 @@ describe("node.invoke APNs wake path", () => { mocks.shouldClearStoredApnsRegistration.mockReturnValue(true); const wake = await maybeWakeNodeWithApns("ios-node-stale", { force: true }); - expectRecordFields(wake, "wake result", { - available: true, - throttled: false, - path: "send-error", - apnsReason: "BadDeviceToken", - apnsStatus: 400, - }); + expectWakeSendError(wake, "BadDeviceToken", 400); expect(mocks.clearApnsRegistrationIfCurrent).toHaveBeenCalledWith({ nodeId: "ios-node-stale", registration, @@ -743,13 +780,7 @@ describe("node.invoke APNs wake path", () => { mocks.shouldClearStoredApnsRegistration.mockReturnValue(false); const wake = await maybeWakeNodeWithApns("ios-node-relay", { force: true }); - expectRecordFields(wake, "wake result", { - available: true, - throttled: false, - path: "send-error", - apnsReason: "Unregistered", - apnsStatus: 410, - }); + expectWakeSendError(wake, "Unregistered", 410); expect(mocks.resolveApnsRelayConfigFromEnv).toHaveBeenCalledWith( process.env, { @@ -780,10 +811,7 @@ describe("node.invoke APNs wake path", () => { vi.useFakeTimers(); mockDirectWakeConfig("ios-node-throttle"); - const nodeRegistry = { - get: vi.fn(() => undefined), - invoke: vi.fn().mockResolvedValue({ ok: true }), - }; + const nodeRegistry = createMissingNodeRegistry(); const invokePromise = invokeNode({ nodeRegistry, @@ -799,20 +827,11 @@ describe("node.invoke APNs wake path", () => { it("queues iOS foreground-only command failures and keeps them until acked", async () => { mocks.loadApnsRegistration.mockResolvedValue(null); - const nodeRegistry = { - get: vi.fn(() => ({ - nodeId: "ios-node-queued", - commands: ["canvas.navigate"], - platform: "iOS 26.4.0", - })), - invoke: vi.fn().mockResolvedValue({ - ok: false, - error: { - code: "NODE_BACKGROUND_UNAVAILABLE", - message: "NODE_BACKGROUND_UNAVAILABLE: canvas/camera/screen commands require foreground", - }, - }), - }; + const nodeRegistry = createForegroundUnavailableNodeRegistry({ + nodeId: "ios-node-queued", + commands: ["canvas.navigate"], + platform: "iOS 26.4.0", + }); const respond = await invokeNode({ nodeRegistry, @@ -893,20 +912,11 @@ describe("node.invoke APNs wake path", () => { }, ); - const nodeRegistry = { - get: vi.fn(() => ({ - nodeId: "ios-node-policy", - commands: ["camera.snap", "canvas.navigate"], - platform: "iOS 26.4.0", - })), - invoke: vi.fn().mockResolvedValue({ - ok: false, - error: { - code: "NODE_BACKGROUND_UNAVAILABLE", - message: "NODE_BACKGROUND_UNAVAILABLE: canvas/camera/screen commands require foreground", - }, - }), - }; + const nodeRegistry = createForegroundUnavailableNodeRegistry({ + nodeId: "ios-node-policy", + commands: ["camera.snap", "canvas.navigate"], + platform: "iOS 26.4.0", + }); await invokeNode({ nodeRegistry, @@ -945,39 +955,23 @@ describe("node.invoke APNs wake path", () => { it("dedupes queued foreground actions by idempotency key", async () => { mocks.loadApnsRegistration.mockResolvedValue(null); - const nodeRegistry = { - get: vi.fn(() => ({ - nodeId: "ios-node-dedupe", - commands: ["canvas.navigate"], - platform: "iPadOS 26.4.0", - })), - invoke: vi.fn().mockResolvedValue({ - ok: false, - error: { - code: "NODE_BACKGROUND_UNAVAILABLE", - message: "NODE_BACKGROUND_UNAVAILABLE: canvas/camera/screen commands require foreground", - }, - }), - }; + const nodeRegistry = createForegroundUnavailableNodeRegistry({ + nodeId: "ios-node-dedupe", + commands: ["canvas.navigate"], + platform: "iPadOS 26.4.0", + }); - await invokeNode({ - nodeRegistry, - requestParams: { - nodeId: "ios-node-dedupe", - command: "canvas.navigate", - params: { url: "http://example.com/first" }, - idempotencyKey: "idem-dedupe", - }, - }); - await invokeNode({ - nodeRegistry, - requestParams: { - nodeId: "ios-node-dedupe", - command: "canvas.navigate", - params: { url: "http://example.com/first" }, - idempotencyKey: "idem-dedupe", - }, - }); + for (let attempt = 0; attempt < 2; attempt += 1) { + await invokeNode({ + nodeRegistry, + requestParams: { + nodeId: "ios-node-dedupe", + command: "canvas.navigate", + params: { url: "http://example.com/first" }, + idempotencyKey: "idem-dedupe", + }, + }); + } const pullRespond = await pullPending("ios-node-dedupe", ["canvas.navigate"]); const pullCall = firstRespondCall(pullRespond);