Files
openclaw/src/plugin-sdk/index.test.ts
2026-03-26 21:41:49 +00:00

111 lines
3.4 KiB
TypeScript

import fs from "node:fs/promises";
import path from "node:path";
import { describe, expect, it } from "vitest";
import { buildPluginSdkPackageExports } from "./entrypoints.js";
async function collectRuntimeExports(filePath: string, seen = new Set<string>()) {
const normalizedPath = path.resolve(filePath);
if (seen.has(normalizedPath)) {
return new Set<string>();
}
seen.add(normalizedPath);
const source = await fs.readFile(normalizedPath, "utf8");
const exportNames = new Set<string>();
for (const match of source.matchAll(/export\s+(?!type\b)\{([\s\S]*?)\}\s+from\s+"([^"]+)";/g)) {
const names = match[1]
.split(",")
.map((part) => part.trim())
.filter(Boolean)
.map((part) => part.split(/\s+as\s+/).at(-1) ?? part);
for (const name of names) {
exportNames.add(name);
}
}
for (const match of source.matchAll(/export\s+\*\s+from\s+"([^"]+)";/g)) {
const specifier = match[1];
if (!specifier.startsWith(".")) {
continue;
}
const nestedPath = path.resolve(
path.dirname(normalizedPath),
specifier.replace(/\.js$/, ".ts"),
);
const nestedExports = await collectRuntimeExports(nestedPath, seen);
for (const name of nestedExports) {
exportNames.add(name);
}
}
return exportNames;
}
describe("plugin-sdk exports", () => {
it("does not expose runtime modules", async () => {
const runtimeExports = await collectRuntimeExports(path.join(import.meta.dirname, "index.ts"));
const forbidden = [
"chunkMarkdownText",
"chunkText",
"hasControlCommand",
"isControlCommandMessage",
"shouldComputeCommandAuthorized",
"shouldHandleTextCommands",
"buildMentionRegexes",
"matchesMentionPatterns",
"resolveStateDir",
"writeConfigFile",
"enqueueSystemEvent",
"fetchRemoteMedia",
"saveMediaBuffer",
"formatAgentEnvelope",
"buildPairingReply",
"resolveAgentRoute",
"dispatchReplyFromConfig",
"createReplyDispatcherWithTyping",
"dispatchReplyWithBufferedBlockDispatcher",
"resolveCommandAuthorizedFromAuthorizers",
"monitorSlackProvider",
"monitorTelegramProvider",
"monitorIMessageProvider",
"monitorSignalProvider",
"sendMessageSlack",
"sendMessageTelegram",
"sendMessageIMessage",
"sendMessageSignal",
"sendMessageWhatsApp",
"probeSlack",
"probeTelegram",
"probeIMessage",
"probeSignal",
];
for (const key of forbidden) {
expect(runtimeExports.has(key)).toBe(false);
}
});
it("keeps the root runtime surface intentionally small", async () => {
const runtimeExports = await collectRuntimeExports(path.join(import.meta.dirname, "index.ts"));
expect([...runtimeExports].toSorted()).toEqual([
"delegateCompactionToRuntime",
"emptyPluginConfigSchema",
"onDiagnosticEvent",
"registerContextEngine",
]);
});
it("keeps package.json plugin-sdk exports synced with the manifest", async () => {
const packageJsonPath = path.join(process.cwd(), "package.json");
const packageJson = JSON.parse(await fs.readFile(packageJsonPath, "utf8")) as {
exports?: Record<string, unknown>;
};
const currentPluginSdkExports = Object.fromEntries(
Object.entries(packageJson.exports ?? {}).filter(([key]) => key.startsWith("./plugin-sdk")),
);
expect(currentPluginSdkExports).toEqual(buildPluginSdkPackageExports());
});
});