refactor: share node invoke wake test helpers

This commit is contained in:
Vincent Koc
2026-06-02 03:02:03 +02:00
parent 489efc8f5e
commit 2f92fddef0

View File

@@ -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<string, unknown>,
label = "wake result",
) {
expectRecordFields(await maybeWakeNodeWithApns(nodeId), label, expected);
}
async function expectNudgeState(nodeId: string, expected: Record<string, unknown>) {
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);