mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-17 13:00:48 +00:00
242 lines
7.0 KiB
TypeScript
242 lines
7.0 KiB
TypeScript
import fs from "node:fs/promises";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import { pathToFileURL } from "node:url";
|
|
import { build } from "tsdown";
|
|
import { describe, expect, it } from "vitest";
|
|
import * as sdk from "./index.js";
|
|
|
|
const pluginSdkEntrypoints = [
|
|
"index",
|
|
"core",
|
|
"compat",
|
|
"telegram",
|
|
"discord",
|
|
"slack",
|
|
"signal",
|
|
"imessage",
|
|
"whatsapp",
|
|
"line",
|
|
"msteams",
|
|
"acpx",
|
|
"bluebubbles",
|
|
"copilot-proxy",
|
|
"device-pair",
|
|
"diagnostics-otel",
|
|
"diffs",
|
|
"feishu",
|
|
"google-gemini-cli-auth",
|
|
"googlechat",
|
|
"irc",
|
|
"llm-task",
|
|
"lobster",
|
|
"matrix",
|
|
"mattermost",
|
|
"memory-core",
|
|
"memory-lancedb",
|
|
"minimax-portal-auth",
|
|
"nextcloud-talk",
|
|
"nostr",
|
|
"open-prose",
|
|
"phone-control",
|
|
"qwen-portal-auth",
|
|
"synology-chat",
|
|
"talk-voice",
|
|
"test-utils",
|
|
"thread-ownership",
|
|
"tlon",
|
|
"twitch",
|
|
"voice-call",
|
|
"zalo",
|
|
"zalouser",
|
|
"account-id",
|
|
"keyed-async-queue",
|
|
] as const;
|
|
|
|
const pluginSdkSpecifiers = pluginSdkEntrypoints.map((entry) =>
|
|
entry === "index" ? "openclaw/plugin-sdk" : `openclaw/plugin-sdk/${entry}`,
|
|
);
|
|
|
|
function buildPluginSdkPackageExports() {
|
|
return Object.fromEntries(
|
|
pluginSdkEntrypoints.map((entry) => [
|
|
entry === "index" ? "./plugin-sdk" : `./plugin-sdk/${entry}`,
|
|
{
|
|
default: `./dist/plugin-sdk/${entry}.js`,
|
|
},
|
|
]),
|
|
);
|
|
}
|
|
|
|
describe("plugin-sdk exports", () => {
|
|
it("does not expose runtime modules", () => {
|
|
const forbidden = [
|
|
"chunkMarkdownText",
|
|
"chunkText",
|
|
"resolveTextChunkLimit",
|
|
"hasControlCommand",
|
|
"isControlCommandMessage",
|
|
"shouldComputeCommandAuthorized",
|
|
"shouldHandleTextCommands",
|
|
"buildMentionRegexes",
|
|
"matchesMentionPatterns",
|
|
"resolveStateDir",
|
|
"loadConfig",
|
|
"writeConfigFile",
|
|
"runCommandWithTimeout",
|
|
"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(Object.prototype.hasOwnProperty.call(sdk, key)).toBe(false);
|
|
}
|
|
});
|
|
|
|
// Verify critical functions that extensions depend on are exported and callable.
|
|
// Regression guard for #27569 where isDangerousNameMatchingEnabled was missing
|
|
// from the compiled output, breaking mattermost/googlechat/msteams/irc plugins.
|
|
it("exports critical functions used by channel extensions", () => {
|
|
const requiredFunctions = [
|
|
"isDangerousNameMatchingEnabled",
|
|
"createAccountListHelpers",
|
|
"buildAgentMediaPayload",
|
|
"createReplyPrefixOptions",
|
|
"createTypingCallbacks",
|
|
"logInboundDrop",
|
|
"logTypingFailure",
|
|
"buildPendingHistoryContextFromMap",
|
|
"clearHistoryEntriesIfEnabled",
|
|
"recordPendingHistoryEntryIfEnabled",
|
|
"resolveControlCommandGate",
|
|
"resolveDmGroupAccessWithLists",
|
|
"resolveAllowlistProviderRuntimeGroupPolicy",
|
|
"resolveDefaultGroupPolicy",
|
|
"resolveChannelMediaMaxBytes",
|
|
"warnMissingProviderGroupPolicyFallbackOnce",
|
|
"createDedupeCache",
|
|
"formatInboundFromLabel",
|
|
"resolveRuntimeGroupPolicy",
|
|
"emptyPluginConfigSchema",
|
|
"normalizePluginHttpPath",
|
|
"registerPluginHttpRoute",
|
|
"buildBaseAccountStatusSnapshot",
|
|
"buildBaseChannelStatusSummary",
|
|
"buildTokenChannelStatusSummary",
|
|
"collectStatusIssuesFromLastError",
|
|
"createDefaultChannelRuntimeState",
|
|
"resolveChannelEntryMatch",
|
|
"resolveChannelEntryMatchWithFallback",
|
|
"normalizeChannelSlug",
|
|
"buildChannelKeyCandidates",
|
|
];
|
|
|
|
for (const key of requiredFunctions) {
|
|
expect(sdk).toHaveProperty(key);
|
|
expect(typeof (sdk as Record<string, unknown>)[key]).toBe("function");
|
|
}
|
|
});
|
|
|
|
// Verify critical constants that extensions depend on are exported.
|
|
it("exports critical constants used by channel extensions", () => {
|
|
const requiredConstants = [
|
|
"DEFAULT_GROUP_HISTORY_LIMIT",
|
|
"DEFAULT_ACCOUNT_ID",
|
|
"SILENT_REPLY_TOKEN",
|
|
"PAIRING_APPROVED_MESSAGE",
|
|
];
|
|
|
|
for (const key of requiredConstants) {
|
|
expect(sdk).toHaveProperty(key);
|
|
}
|
|
});
|
|
|
|
it("emits importable bundled subpath entries", { timeout: 240_000 }, async () => {
|
|
const outDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-plugin-sdk-build-"));
|
|
const fixtureDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-plugin-sdk-consumer-"));
|
|
|
|
try {
|
|
await build({
|
|
clean: true,
|
|
config: false,
|
|
dts: false,
|
|
entry: Object.fromEntries(
|
|
pluginSdkEntrypoints.map((entry) => [entry, `src/plugin-sdk/${entry}.ts`]),
|
|
),
|
|
env: { NODE_ENV: "production" },
|
|
fixedExtension: false,
|
|
logLevel: "error",
|
|
outDir,
|
|
platform: "node",
|
|
});
|
|
|
|
for (const entry of pluginSdkEntrypoints) {
|
|
const module = await import(pathToFileURL(path.join(outDir, `${entry}.js`)).href);
|
|
expect(module).toBeTypeOf("object");
|
|
}
|
|
|
|
const packageDir = path.join(fixtureDir, "openclaw");
|
|
const consumerDir = path.join(fixtureDir, "consumer");
|
|
const consumerEntry = path.join(consumerDir, "import-plugin-sdk.mjs");
|
|
|
|
await fs.mkdir(path.join(packageDir, "dist"), { recursive: true });
|
|
await fs.symlink(outDir, path.join(packageDir, "dist", "plugin-sdk"), "dir");
|
|
await fs.writeFile(
|
|
path.join(packageDir, "package.json"),
|
|
JSON.stringify(
|
|
{
|
|
exports: buildPluginSdkPackageExports(),
|
|
name: "openclaw",
|
|
type: "module",
|
|
},
|
|
null,
|
|
2,
|
|
),
|
|
);
|
|
|
|
await fs.mkdir(path.join(consumerDir, "node_modules"), { recursive: true });
|
|
await fs.symlink(packageDir, path.join(consumerDir, "node_modules", "openclaw"), "dir");
|
|
await fs.writeFile(
|
|
consumerEntry,
|
|
[
|
|
`const specifiers = ${JSON.stringify(pluginSdkSpecifiers)};`,
|
|
"const results = {};",
|
|
"for (const specifier of specifiers) {",
|
|
" results[specifier] = typeof (await import(specifier));",
|
|
"}",
|
|
"export default results;",
|
|
].join("\n"),
|
|
);
|
|
|
|
const { default: importResults } = await import(pathToFileURL(consumerEntry).href);
|
|
expect(importResults).toEqual(
|
|
Object.fromEntries(pluginSdkSpecifiers.map((specifier) => [specifier, "object"])),
|
|
);
|
|
} finally {
|
|
await fs.rm(outDir, { recursive: true, force: true });
|
|
await fs.rm(fixtureDir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
});
|