import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { afterEach, describe, expect, it } from "vitest"; import { stageBundledPluginRuntime } from "../../scripts/stage-bundled-plugin-runtime.mjs"; import { loadPluginBoundaryModuleWithJiti } from "./runtime/runtime-plugin-boundary.js"; type LightModule = { getActiveWebListener: (accountId?: string | null) => unknown; }; type HeavyModule = { setActiveWebListener: ( accountId: string | null | undefined, listener: { sendMessage: () => Promise<{ messageId: string }> } | null, ) => void; }; const tempDirs: string[] = []; function createBundledWhatsAppRuntimeFixture() { const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-whatsapp-boundary-")); tempDirs.push(rootDir); const distRoot = path.join(rootDir, "dist"); const whatsappDistDir = path.join(distRoot, "extensions", "whatsapp"); fs.mkdirSync(whatsappDistDir, { recursive: true }); fs.writeFileSync( path.join(rootDir, "package.json"), JSON.stringify( { name: "openclaw", type: "module", bin: { openclaw: "openclaw.mjs", }, exports: { "./plugin-sdk": { default: "./dist/plugin-sdk/index.js", }, }, }, null, 2, ), "utf8", ); fs.writeFileSync(path.join(rootDir, "openclaw.mjs"), "export {};\n", "utf8"); fs.writeFileSync(path.join(whatsappDistDir, "index.js"), "export default {};\n", "utf8"); fs.writeFileSync( path.join(whatsappDistDir, "light-runtime-api.js"), 'export { getActiveWebListener } from "../../active-listener.js";\n', "utf8", ); fs.writeFileSync( path.join(whatsappDistDir, "runtime-api.js"), 'export { getActiveWebListener, setActiveWebListener } from "../../active-listener.js";\n', "utf8", ); fs.writeFileSync( path.join(distRoot, "active-listener.js"), [ 'const key = Symbol.for("openclaw.whatsapp.activeListenerState");', "const g = globalThis;", "if (!g[key]) {", " g[key] = { listeners: new Map(), current: null };", "}", "const state = g[key];", "export function setActiveWebListener(accountIdOrListener, maybeListener) {", ' const accountId = typeof accountIdOrListener === "string" ? accountIdOrListener : "default";', ' const listener = typeof accountIdOrListener === "string" ? (maybeListener ?? null) : (accountIdOrListener ?? null);', " if (!listener) state.listeners.delete(accountId);", " else state.listeners.set(accountId, listener);", ' if (accountId === "default") state.current = listener;', "}", "export function getActiveWebListener(accountId) {", ' return state.listeners.get(accountId ?? "default") ?? null;', "}", "", ].join("\n"), "utf8", ); stageBundledPluginRuntime({ repoRoot: rootDir }); return path.join(rootDir, "dist-runtime", "extensions", "whatsapp"); } afterEach(() => { for (const dir of tempDirs.splice(0, tempDirs.length)) { fs.rmSync(dir, { recursive: true, force: true }); } }); describe("runtime plugin boundary whatsapp seam", () => { it("shares listener state between staged light and heavy runtime modules", () => { const runtimePluginDir = createBundledWhatsAppRuntimeFixture(); const loaders = new Map>(); const light = loadPluginBoundaryModuleWithJiti( path.join(runtimePluginDir, "light-runtime-api.js"), loaders, ); const heavy = loadPluginBoundaryModuleWithJiti( path.join(runtimePluginDir, "runtime-api.js"), loaders, ); const listener = { sendMessage: async () => ({ messageId: "msg-1" }), }; heavy.setActiveWebListener("work", listener); expect(light.getActiveWebListener("work")).toBe(listener); heavy.setActiveWebListener("work", null); }); });