mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-21 23:11:01 +00:00
* ACPX: keep plugin-local runtime installs out of dist * Gateway: harden ACP startup and service PATH * ACP: reinitialize error-state configured bindings * ACP: classify pre-turn runtime failures as session init failures * Plugins: move configured ACP routing behind channel seams * Telegram tests: align startup probe assertions after rebase * Discord: harden ACP configured binding recovery * ACP: recover Discord bindings after stale runtime exits * ACPX: replace dead sessions during ensure * Discord: harden ACP binding recovery * Discord: fix review follow-ups * ACP bindings: load channel snapshots across workspaces * ACP bindings: cache snapshot channel plugin resolution * Experiments: add ACP pluginification holy grail plan * Experiments: rename ACP pluginification plan doc * Experiments: drop old ACP pluginification doc path * ACP: move configured bindings behind plugin services * Experiments: update bindings capability architecture plan * Bindings: isolate configured binding routing and targets * Discord tests: fix runtime env helper path * Tests: fix channel binding CI regressions * Tests: normalize ACP workspace assertion on Windows * Bindings: isolate configured binding registry * Bindings: finish configured binding cleanup * Bindings: finish generic cleanup * Bindings: align runtime approval callbacks * ACP: delete residual bindings barrel * Bindings: restore legacy compatibility * Revert "Bindings: restore legacy compatibility" This reverts commit ac2ed68fa2426ecc874d68278c71c71ad363fcfe. * Tests: drop ACP route legacy helper names * Discord/ACP: fix binding regressions --------- Co-authored-by: Onur <2453968+osolmaz@users.noreply.github.com>
158 lines
5.5 KiB
TypeScript
158 lines
5.5 KiB
TypeScript
import { execFile } from "node:child_process";
|
|
import fs from "node:fs/promises";
|
|
import { createRequire } from "node:module";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import { pathToFileURL } from "node:url";
|
|
import { promisify } from "node:util";
|
|
import { describe, expect, it } from "vitest";
|
|
import {
|
|
buildPluginSdkEntrySources,
|
|
buildPluginSdkPackageExports,
|
|
buildPluginSdkSpecifiers,
|
|
pluginSdkEntrypoints,
|
|
} from "./entrypoints.js";
|
|
import * as sdk from "./index.js";
|
|
|
|
const pluginSdkSpecifiers = buildPluginSdkSpecifiers();
|
|
const execFileAsync = promisify(execFile);
|
|
const require = createRequire(import.meta.url);
|
|
const tsdownModuleUrl = pathToFileURL(require.resolve("tsdown")).href;
|
|
|
|
describe("plugin-sdk exports", () => {
|
|
it("does not expose runtime modules", () => {
|
|
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(Object.prototype.hasOwnProperty.call(sdk, key)).toBe(false);
|
|
}
|
|
});
|
|
|
|
it("keeps the root runtime surface intentionally small", () => {
|
|
expect(typeof sdk.emptyPluginConfigSchema).toBe("function");
|
|
expect(Object.prototype.hasOwnProperty.call(sdk, "resolveControlCommandGate")).toBe(false);
|
|
expect(Object.prototype.hasOwnProperty.call(sdk, "buildAgentSessionKey")).toBe(false);
|
|
expect(Object.prototype.hasOwnProperty.call(sdk, "isDangerousNameMatchingEnabled")).toBe(false);
|
|
});
|
|
|
|
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 {
|
|
const buildScriptPath = path.join(fixtureDir, "build-plugin-sdk.mjs");
|
|
await fs.writeFile(
|
|
buildScriptPath,
|
|
`import { build } from ${JSON.stringify(tsdownModuleUrl)};
|
|
await build(${JSON.stringify({
|
|
clean: true,
|
|
config: false,
|
|
dts: false,
|
|
entry: buildPluginSdkEntrySources(),
|
|
env: { NODE_ENV: "production" },
|
|
fixedExtension: false,
|
|
logLevel: "error",
|
|
outDir,
|
|
platform: "node",
|
|
})});
|
|
`,
|
|
);
|
|
await execFileAsync(process.execPath, [buildScriptPath], {
|
|
cwd: process.cwd(),
|
|
});
|
|
|
|
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: string) => [specifier, "object"])),
|
|
);
|
|
} finally {
|
|
await fs.rm(outDir, { recursive: true, force: true });
|
|
await fs.rm(fixtureDir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
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());
|
|
});
|
|
});
|