From 369430f9ab98af384f1e2342529eb88bf9acfdc7 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 13 Mar 2026 16:53:14 +0000 Subject: [PATCH 1/4] refactor: share tlon upload test mocks --- extensions/tlon/src/urbit/upload.test.ts | 113 ++++++++++------------- 1 file changed, 51 insertions(+), 62 deletions(-) diff --git a/extensions/tlon/src/urbit/upload.test.ts b/extensions/tlon/src/urbit/upload.test.ts index ca95a0412d4..1a573a6b359 100644 --- a/extensions/tlon/src/urbit/upload.test.ts +++ b/extensions/tlon/src/urbit/upload.test.ts @@ -15,6 +15,36 @@ vi.mock("@tloncorp/api", () => ({ })); describe("uploadImageFromUrl", () => { + async function loadUploadMocks() { + const { fetchWithSsrFGuard } = await import("openclaw/plugin-sdk/tlon"); + const { uploadFile } = await import("@tloncorp/api"); + const { uploadImageFromUrl } = await import("./upload.js"); + return { + mockFetch: vi.mocked(fetchWithSsrFGuard), + mockUploadFile: vi.mocked(uploadFile), + uploadImageFromUrl, + }; + } + + type UploadMocks = Awaited>; + + function mockSuccessfulFetch(params: { + mockFetch: UploadMocks["mockFetch"]; + blob: Blob; + finalUrl: string; + contentType: string; + }) { + params.mockFetch.mockResolvedValue({ + response: { + ok: true, + headers: new Headers({ "content-type": params.contentType }), + blob: () => Promise.resolve(params.blob), + } as unknown as Response, + finalUrl: params.finalUrl, + release: vi.fn().mockResolvedValue(undefined), + }); + } + beforeEach(() => { vi.clearAllMocks(); }); @@ -24,28 +54,17 @@ describe("uploadImageFromUrl", () => { }); it("fetches image and calls uploadFile, returns uploaded URL", async () => { - const { fetchWithSsrFGuard } = await import("openclaw/plugin-sdk/tlon"); - const mockFetch = vi.mocked(fetchWithSsrFGuard); + const { mockFetch, mockUploadFile, uploadImageFromUrl } = await loadUploadMocks(); - const { uploadFile } = await import("@tloncorp/api"); - const mockUploadFile = vi.mocked(uploadFile); - - // Mock fetchWithSsrFGuard to return a successful response with a blob const mockBlob = new Blob(["fake-image"], { type: "image/png" }); - mockFetch.mockResolvedValue({ - response: { - ok: true, - headers: new Headers({ "content-type": "image/png" }), - blob: () => Promise.resolve(mockBlob), - } as unknown as Response, + mockSuccessfulFetch({ + mockFetch, + blob: mockBlob, finalUrl: "https://example.com/image.png", - release: vi.fn().mockResolvedValue(undefined), + contentType: "image/png", }); - - // Mock uploadFile to return a successful upload mockUploadFile.mockResolvedValue({ url: "https://memex.tlon.network/uploaded.png" }); - const { uploadImageFromUrl } = await import("./upload.js"); const result = await uploadImageFromUrl("https://example.com/image.png"); expect(result).toBe("https://memex.tlon.network/uploaded.png"); @@ -59,10 +78,8 @@ describe("uploadImageFromUrl", () => { }); it("returns original URL if fetch fails", async () => { - const { fetchWithSsrFGuard } = await import("openclaw/plugin-sdk/tlon"); - const mockFetch = vi.mocked(fetchWithSsrFGuard); + const { mockFetch, uploadImageFromUrl } = await loadUploadMocks(); - // Mock fetchWithSsrFGuard to return a failed response mockFetch.mockResolvedValue({ response: { ok: false, @@ -72,35 +89,23 @@ describe("uploadImageFromUrl", () => { release: vi.fn().mockResolvedValue(undefined), }); - const { uploadImageFromUrl } = await import("./upload.js"); const result = await uploadImageFromUrl("https://example.com/image.png"); expect(result).toBe("https://example.com/image.png"); }); it("returns original URL if upload fails", async () => { - const { fetchWithSsrFGuard } = await import("openclaw/plugin-sdk/tlon"); - const mockFetch = vi.mocked(fetchWithSsrFGuard); + const { mockFetch, mockUploadFile, uploadImageFromUrl } = await loadUploadMocks(); - const { uploadFile } = await import("@tloncorp/api"); - const mockUploadFile = vi.mocked(uploadFile); - - // Mock fetchWithSsrFGuard to return a successful response const mockBlob = new Blob(["fake-image"], { type: "image/png" }); - mockFetch.mockResolvedValue({ - response: { - ok: true, - headers: new Headers({ "content-type": "image/png" }), - blob: () => Promise.resolve(mockBlob), - } as unknown as Response, + mockSuccessfulFetch({ + mockFetch, + blob: mockBlob, finalUrl: "https://example.com/image.png", - release: vi.fn().mockResolvedValue(undefined), + contentType: "image/png", }); - - // Mock uploadFile to throw an error mockUploadFile.mockRejectedValue(new Error("Upload failed")); - const { uploadImageFromUrl } = await import("./upload.js"); const result = await uploadImageFromUrl("https://example.com/image.png"); expect(result).toBe("https://example.com/image.png"); @@ -127,26 +132,18 @@ describe("uploadImageFromUrl", () => { }); it("extracts filename from URL path", async () => { - const { fetchWithSsrFGuard } = await import("openclaw/plugin-sdk/tlon"); - const mockFetch = vi.mocked(fetchWithSsrFGuard); - - const { uploadFile } = await import("@tloncorp/api"); - const mockUploadFile = vi.mocked(uploadFile); + const { mockFetch, mockUploadFile, uploadImageFromUrl } = await loadUploadMocks(); const mockBlob = new Blob(["fake-image"], { type: "image/jpeg" }); - mockFetch.mockResolvedValue({ - response: { - ok: true, - headers: new Headers({ "content-type": "image/jpeg" }), - blob: () => Promise.resolve(mockBlob), - } as unknown as Response, + mockSuccessfulFetch({ + mockFetch, + blob: mockBlob, finalUrl: "https://example.com/path/to/my-image.jpg", - release: vi.fn().mockResolvedValue(undefined), + contentType: "image/jpeg", }); mockUploadFile.mockResolvedValue({ url: "https://memex.tlon.network/uploaded.jpg" }); - const { uploadImageFromUrl } = await import("./upload.js"); await uploadImageFromUrl("https://example.com/path/to/my-image.jpg"); expect(mockUploadFile).toHaveBeenCalledWith( @@ -157,26 +154,18 @@ describe("uploadImageFromUrl", () => { }); it("uses default filename when URL has no path", async () => { - const { fetchWithSsrFGuard } = await import("openclaw/plugin-sdk/tlon"); - const mockFetch = vi.mocked(fetchWithSsrFGuard); - - const { uploadFile } = await import("@tloncorp/api"); - const mockUploadFile = vi.mocked(uploadFile); + const { mockFetch, mockUploadFile, uploadImageFromUrl } = await loadUploadMocks(); const mockBlob = new Blob(["fake-image"], { type: "image/png" }); - mockFetch.mockResolvedValue({ - response: { - ok: true, - headers: new Headers({ "content-type": "image/png" }), - blob: () => Promise.resolve(mockBlob), - } as unknown as Response, + mockSuccessfulFetch({ + mockFetch, + blob: mockBlob, finalUrl: "https://example.com/", - release: vi.fn().mockResolvedValue(undefined), + contentType: "image/png", }); mockUploadFile.mockResolvedValue({ url: "https://memex.tlon.network/uploaded.png" }); - const { uploadImageFromUrl } = await import("./upload.js"); await uploadImageFromUrl("https://example.com/"); expect(mockUploadFile).toHaveBeenCalledWith( From 4a00cefe63cbe697704819379fc0bacd44d45783 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 13 Mar 2026 16:53:31 +0000 Subject: [PATCH 2/4] refactor: share outbound plugin test results --- .../outbound/outbound-send-service.test.ts | 42 +++++++------------ 1 file changed, 15 insertions(+), 27 deletions(-) diff --git a/src/infra/outbound/outbound-send-service.test.ts b/src/infra/outbound/outbound-send-service.test.ts index ae12622fcae..68c956d93fc 100644 --- a/src/infra/outbound/outbound-send-service.test.ts +++ b/src/infra/outbound/outbound-send-service.test.ts @@ -34,6 +34,18 @@ vi.mock("../../config/sessions.js", () => ({ import { executePollAction, executeSendAction } from "./outbound-send-service.js"; describe("executeSendAction", () => { + function pluginActionResult(messageId: string) { + return { + ok: true, + value: { messageId }, + continuePrompt: "", + output: "", + sessionId: "s1", + model: "gpt-5.2", + usage: {}, + }; + } + beforeEach(() => { mocks.dispatchChannelMessageAction.mockClear(); mocks.sendMessage.mockClear(); @@ -75,15 +87,7 @@ describe("executeSendAction", () => { }); it("uses plugin poll action when available", async () => { - mocks.dispatchChannelMessageAction.mockResolvedValue({ - ok: true, - value: { messageId: "poll-plugin" }, - continuePrompt: "", - output: "", - sessionId: "s1", - model: "gpt-5.2", - usage: {}, - }); + mocks.dispatchChannelMessageAction.mockResolvedValue(pluginActionResult("poll-plugin")); const result = await executePollAction({ ctx: { @@ -103,15 +107,7 @@ describe("executeSendAction", () => { }); it("passes agent-scoped media local roots to plugin dispatch", async () => { - mocks.dispatchChannelMessageAction.mockResolvedValue({ - ok: true, - value: { messageId: "msg-plugin" }, - continuePrompt: "", - output: "", - sessionId: "s1", - model: "gpt-5.2", - usage: {}, - }); + mocks.dispatchChannelMessageAction.mockResolvedValue(pluginActionResult("msg-plugin")); await executeSendAction({ ctx: { @@ -134,15 +130,7 @@ describe("executeSendAction", () => { }); it("passes mirror idempotency keys through plugin-handled sends", async () => { - mocks.dispatchChannelMessageAction.mockResolvedValue({ - ok: true, - value: { messageId: "msg-plugin" }, - continuePrompt: "", - output: "", - sessionId: "s1", - model: "gpt-5.2", - usage: {}, - }); + mocks.dispatchChannelMessageAction.mockResolvedValue(pluginActionResult("msg-plugin")); await executeSendAction({ ctx: { From 8de94abfbc9b48d1ac8aae722cef074e5c8be295 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 13 Mar 2026 16:55:23 +0000 Subject: [PATCH 3/4] refactor: share chat abort test helpers --- .../chat.abort-authorization.test.ts | 96 ++++++------------ .../chat.abort-persistence.test.ts | 97 +++++++------------ .../server-methods/chat.abort.test-helpers.ts | 69 +++++++++++++ 3 files changed, 132 insertions(+), 130 deletions(-) create mode 100644 src/gateway/server-methods/chat.abort.test-helpers.ts diff --git a/src/gateway/server-methods/chat.abort-authorization.test.ts b/src/gateway/server-methods/chat.abort-authorization.test.ts index 6fbf0478df3..607e80b58ff 100644 --- a/src/gateway/server-methods/chat.abort-authorization.test.ts +++ b/src/gateway/server-methods/chat.abort-authorization.test.ts @@ -1,68 +1,24 @@ -import { describe, expect, it, vi } from "vitest"; +import { describe, expect, it } from "vitest"; +import { + createActiveRun, + createChatAbortContext, + invokeChatAbortHandler, +} from "./chat.abort.test-helpers.js"; import { chatHandlers } from "./chat.js"; -function createActiveRun(sessionKey: string, owner?: { connId?: string; deviceId?: string }) { - const now = Date.now(); - return { - controller: new AbortController(), - sessionId: `${sessionKey}-session`, - sessionKey, - startedAtMs: now, - expiresAtMs: now + 30_000, - ownerConnId: owner?.connId, - ownerDeviceId: owner?.deviceId, - }; -} - -function createContext(overrides: Record = {}) { - return { - chatAbortControllers: new Map(), - chatRunBuffers: new Map(), - chatDeltaSentAt: new Map(), - chatAbortedRuns: new Map(), - removeChatRun: vi - .fn() - .mockImplementation((run: string) => ({ sessionKey: "main", clientRunId: run })), - agentRunSeq: new Map(), - broadcast: vi.fn(), - nodeSendToSession: vi.fn(), - logGateway: { warn: vi.fn() }, - ...overrides, - }; -} - -async function invokeChatAbort(params: { - context: ReturnType; - request: { sessionKey: string; runId?: string }; - client?: { - connId?: string; - connect?: { - device?: { id?: string }; - scopes?: string[]; - }; - } | null; -}) { - const respond = vi.fn(); - await chatHandlers["chat.abort"]({ - params: params.request, - respond: respond as never, - context: params.context as never, - req: {} as never, - client: (params.client ?? null) as never, - isWebchatConnect: () => false, - }); - return respond; -} - describe("chat.abort authorization", () => { it("rejects explicit run aborts from other clients", async () => { - const context = createContext({ + const context = createChatAbortContext({ chatAbortControllers: new Map([ - ["run-1", createActiveRun("main", { connId: "conn-owner", deviceId: "dev-owner" })], + [ + "run-1", + createActiveRun("main", { owner: { connId: "conn-owner", deviceId: "dev-owner" } }), + ], ]), }); - const respond = await invokeChatAbort({ + const respond = await invokeChatAbortHandler({ + handler: chatHandlers["chat.abort"], context, request: { sessionKey: "main", runId: "run-1" }, client: { @@ -79,13 +35,14 @@ describe("chat.abort authorization", () => { }); it("allows the same paired device to abort after reconnecting", async () => { - const context = createContext({ + const context = createChatAbortContext({ chatAbortControllers: new Map([ - ["run-1", createActiveRun("main", { connId: "conn-old", deviceId: "dev-1" })], + ["run-1", createActiveRun("main", { owner: { connId: "conn-old", deviceId: "dev-1" } })], ]), }); - const respond = await invokeChatAbort({ + const respond = await invokeChatAbortHandler({ + handler: chatHandlers["chat.abort"], context, request: { sessionKey: "main", runId: "run-1" }, client: { @@ -101,14 +58,15 @@ describe("chat.abort authorization", () => { }); it("only aborts session-scoped runs owned by the requester", async () => { - const context = createContext({ + const context = createChatAbortContext({ chatAbortControllers: new Map([ - ["run-mine", createActiveRun("main", { deviceId: "dev-1" })], - ["run-other", createActiveRun("main", { deviceId: "dev-2" })], + ["run-mine", createActiveRun("main", { owner: { deviceId: "dev-1" } })], + ["run-other", createActiveRun("main", { owner: { deviceId: "dev-2" } })], ]), }); - const respond = await invokeChatAbort({ + const respond = await invokeChatAbortHandler({ + handler: chatHandlers["chat.abort"], context, request: { sessionKey: "main" }, client: { @@ -125,13 +83,17 @@ describe("chat.abort authorization", () => { }); it("allows operator.admin clients to bypass owner checks", async () => { - const context = createContext({ + const context = createChatAbortContext({ chatAbortControllers: new Map([ - ["run-1", createActiveRun("main", { connId: "conn-owner", deviceId: "dev-owner" })], + [ + "run-1", + createActiveRun("main", { owner: { connId: "conn-owner", deviceId: "dev-owner" } }), + ], ]), }); - const respond = await invokeChatAbort({ + const respond = await invokeChatAbortHandler({ + handler: chatHandlers["chat.abort"], context, request: { sessionKey: "main", runId: "run-1" }, client: { diff --git a/src/gateway/server-methods/chat.abort-persistence.test.ts b/src/gateway/server-methods/chat.abort-persistence.test.ts index b7add3740eb..31a00a3f186 100644 --- a/src/gateway/server-methods/chat.abort-persistence.test.ts +++ b/src/gateway/server-methods/chat.abort-persistence.test.ts @@ -3,6 +3,11 @@ import os from "node:os"; import path from "node:path"; import { CURRENT_SESSION_VERSION } from "@mariozechner/pi-coding-agent"; import { afterEach, describe, expect, it, vi } from "vitest"; +import { + createActiveRun, + createChatAbortContext, + invokeChatAbortHandler, +} from "./chat.abort.test-helpers.js"; type TranscriptLine = { message?: Record; @@ -31,17 +36,6 @@ vi.mock("../session-utils.js", async (importOriginal) => { const { chatHandlers } = await import("./chat.js"); -function createActiveRun(sessionKey: string, sessionId: string) { - const now = Date.now(); - return { - controller: new AbortController(), - sessionId, - sessionKey, - startedAtMs: now, - expiresAtMs: now + 30_000, - }; -} - async function writeTranscriptHeader(transcriptPath: string, sessionId: string) { const header = { type: "session", @@ -81,49 +75,6 @@ async function createTranscriptFixture(prefix: string) { return { transcriptPath, sessionId }; } -function createChatAbortContext(overrides: Record = {}): { - chatAbortControllers: Map>; - chatRunBuffers: Map; - chatDeltaSentAt: Map; - chatAbortedRuns: Map; - removeChatRun: ReturnType; - agentRunSeq: Map; - broadcast: ReturnType; - nodeSendToSession: ReturnType; - logGateway: { warn: ReturnType }; - dedupe?: { get: ReturnType }; -} { - return { - chatAbortControllers: new Map(), - chatRunBuffers: new Map(), - chatDeltaSentAt: new Map(), - chatAbortedRuns: new Map(), - removeChatRun: vi - .fn() - .mockImplementation((run: string) => ({ sessionKey: "main", clientRunId: run })), - agentRunSeq: new Map(), - broadcast: vi.fn(), - nodeSendToSession: vi.fn(), - logGateway: { warn: vi.fn() }, - ...overrides, - }; -} - -async function invokeChatAbort( - context: ReturnType, - params: { sessionKey: string; runId?: string }, - respond: ReturnType, -) { - await chatHandlers["chat.abort"]({ - params, - respond: respond as never, - context: context as never, - req: {} as never, - client: null, - isWebchatConnect: () => false, - }); -} - afterEach(() => { vi.restoreAllMocks(); }); @@ -134,7 +85,7 @@ describe("chat abort transcript persistence", () => { const runId = "idem-abort-run-1"; const respond = vi.fn(); const context = createChatAbortContext({ - chatAbortControllers: new Map([[runId, createActiveRun("main", sessionId)]]), + chatAbortControllers: new Map([[runId, createActiveRun("main", { sessionId })]]), chatRunBuffers: new Map([[runId, "Partial from run abort"]]), chatDeltaSentAt: new Map([[runId, Date.now()]]), removeChatRun: vi @@ -149,17 +100,27 @@ describe("chat abort transcript persistence", () => { logGateway: { warn: vi.fn() }, }); - await invokeChatAbort(context, { sessionKey: "main", runId }, respond); + await invokeChatAbortHandler({ + handler: chatHandlers["chat.abort"], + context, + request: { sessionKey: "main", runId }, + respond, + }); const [ok1, payload1] = respond.mock.calls.at(-1) ?? []; expect(ok1).toBe(true); expect(payload1).toMatchObject({ aborted: true, runIds: [runId] }); - context.chatAbortControllers.set(runId, createActiveRun("main", sessionId)); + context.chatAbortControllers.set(runId, createActiveRun("main", { sessionId })); context.chatRunBuffers.set(runId, "Partial from run abort"); context.chatDeltaSentAt.set(runId, Date.now()); - await invokeChatAbort(context, { sessionKey: "main", runId }, respond); + await invokeChatAbortHandler({ + handler: chatHandlers["chat.abort"], + context, + request: { sessionKey: "main", runId }, + respond, + }); const lines = await readTranscriptLines(transcriptPath); const persisted = lines @@ -188,8 +149,8 @@ describe("chat abort transcript persistence", () => { const respond = vi.fn(); const context = createChatAbortContext({ chatAbortControllers: new Map([ - ["run-a", createActiveRun("main", sessionId)], - ["run-b", createActiveRun("main", sessionId)], + ["run-a", createActiveRun("main", { sessionId })], + ["run-b", createActiveRun("main", { sessionId })], ]), chatRunBuffers: new Map([ ["run-a", "Session abort partial"], @@ -201,7 +162,12 @@ describe("chat abort transcript persistence", () => { ]), }); - await invokeChatAbort(context, { sessionKey: "main" }, respond); + await invokeChatAbortHandler({ + handler: chatHandlers["chat.abort"], + context, + request: { sessionKey: "main" }, + respond, + }); const [ok, payload] = respond.mock.calls.at(-1) ?? []; expect(ok).toBe(true); @@ -280,12 +246,17 @@ describe("chat abort transcript persistence", () => { const runId = "idem-abort-run-blank"; const respond = vi.fn(); const context = createChatAbortContext({ - chatAbortControllers: new Map([[runId, createActiveRun("main", sessionId)]]), + chatAbortControllers: new Map([[runId, createActiveRun("main", { sessionId })]]), chatRunBuffers: new Map([[runId, " \n\t "]]), chatDeltaSentAt: new Map([[runId, Date.now()]]), }); - await invokeChatAbort(context, { sessionKey: "main", runId }, respond); + await invokeChatAbortHandler({ + handler: chatHandlers["chat.abort"], + context, + request: { sessionKey: "main", runId }, + respond, + }); const [ok, payload] = respond.mock.calls.at(-1) ?? []; expect(ok).toBe(true); diff --git a/src/gateway/server-methods/chat.abort.test-helpers.ts b/src/gateway/server-methods/chat.abort.test-helpers.ts new file mode 100644 index 00000000000..fe5cd324ccb --- /dev/null +++ b/src/gateway/server-methods/chat.abort.test-helpers.ts @@ -0,0 +1,69 @@ +import { vi } from "vitest"; + +export function createActiveRun( + sessionKey: string, + params: { + sessionId?: string; + owner?: { connId?: string; deviceId?: string }; + } = {}, +) { + const now = Date.now(); + return { + controller: new AbortController(), + sessionId: params.sessionId ?? `${sessionKey}-session`, + sessionKey, + startedAtMs: now, + expiresAtMs: now + 30_000, + ownerConnId: params.owner?.connId, + ownerDeviceId: params.owner?.deviceId, + }; +} + +export function createChatAbortContext(overrides: Record = {}) { + return { + chatAbortControllers: new Map(), + chatRunBuffers: new Map(), + chatDeltaSentAt: new Map(), + chatAbortedRuns: new Map(), + removeChatRun: vi + .fn() + .mockImplementation((run: string) => ({ sessionKey: "main", clientRunId: run })), + agentRunSeq: new Map(), + broadcast: vi.fn(), + nodeSendToSession: vi.fn(), + logGateway: { warn: vi.fn() }, + ...overrides, + }; +} + +export async function invokeChatAbortHandler(params: { + handler: (args: { + params: { sessionKey: string; runId?: string }; + respond: never; + context: never; + req: never; + client: never; + isWebchatConnect: () => boolean; + }) => Promise; + context: ReturnType; + request: { sessionKey: string; runId?: string }; + client?: { + connId?: string; + connect?: { + device?: { id?: string }; + scopes?: string[]; + }; + } | null; + respond?: ReturnType; +}) { + const respond = params.respond ?? vi.fn(); + await params.handler({ + params: params.request, + respond: respond as never, + context: params.context as never, + req: {} as never, + client: (params.client ?? null) as never, + isWebchatConnect: () => false, + }); + return respond; +} From 644fb76960ccc63af925ebe3c460489dbec96207 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 13 Mar 2026 16:56:38 +0000 Subject: [PATCH 4/4] refactor: share node pending test client --- .../server-methods/nodes.invoke-wake.test.ts | 41 ++++++++----------- 1 file changed, 17 insertions(+), 24 deletions(-) diff --git a/src/gateway/server-methods/nodes.invoke-wake.test.ts b/src/gateway/server-methods/nodes.invoke-wake.test.ts index 23976d71db0..58596d582f8 100644 --- a/src/gateway/server-methods/nodes.invoke-wake.test.ts +++ b/src/gateway/server-methods/nodes.invoke-wake.test.ts @@ -195,24 +195,28 @@ async function invokeNode(params: { return respond; } +function createNodeClient(nodeId: string) { + return { + connect: { + role: "node" as const, + client: { + id: nodeId, + mode: "node" as const, + name: "ios-test", + platform: "iOS 26.4.0", + version: "test", + }, + }, + }; +} + async function pullPending(nodeId: string) { const respond = vi.fn(); await nodeHandlers["node.pending.pull"]({ params: {}, respond: respond as never, context: {} as never, - client: { - connect: { - role: "node", - client: { - id: nodeId, - mode: "node", - name: "ios-test", - platform: "iOS 26.4.0", - version: "test", - }, - }, - } as never, + client: createNodeClient(nodeId) as never, req: { type: "req", id: "req-node-pending", method: "node.pending.pull" }, isWebchatConnect: () => false, }); @@ -225,18 +229,7 @@ async function ackPending(nodeId: string, ids: string[]) { params: { ids }, respond: respond as never, context: {} as never, - client: { - connect: { - role: "node", - client: { - id: nodeId, - mode: "node", - name: "ios-test", - platform: "iOS 26.4.0", - version: "test", - }, - }, - } as never, + client: createNodeClient(nodeId) as never, req: { type: "req", id: "req-node-pending-ack", method: "node.pending.ack" }, isWebchatConnect: () => false, });