diff --git a/src/gateway/server.canvas-auth.e2e.test.ts b/src/gateway/server.canvas-auth.e2e.test.ts index 05a7d41458..c6114943b2 100644 --- a/src/gateway/server.canvas-auth.e2e.test.ts +++ b/src/gateway/server.canvas-auth.e2e.test.ts @@ -268,6 +268,7 @@ describe("gateway canvas host auth", () => { maxAttempts: 1, windowMs: 60_000, lockoutMs: 60_000, + exemptLoopback: false, }); const canvasWss = new WebSocketServer({ noServer: true }); const canvasHost: CanvasHostHandler = { diff --git a/src/plugins/runtime/index.ts b/src/plugins/runtime/index.ts index 5da8dd15a9..be557a6f06 100644 --- a/src/plugins/runtime/index.ts +++ b/src/plugins/runtime/index.ts @@ -3,7 +3,6 @@ import type { PluginRuntime } from "./types.js"; import { resolveEffectiveMessagesConfig, resolveHumanDelayConfig } from "../../agents/identity.js"; import { createMemoryGetTool, createMemorySearchTool } from "../../agents/tools/memory-tool.js"; import { handleSlackAction } from "../../agents/tools/slack-actions.js"; -import { handleWhatsAppAction } from "../../agents/tools/whatsapp-actions.js"; import { chunkByNewline, chunkMarkdownText, @@ -44,7 +43,6 @@ import { signalMessageActions } from "../../channels/plugins/actions/signal.js"; import { telegramMessageActions } from "../../channels/plugins/actions/telegram.js"; import { createWhatsAppLoginTool } from "../../channels/plugins/agent-tools/whatsapp-login.js"; import { recordInboundSession } from "../../channels/session.js"; -import { monitorWebChannel } from "../../channels/web/index.js"; import { registerMemoryCli } from "../../cli/memory-cli.js"; import { loadConfig, writeConfigFile } from "../../config/config.js"; import { @@ -139,10 +137,7 @@ import { readWebSelfId, webAuthExists, } from "../../web/auth-store.js"; -import { startWebLoginWithQr, waitForWebLogin } from "../../web/login-qr.js"; -import { loginWeb } from "../../web/login.js"; import { loadWebMedia } from "../../web/media.js"; -import { sendMessageWhatsApp, sendPollWhatsApp } from "../../web/outbound.js"; import { formatNativeDependencyHint } from "./native-deps.js"; let cachedVersion: string | null = null; @@ -162,6 +157,85 @@ function resolveVersion(): string { } } +const sendMessageWhatsAppLazy: PluginRuntime["channel"]["whatsapp"]["sendMessageWhatsApp"] = async ( + ...args +) => { + const { sendMessageWhatsApp } = await loadWebOutbound(); + return sendMessageWhatsApp(...args); +}; + +const sendPollWhatsAppLazy: PluginRuntime["channel"]["whatsapp"]["sendPollWhatsApp"] = async ( + ...args +) => { + const { sendPollWhatsApp } = await loadWebOutbound(); + return sendPollWhatsApp(...args); +}; + +const loginWebLazy: PluginRuntime["channel"]["whatsapp"]["loginWeb"] = async (...args) => { + const { loginWeb } = await loadWebLogin(); + return loginWeb(...args); +}; + +const startWebLoginWithQrLazy: PluginRuntime["channel"]["whatsapp"]["startWebLoginWithQr"] = async ( + ...args +) => { + const { startWebLoginWithQr } = await loadWebLoginQr(); + return startWebLoginWithQr(...args); +}; + +const waitForWebLoginLazy: PluginRuntime["channel"]["whatsapp"]["waitForWebLogin"] = async ( + ...args +) => { + const { waitForWebLogin } = await loadWebLoginQr(); + return waitForWebLogin(...args); +}; + +const monitorWebChannelLazy: PluginRuntime["channel"]["whatsapp"]["monitorWebChannel"] = async ( + ...args +) => { + const { monitorWebChannel } = await loadWebChannel(); + return monitorWebChannel(...args); +}; + +const handleWhatsAppActionLazy: PluginRuntime["channel"]["whatsapp"]["handleWhatsAppAction"] = + async (...args) => { + const { handleWhatsAppAction } = await loadWhatsAppActions(); + return handleWhatsAppAction(...args); + }; + +let webOutboundPromise: Promise | null = null; +let webLoginPromise: Promise | null = null; +let webLoginQrPromise: Promise | null = null; +let webChannelPromise: Promise | null = null; +let whatsappActionsPromise: Promise< + typeof import("../../agents/tools/whatsapp-actions.js") +> | null = null; + +function loadWebOutbound() { + webOutboundPromise ??= import("../../web/outbound.js"); + return webOutboundPromise; +} + +function loadWebLogin() { + webLoginPromise ??= import("../../web/login.js"); + return webLoginPromise; +} + +function loadWebLoginQr() { + webLoginQrPromise ??= import("../../web/login-qr.js"); + return webLoginQrPromise; +} + +function loadWebChannel() { + webChannelPromise ??= import("../../channels/web/index.js"); + return webChannelPromise; +} + +function loadWhatsAppActions() { + whatsappActionsPromise ??= import("../../agents/tools/whatsapp-actions.js"); + return whatsappActionsPromise; +} + export function createPluginRuntime(): PluginRuntime { return { version: resolveVersion(), @@ -310,13 +384,13 @@ export function createPluginRuntime(): PluginRuntime { logWebSelfId, readWebSelfId, webAuthExists, - sendMessageWhatsApp, - sendPollWhatsApp, - loginWeb, - startWebLoginWithQr, - waitForWebLogin, - monitorWebChannel, - handleWhatsAppAction, + sendMessageWhatsApp: sendMessageWhatsAppLazy, + sendPollWhatsApp: sendPollWhatsAppLazy, + loginWeb: loginWebLazy, + startWebLoginWithQr: startWebLoginWithQrLazy, + waitForWebLogin: waitForWebLoginLazy, + monitorWebChannel: monitorWebChannelLazy, + handleWhatsAppAction: handleWhatsAppActionLazy, createLoginTool: createWhatsAppLoginTool, }, line: { diff --git a/test/gateway.multi.e2e.test.ts b/test/gateway.multi.e2e.test.ts index e5f855ff6d..4e22dfe2c7 100644 --- a/test/gateway.multi.e2e.test.ts +++ b/test/gateway.multi.e2e.test.ts @@ -231,7 +231,7 @@ const runCliJson = async (args: string[], env: NodeJS.ProcessEnv): Promise { +const postJson = async (url: string, body: unknown, headers?: Record) => { const payload = JSON.stringify(body); const parsed = new URL(url); return await new Promise<{ status: number; json: unknown }>((resolve, reject) => { @@ -244,6 +244,7 @@ const postJson = async (url: string, body: unknown) => { headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(payload), + ...headers, }, }, (res) => { @@ -440,14 +441,22 @@ describe("gateway multi-instance e2e", () => { expect(healthB.ok).toBe(true); const [hookResA, hookResB] = await Promise.all([ - postJson(`http://127.0.0.1:${gwA.port}/hooks/wake?token=${gwA.hookToken}`, { - text: "wake a", - mode: "now", - }), - postJson(`http://127.0.0.1:${gwB.port}/hooks/wake?token=${gwB.hookToken}`, { - text: "wake b", - mode: "now", - }), + postJson( + `http://127.0.0.1:${gwA.port}/hooks/wake`, + { + text: "wake a", + mode: "now", + }, + { "x-openclaw-token": gwA.hookToken }, + ), + postJson( + `http://127.0.0.1:${gwB.port}/hooks/wake`, + { + text: "wake b", + mode: "now", + }, + { "x-openclaw-token": gwB.hookToken }, + ), ]); expect(hookResA.status).toBe(200); expect((hookResA.json as { ok?: boolean } | undefined)?.ok).toBe(true); diff --git a/test/setup.ts b/test/setup.ts index a7eb44f9ea..6ccce0f0dc 100644 --- a/test/setup.ts +++ b/test/setup.ts @@ -2,6 +2,12 @@ import { afterAll, afterEach, beforeEach, vi } from "vitest"; // Ensure Vitest environment is properly set process.env.VITEST = "true"; +// Vitest vm forks can load transitive lockfile helpers many times per worker. +// Raise listener budget to avoid noisy MaxListeners warnings and warning-stack overhead. +const TEST_PROCESS_MAX_LISTENERS = 128; +if (process.getMaxListeners() > 0 && process.getMaxListeners() < TEST_PROCESS_MAX_LISTENERS) { + process.setMaxListeners(TEST_PROCESS_MAX_LISTENERS); +} import type { ChannelId,