openclaw/src/plugins/runtime-plugin-boundary.wha...

116 lines
3.9 KiB
TypeScript

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<boolean, ReturnType<typeof import("jiti").createJiti>>();
const light = loadPluginBoundaryModuleWithJiti<LightModule>(
path.join(runtimePluginDir, "light-runtime-api.js"),
loaders,
);
const heavy = loadPluginBoundaryModuleWithJiti<HeavyModule>(
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);
});
});