iOS/Gateway: stabilize background wake and reconnect behavior (#21226)

Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: 7705a7741e
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Reviewed-by: @mbelinky
This commit is contained in:
Mariano
2026-02-19 20:20:28 +00:00
committed by GitHub
parent f7a8c2df2c
commit e98ccc8e17
10 changed files with 604 additions and 54 deletions

View File

@@ -13,6 +13,7 @@ const mocks = vi.hoisted(() => ({
loadApnsRegistration: vi.fn(),
resolveApnsAuthConfigFromEnv: vi.fn(),
sendApnsBackgroundWake: vi.fn(),
sendApnsAlert: vi.fn(),
}));
vi.mock("../../config/config.js", () => ({
@@ -32,6 +33,7 @@ vi.mock("../../infra/push-apns.js", () => ({
loadApnsRegistration: mocks.loadApnsRegistration,
resolveApnsAuthConfigFromEnv: mocks.resolveApnsAuthConfigFromEnv,
sendApnsBackgroundWake: mocks.sendApnsBackgroundWake,
sendApnsAlert: mocks.sendApnsAlert,
}));
type RespondCall = [
@@ -81,12 +83,17 @@ async function invokeNode(params: {
requestParams?: Partial<Record<string, unknown>>;
}) {
const respond = vi.fn();
const logGateway = {
info: vi.fn(),
warn: vi.fn(),
};
await nodeHandlers["node.invoke"]({
params: makeNodeInvokeParams(params.requestParams),
respond: respond as never,
context: {
nodeRegistry: params.nodeRegistry,
execApprovalManager: undefined,
logGateway,
} as never,
client: null,
req: { type: "req", id: "req-node-invoke", method: "node.invoke" },
@@ -135,6 +142,7 @@ describe("node.invoke APNs wake path", () => {
mocks.loadApnsRegistration.mockReset();
mocks.resolveApnsAuthConfigFromEnv.mockReset();
mocks.sendApnsBackgroundWake.mockReset();
mocks.sendApnsAlert.mockReset();
});
afterEach(() => {
@@ -202,7 +210,7 @@ describe("node.invoke APNs wake path", () => {
expect(call?.[1]).toMatchObject({ ok: true, nodeId: "ios-node-reconnect" });
});
it("throttles repeated wake attempts for the same disconnected node", async () => {
it("forces one retry wake when the first wake still fails to reconnect", async () => {
vi.useFakeTimers();
mockSuccessfulWakeConfig("ios-node-throttle");
@@ -211,21 +219,14 @@ describe("node.invoke APNs wake path", () => {
invoke: vi.fn().mockResolvedValue({ ok: true }),
};
const first = invokeNode({
const invokePromise = invokeNode({
nodeRegistry,
requestParams: { nodeId: "ios-node-throttle", idempotencyKey: "idem-throttle-1" },
});
await vi.advanceTimersByTimeAsync(WAKE_WAIT_TIMEOUT_MS);
await first;
await vi.advanceTimersByTimeAsync(20_000);
await invokePromise;
const second = invokeNode({
nodeRegistry,
requestParams: { nodeId: "ios-node-throttle", idempotencyKey: "idem-throttle-2" },
});
await vi.advanceTimersByTimeAsync(WAKE_WAIT_TIMEOUT_MS);
await second;
expect(mocks.sendApnsBackgroundWake).toHaveBeenCalledTimes(1);
expect(mocks.sendApnsBackgroundWake).toHaveBeenCalledTimes(2);
expect(nodeRegistry.invoke).not.toHaveBeenCalled();
});
});