Files
openclaw/src/plugin-sdk/index.test.ts
Bob ea15819ecf ACP: harden startup and move configured routing behind plugin seams (#48197)
* 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>
2026-03-17 17:27:52 +01:00

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());
});
});