mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-20 21:51:28 +00:00
test(plugin-sdk): tighten boundary guardrails
This commit is contained in:
@@ -1004,8 +1004,10 @@ authoring plugins:
|
||||
contract on the plugin. Core then reads approval auth, delivery, render, and
|
||||
native-routing behavior through that one capability instead of mixing
|
||||
approval behavior into unrelated plugin fields.
|
||||
- `openclaw/plugin-sdk/channel-runtime` remains only as a compatibility shim.
|
||||
New code should import the narrower primitives instead.
|
||||
- `openclaw/plugin-sdk/channel-runtime` is deprecated and remains only as a
|
||||
compatibility shim for older plugins. New code should import the narrower
|
||||
generic primitives instead, and repo code should not add new imports of the
|
||||
shim.
|
||||
- Bundled extension internals remain private. External plugins should use only
|
||||
`openclaw/plugin-sdk/*` subpaths. OpenClaw core/test code may use the repo
|
||||
public entry points under a plugin package root such as `index.js`, `api.js`,
|
||||
|
||||
@@ -155,7 +155,7 @@ bundled plugin workspace, keep provider-owned helpers in that plugin's own
|
||||
| `plugin-sdk/channel-config-schema` | Config schema builders | Channel config schema types |
|
||||
| `plugin-sdk/channel-policy` | Group/DM policy resolution | `resolveChannelGroupRequireMention` |
|
||||
| `plugin-sdk/channel-lifecycle` | Account status tracking | `createAccountStatusSink` |
|
||||
| `plugin-sdk/channel-runtime` | Runtime wiring helpers | Channel runtime utilities |
|
||||
| `plugin-sdk/channel-runtime` | Deprecated compatibility shim | Legacy channel runtime utilities only |
|
||||
| `plugin-sdk/channel-send-result` | Send result types | Reply result types |
|
||||
| `plugin-sdk/runtime-store` | Persistent plugin storage | `createPluginRuntimeStore` |
|
||||
| `plugin-sdk/approval-runtime` | Approval prompt helpers | Exec/plugin approval payload, approval capability/profile helpers, native approval routing/runtime helpers |
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
// Legacy compatibility shim for older channel helpers. Prefer the dedicated
|
||||
// plugin-sdk subpaths instead of adding new imports here.
|
||||
/**
|
||||
* @deprecated Compatibility shim only. Keep old plugins working, but do not
|
||||
* add new imports here and do not use this subpath from repo code.
|
||||
* Prefer the dedicated generic plugin-sdk subpaths instead.
|
||||
*/
|
||||
|
||||
export * from "../channels/chat-type.js";
|
||||
export * from "../channels/reply-prefix.js";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { readFileSync, readdirSync } from "node:fs";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import type {
|
||||
@@ -45,8 +45,9 @@ import { pluginSdkSubpaths } from "../../plugin-sdk/entrypoints.js";
|
||||
import type { PluginRuntime } from "../runtime/types.js";
|
||||
import type { OpenClawPluginApi } from "../types.js";
|
||||
|
||||
const ROOT_DIR = resolve(dirname(fileURLToPath(import.meta.url)), "../..");
|
||||
const PLUGIN_SDK_DIR = resolve(ROOT_DIR, "plugin-sdk");
|
||||
const SRC_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "../..");
|
||||
const REPO_ROOT = resolve(SRC_ROOT, "..");
|
||||
const PLUGIN_SDK_DIR = resolve(SRC_ROOT, "plugin-sdk");
|
||||
const sourceCache = new Map<string, string>();
|
||||
const representativeRuntimeSmokeSubpaths = ["channel-runtime", "conversation-runtime"] as const;
|
||||
|
||||
@@ -63,6 +64,39 @@ function readPluginSdkSource(subpath: string): string {
|
||||
return text;
|
||||
}
|
||||
|
||||
function listRepoTsFiles(dir: string): string[] {
|
||||
const entries = readdirSync(dir, { withFileTypes: true });
|
||||
return entries.flatMap((entry) => {
|
||||
const absolute = resolve(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
if (entry.name === "dist" || entry.name === "node_modules") {
|
||||
return [];
|
||||
}
|
||||
return listRepoTsFiles(absolute);
|
||||
}
|
||||
if (!entry.isFile()) {
|
||||
return [];
|
||||
}
|
||||
return absolute.endsWith(".ts") ? [absolute] : [];
|
||||
});
|
||||
}
|
||||
|
||||
function findRepoFilesContaining(params: {
|
||||
roots: readonly string[];
|
||||
pattern: RegExp;
|
||||
exclude?: readonly string[];
|
||||
excludeFilesMatching?: readonly RegExp[];
|
||||
}) {
|
||||
const excluded = new Set((params.exclude ?? []).map((entry) => resolve(REPO_ROOT, entry)));
|
||||
return params.roots
|
||||
.flatMap((root) => listRepoTsFiles(root))
|
||||
.filter((file) => !excluded.has(file))
|
||||
.filter((file) => !(params.excludeFilesMatching ?? []).some((pattern) => pattern.test(file)))
|
||||
.filter((file) => params.pattern.test(readFileSync(file, "utf8")))
|
||||
.map((file) => file.slice(REPO_ROOT.length + 1))
|
||||
.toSorted();
|
||||
}
|
||||
|
||||
function isIdentifierCode(code: number): boolean {
|
||||
return (
|
||||
(code >= 48 && code <= 57) ||
|
||||
@@ -321,6 +355,33 @@ describe("plugin-sdk subpath exports", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps the deprecated channel-runtime shim unused in repo imports", () => {
|
||||
const matches = findRepoFilesContaining({
|
||||
roots: [
|
||||
resolve(REPO_ROOT, "src"),
|
||||
resolve(REPO_ROOT, "extensions"),
|
||||
resolve(REPO_ROOT, "test"),
|
||||
],
|
||||
pattern: /openclaw\/plugin-sdk\/channel-runtime/u,
|
||||
exclude: ["src/plugins/sdk-alias.test.ts"],
|
||||
});
|
||||
expect(matches).toEqual([]);
|
||||
});
|
||||
|
||||
it("keeps removed channel-named runtime boundaries out of core imports", () => {
|
||||
const matches = findRepoFilesContaining({
|
||||
roots: [resolve(REPO_ROOT, "src")],
|
||||
pattern:
|
||||
/plugins\/runtime\/runtime-(?:discord|imessage|line|signal|slack|telegram|whatsapp)(?:[-.][^"']*)?\.js/u,
|
||||
exclude: [
|
||||
"src/plugins/runtime/runtime-plugin-boundary.ts",
|
||||
"src/plugins/runtime/runtime-web-channel-boundary.ts",
|
||||
],
|
||||
excludeFilesMatching: [/\.test\.ts$/u, /\.test-harness\.ts$/u],
|
||||
});
|
||||
expect(matches).toEqual([]);
|
||||
});
|
||||
|
||||
it("exports channel runtime helpers from the dedicated subpath", () => {
|
||||
expectSourceOmits("channel-runtime", [
|
||||
"applyChannelMatchMeta",
|
||||
|
||||
@@ -41,6 +41,12 @@ function findExtensionImports(source: string): string[] {
|
||||
].map((match) => match[1]);
|
||||
}
|
||||
|
||||
function isAllowedExtensionPublicImport(specifier: string): boolean {
|
||||
return /(?:^|\/)extensions\/[^/]+\/(?:api|index|runtime-api|setup-entry|login-qr-api)\.js$/u.test(
|
||||
specifier,
|
||||
);
|
||||
}
|
||||
|
||||
function findPluginSdkImports(source: string): string[] {
|
||||
return [
|
||||
...source.matchAll(/from\s+["']((?:\.\.\/)+plugin-sdk\/[^"']+)["']/g),
|
||||
@@ -78,7 +84,9 @@ describe("non-extension test boundaries", () => {
|
||||
const offenders = testFiles
|
||||
.map((file) => {
|
||||
const source = fs.readFileSync(path.join(repoRoot, file), "utf8");
|
||||
const imports = findExtensionImports(source);
|
||||
const imports = findExtensionImports(source).filter(
|
||||
(specifier) => !isAllowedExtensionPublicImport(specifier),
|
||||
);
|
||||
if (imports.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user