mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-06 05:51:15 +08:00
refactor: share node invoke wake test helpers
This commit is contained in:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user