From 107fbe88a88667b7c2ac3a1174d20677311560a7 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 23 Apr 2026 08:30:22 +0100 Subject: [PATCH] test(zalo): cache lifecycle monitor imports --- .../zalo/src/monitor.image.polling.test.ts | 6 ++-- .../src/monitor.pairing.lifecycle.test.ts | 10 ++++-- .../src/monitor.polling.media-reply.test.ts | 14 +++++--- .../src/monitor.reply-once.lifecycle.test.ts | 10 ++++-- .../monitor-mocks-test-support.ts | 32 ++++++++++++++++--- 5 files changed, 56 insertions(+), 16 deletions(-) diff --git a/extensions/zalo/src/monitor.image.polling.test.ts b/extensions/zalo/src/monitor.image.polling.test.ts index 1d86d6bdc1b..72878c72a72 100644 --- a/extensions/zalo/src/monitor.image.polling.test.ts +++ b/extensions/zalo/src/monitor.image.polling.test.ts @@ -9,7 +9,7 @@ import { import { getUpdatesMock, getZaloRuntimeMock, - loadLifecycleMonitorModule, + loadCachedLifecycleMonitorModule, resetLifecycleTestState, sendMessageMock, } from "../test-support/monitor-mocks-test-support.js"; @@ -40,7 +40,7 @@ describe("Zalo polling image handling", () => { }) .mockImplementation(() => new Promise(() => {})); - const { monitorZaloProvider } = await loadLifecycleMonitorModule(); + const { monitorZaloProvider } = await loadCachedLifecycleMonitorModule("zalo-image-polling"); const abort = new AbortController(); const runtime = createRuntimeEnv(); const { account, config } = createLifecycleMonitorSetup({ @@ -79,7 +79,7 @@ describe("Zalo polling image handling", () => { }) .mockImplementation(() => new Promise(() => {})); - const { monitorZaloProvider } = await loadLifecycleMonitorModule(); + const { monitorZaloProvider } = await loadCachedLifecycleMonitorModule("zalo-image-polling"); const abort = new AbortController(); const runtime = createRuntimeEnv(); const { account, config } = createLifecycleMonitorSetup({ diff --git a/extensions/zalo/src/monitor.pairing.lifecycle.test.ts b/extensions/zalo/src/monitor.pairing.lifecycle.test.ts index fe4bd053de1..aa470783789 100644 --- a/extensions/zalo/src/monitor.pairing.lifecycle.test.ts +++ b/extensions/zalo/src/monitor.pairing.lifecycle.test.ts @@ -44,7 +44,10 @@ describe("Zalo pairing lifecycle", () => { } it("emits one pairing reply across duplicate webhook replay and scopes reads and writes to accountId", async () => { - const monitor = await startWebhookLifecycleMonitor(createPairingMonitorSetup()); + const monitor = await startWebhookLifecycleMonitor({ + ...createPairingMonitorSetup(), + cacheKey: "zalo-pairing-lifecycle", + }); try { await withServer( @@ -100,7 +103,10 @@ describe("Zalo pairing lifecycle", () => { it("does not emit a second pairing reply when replay arrives after the first send fails", async () => { sendMessageMock.mockRejectedValueOnce(new Error("pairing send failed")); - const monitor = await startWebhookLifecycleMonitor(createPairingMonitorSetup()); + const monitor = await startWebhookLifecycleMonitor({ + ...createPairingMonitorSetup(), + cacheKey: "zalo-pairing-lifecycle", + }); try { await withServer( diff --git a/extensions/zalo/src/monitor.polling.media-reply.test.ts b/extensions/zalo/src/monitor.polling.media-reply.test.ts index b1a2c5a59e8..7344ba52373 100644 --- a/extensions/zalo/src/monitor.polling.media-reply.test.ts +++ b/extensions/zalo/src/monitor.polling.media-reply.test.ts @@ -9,7 +9,7 @@ import { } from "../test-support/lifecycle-test-support.js"; import { getUpdatesMock, - loadLifecycleMonitorModule, + loadCachedLifecycleMonitorModule, resetLifecycleTestState, sendPhotoMock, setLifecycleRuntimeCore, @@ -95,7 +95,9 @@ describe("Zalo polling media replies", () => { }) .mockImplementation(() => new Promise(() => {})); - const { monitorZaloProvider } = await loadLifecycleMonitorModule(); + const { monitorZaloProvider } = await loadCachedLifecycleMonitorModule( + "zalo-polling-media-reply", + ); const abort = new AbortController(); const runtime = createRuntimeEnv(); const { account, config } = createLifecycleMonitorSetup({ @@ -155,7 +157,9 @@ describe("Zalo polling media replies", () => { }) .mockImplementation(() => new Promise(() => {})); - const { monitorZaloProvider } = await loadLifecycleMonitorModule(); + const { monitorZaloProvider } = await loadCachedLifecycleMonitorModule( + "zalo-polling-media-reply", + ); const abort = new AbortController(); const runtime = createRuntimeEnv(); const { account, config } = createLifecycleMonitorSetup({ @@ -195,7 +199,9 @@ describe("Zalo polling media replies", () => { setActivePluginRegistry(firstRegistry); getUpdatesMock.mockImplementation(() => new Promise(() => {})); - const { monitorZaloProvider } = await loadLifecycleMonitorModule(); + const { monitorZaloProvider } = await loadCachedLifecycleMonitorModule( + "zalo-polling-media-reply", + ); const firstAbort = new AbortController(); const firstRuntime = createRuntimeEnv(); const { account, config } = createLifecycleMonitorSetup({ diff --git a/extensions/zalo/src/monitor.reply-once.lifecycle.test.ts b/extensions/zalo/src/monitor.reply-once.lifecycle.test.ts index f8a8f7062fc..3d847e82236 100644 --- a/extensions/zalo/src/monitor.reply-once.lifecycle.test.ts +++ b/extensions/zalo/src/monitor.reply-once.lifecycle.test.ts @@ -65,7 +65,10 @@ describe("Zalo reply-once lifecycle", () => { }, ); - const monitor = await startWebhookLifecycleMonitor(createReplyOnceMonitorSetup()); + const monitor = await startWebhookLifecycleMonitor({ + ...createReplyOnceMonitorSetup(), + cacheKey: "zalo-reply-once-lifecycle", + }); try { await withServer( @@ -131,7 +134,10 @@ describe("Zalo reply-once lifecycle", () => { }, ); - const monitor = await startWebhookLifecycleMonitor(createReplyOnceMonitorSetup()); + const monitor = await startWebhookLifecycleMonitor({ + ...createReplyOnceMonitorSetup(), + cacheKey: "zalo-reply-once-lifecycle", + }); try { await withServer( diff --git a/extensions/zalo/test-support/monitor-mocks-test-support.ts b/extensions/zalo/test-support/monitor-mocks-test-support.ts index f641c5e1d61..167888c04f6 100644 --- a/extensions/zalo/test-support/monitor-mocks-test-support.ts +++ b/extensions/zalo/test-support/monitor-mocks-test-support.ts @@ -21,6 +21,8 @@ const runtimeModuleId = new URL("../src/runtime.js", import.meta.url).pathname; type UnknownMock = Mock<(...args: unknown[]) => unknown>; type AsyncUnknownMock = Mock<(...args: unknown[]) => Promise>; const loadedMonitorModules = new Set(); +const cachedMonitorModules = new Map>(); +let cachedWebhookModule: Promise | undefined; type ZaloLifecycleMocks = { setWebhookMock: AsyncUnknownMock; @@ -102,17 +104,17 @@ async function importSecretInputModule(cacheBust: string): Promise { - return (await import(`${webhookModuleUrl}?t=${cacheBust}-${Date.now()}`)) as WebhookModule; +async function importCachedWebhookModule(): Promise { + cachedWebhookModule ??= import(webhookModuleUrl) as Promise; + return await cachedWebhookModule; } export async function resetLifecycleTestState() { vi.clearAllMocks(); - (await importWebhookModule("reset-webhook")).clearZaloWebhookSecurityStateForTest(); + (await importCachedWebhookModule()).clearZaloWebhookSecurityStateForTest(); for (const module of loadedMonitorModules) { module.__testing.clearHostedMediaRouteRefsForTest(); } - loadedMonitorModules.clear(); setActivePluginRegistry(createEmptyPluginRegistry()); } @@ -130,12 +132,30 @@ export async function loadLifecycleMonitorModule(): Promise { return await importMonitorModule({ cacheBust: "monitor", mocked: true }); } +export async function loadCachedLifecycleMonitorModule(cacheKey: string): Promise { + const key = cacheKey.trim(); + if (!key) { + throw new Error("cacheKey is required"); + } + const cached = + cachedMonitorModules.get(key) ?? + (async () => { + installLifecycleModuleMocks(); + const module = (await import(`${monitorModuleUrl}?t=${key}`)) as MonitorModule; + loadedMonitorModules.add(module); + return module; + })(); + cachedMonitorModules.set(key, cached); + return await cached; +} + export async function startWebhookLifecycleMonitor(params: { account: ResolvedZaloAccount; config: OpenClawConfig; token?: string; webhookUrl?: string; webhookSecret?: string; + cacheKey?: string; }) { const registry = createEmptyPluginRegistry(); setActivePluginRegistry(registry); @@ -149,7 +169,9 @@ export async function startWebhookLifecycleMonitor(params: { const { normalizeSecretInputString } = await importSecretInputModule("secret-input"); const webhookSecret = params.webhookSecret ?? normalizeSecretInputString(params.account.config?.webhookSecret); - const { monitorZaloProvider } = await loadLifecycleMonitorModule(); + const { monitorZaloProvider } = params.cacheKey + ? await loadCachedLifecycleMonitorModule(params.cacheKey) + : await loadLifecycleMonitorModule(); const run = monitorZaloProvider({ token: params.token ?? "zalo-token", account: params.account,