mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-13 02:22:55 +00:00
refactor(core): dedupe infra, media, pairing, and plugin helpers
This commit is contained in:
@@ -26,6 +26,15 @@ async function withStateDir<T>(stateDir: string, fn: () => Promise<T>) {
|
||||
);
|
||||
}
|
||||
|
||||
async function discoverWithStateDir(
|
||||
stateDir: string,
|
||||
params: Parameters<typeof discoverOpenClawPlugins>[0],
|
||||
) {
|
||||
return await withStateDir(stateDir, async () => {
|
||||
return discoverOpenClawPlugins(params);
|
||||
});
|
||||
}
|
||||
|
||||
function writePluginPackageManifest(params: {
|
||||
packageDir: string;
|
||||
packageName: string;
|
||||
@@ -197,9 +206,7 @@ describe("discoverOpenClawPlugins", () => {
|
||||
});
|
||||
fs.writeFileSync(outside, "export default function () {}", "utf-8");
|
||||
|
||||
const result = await withStateDir(stateDir, async () => {
|
||||
return discoverOpenClawPlugins({});
|
||||
});
|
||||
const result = await discoverWithStateDir(stateDir, {});
|
||||
|
||||
expect(result.candidates).toHaveLength(0);
|
||||
expectEscapesPackageDiagnostic(result.diagnostics);
|
||||
@@ -225,9 +232,7 @@ describe("discoverOpenClawPlugins", () => {
|
||||
extensions: ["./linked/escape.ts"],
|
||||
});
|
||||
|
||||
const { candidates, diagnostics } = await withStateDir(stateDir, async () => {
|
||||
return discoverOpenClawPlugins({});
|
||||
});
|
||||
const { candidates, diagnostics } = await discoverWithStateDir(stateDir, {});
|
||||
|
||||
expect(candidates.some((candidate) => candidate.idHint === "pack")).toBe(false);
|
||||
expectEscapesPackageDiagnostic(diagnostics);
|
||||
|
||||
@@ -2,6 +2,41 @@ import { describe, expect, it, vi } from "vitest";
|
||||
import { registerPluginHttpRoute } from "./http-registry.js";
|
||||
import { createEmptyPluginRegistry } from "./registry.js";
|
||||
|
||||
function expectRouteRegistrationDenied(params: {
|
||||
replaceExisting: boolean;
|
||||
expectedLogFragment: string;
|
||||
}) {
|
||||
const registry = createEmptyPluginRegistry();
|
||||
const logs: string[] = [];
|
||||
|
||||
registerPluginHttpRoute({
|
||||
path: "/plugins/demo",
|
||||
auth: "plugin",
|
||||
handler: vi.fn(),
|
||||
registry,
|
||||
pluginId: "demo-a",
|
||||
source: "demo-a-src",
|
||||
log: (msg) => logs.push(msg),
|
||||
});
|
||||
|
||||
const unregister = registerPluginHttpRoute({
|
||||
path: "/plugins/demo",
|
||||
auth: "plugin",
|
||||
...(params.replaceExisting ? { replaceExisting: true } : {}),
|
||||
handler: vi.fn(),
|
||||
registry,
|
||||
pluginId: "demo-b",
|
||||
source: "demo-b-src",
|
||||
log: (msg) => logs.push(msg),
|
||||
});
|
||||
|
||||
expect(registry.httpRoutes).toHaveLength(1);
|
||||
expect(logs.at(-1)).toContain(params.expectedLogFragment);
|
||||
|
||||
unregister();
|
||||
expect(registry.httpRoutes).toHaveLength(1);
|
||||
}
|
||||
|
||||
describe("registerPluginHttpRoute", () => {
|
||||
it("registers route and unregisters it", () => {
|
||||
const registry = createEmptyPluginRegistry();
|
||||
@@ -84,65 +119,16 @@ describe("registerPluginHttpRoute", () => {
|
||||
});
|
||||
|
||||
it("rejects conflicting route registrations without replaceExisting", () => {
|
||||
const registry = createEmptyPluginRegistry();
|
||||
const logs: string[] = [];
|
||||
|
||||
registerPluginHttpRoute({
|
||||
path: "/plugins/demo",
|
||||
auth: "plugin",
|
||||
handler: vi.fn(),
|
||||
registry,
|
||||
pluginId: "demo-a",
|
||||
source: "demo-a-src",
|
||||
log: (msg) => logs.push(msg),
|
||||
expectRouteRegistrationDenied({
|
||||
replaceExisting: false,
|
||||
expectedLogFragment: "route conflict",
|
||||
});
|
||||
|
||||
const unregister = registerPluginHttpRoute({
|
||||
path: "/plugins/demo",
|
||||
auth: "plugin",
|
||||
handler: vi.fn(),
|
||||
registry,
|
||||
pluginId: "demo-b",
|
||||
source: "demo-b-src",
|
||||
log: (msg) => logs.push(msg),
|
||||
});
|
||||
|
||||
expect(registry.httpRoutes).toHaveLength(1);
|
||||
expect(logs.at(-1)).toContain("route conflict");
|
||||
|
||||
unregister();
|
||||
expect(registry.httpRoutes).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("rejects route replacement when a different plugin owns the route", () => {
|
||||
const registry = createEmptyPluginRegistry();
|
||||
const logs: string[] = [];
|
||||
|
||||
registerPluginHttpRoute({
|
||||
path: "/plugins/demo",
|
||||
auth: "plugin",
|
||||
handler: vi.fn(),
|
||||
registry,
|
||||
pluginId: "demo-a",
|
||||
source: "demo-a-src",
|
||||
log: (msg) => logs.push(msg),
|
||||
});
|
||||
|
||||
const unregister = registerPluginHttpRoute({
|
||||
path: "/plugins/demo",
|
||||
auth: "plugin",
|
||||
expectRouteRegistrationDenied({
|
||||
replaceExisting: true,
|
||||
handler: vi.fn(),
|
||||
registry,
|
||||
pluginId: "demo-b",
|
||||
source: "demo-b-src",
|
||||
log: (msg) => logs.push(msg),
|
||||
expectedLogFragment: "route replacement denied",
|
||||
});
|
||||
|
||||
expect(registry.httpRoutes).toHaveLength(1);
|
||||
expect(logs.at(-1)).toContain("route replacement denied");
|
||||
|
||||
unregister();
|
||||
expect(registry.httpRoutes).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -507,6 +507,18 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi
|
||||
record.kind = manifestRecord.kind;
|
||||
record.configUiHints = manifestRecord.configUiHints;
|
||||
record.configJsonSchema = manifestRecord.configSchema;
|
||||
const pushPluginLoadError = (message: string) => {
|
||||
record.status = "error";
|
||||
record.error = message;
|
||||
registry.plugins.push(record);
|
||||
seenIds.set(pluginId, candidate.origin);
|
||||
registry.diagnostics.push({
|
||||
level: "error",
|
||||
pluginId: record.id,
|
||||
source: record.source,
|
||||
message: record.error,
|
||||
});
|
||||
};
|
||||
|
||||
if (!enableState.enabled) {
|
||||
record.status = "disabled";
|
||||
@@ -517,16 +529,7 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi
|
||||
}
|
||||
|
||||
if (!manifestRecord.configSchema) {
|
||||
record.status = "error";
|
||||
record.error = "missing config schema";
|
||||
registry.plugins.push(record);
|
||||
seenIds.set(pluginId, candidate.origin);
|
||||
registry.diagnostics.push({
|
||||
level: "error",
|
||||
pluginId: record.id,
|
||||
source: record.source,
|
||||
message: record.error,
|
||||
});
|
||||
pushPluginLoadError("missing config schema");
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -541,16 +544,7 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi
|
||||
skipLexicalRootCheck: true,
|
||||
});
|
||||
if (!opened.ok) {
|
||||
record.status = "error";
|
||||
record.error = "plugin entry path escapes plugin root or fails alias checks";
|
||||
registry.plugins.push(record);
|
||||
seenIds.set(pluginId, candidate.origin);
|
||||
registry.diagnostics.push({
|
||||
level: "error",
|
||||
pluginId: record.id,
|
||||
source: record.source,
|
||||
message: record.error,
|
||||
});
|
||||
pushPluginLoadError("plugin entry path escapes plugin root or fails alias checks");
|
||||
continue;
|
||||
}
|
||||
const safeSource = opened.path;
|
||||
@@ -634,16 +628,7 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi
|
||||
|
||||
if (!validatedConfig.ok) {
|
||||
logger.error(`[plugins] ${record.id} invalid config: ${validatedConfig.errors?.join(", ")}`);
|
||||
record.status = "error";
|
||||
record.error = `invalid config: ${validatedConfig.errors?.join(", ")}`;
|
||||
registry.plugins.push(record);
|
||||
seenIds.set(pluginId, candidate.origin);
|
||||
registry.diagnostics.push({
|
||||
level: "error",
|
||||
pluginId: record.id,
|
||||
source: record.source,
|
||||
message: record.error,
|
||||
});
|
||||
pushPluginLoadError(`invalid config: ${validatedConfig.errors?.join(", ")}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -655,16 +640,7 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi
|
||||
|
||||
if (typeof register !== "function") {
|
||||
logger.error(`[plugins] ${record.id} missing register/activate export`);
|
||||
record.status = "error";
|
||||
record.error = "plugin export missing register/activate";
|
||||
registry.plugins.push(record);
|
||||
seenIds.set(pluginId, candidate.origin);
|
||||
registry.diagnostics.push({
|
||||
level: "error",
|
||||
pluginId: record.id,
|
||||
source: record.source,
|
||||
message: record.error,
|
||||
});
|
||||
pushPluginLoadError("plugin export missing register/activate");
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -71,64 +71,47 @@ function resolveWithConflictingCoreName(options?: { suppressNameConflicts?: bool
|
||||
});
|
||||
}
|
||||
|
||||
function setOptionalDemoRegistry() {
|
||||
setRegistry([
|
||||
{
|
||||
pluginId: "optional-demo",
|
||||
optional: true,
|
||||
source: "/tmp/optional-demo.js",
|
||||
factory: () => makeTool("optional_tool"),
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
function resolveOptionalDemoTools(toolAllowlist?: string[]) {
|
||||
return resolvePluginTools({
|
||||
context: createContext() as never,
|
||||
...(toolAllowlist ? { toolAllowlist } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
describe("resolvePluginTools optional tools", () => {
|
||||
beforeEach(() => {
|
||||
loadOpenClawPluginsMock.mockClear();
|
||||
});
|
||||
|
||||
it("skips optional tools without explicit allowlist", () => {
|
||||
setRegistry([
|
||||
{
|
||||
pluginId: "optional-demo",
|
||||
optional: true,
|
||||
source: "/tmp/optional-demo.js",
|
||||
factory: () => makeTool("optional_tool"),
|
||||
},
|
||||
]);
|
||||
|
||||
const tools = resolvePluginTools({
|
||||
context: createContext() as never,
|
||||
});
|
||||
setOptionalDemoRegistry();
|
||||
const tools = resolveOptionalDemoTools();
|
||||
|
||||
expect(tools).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("allows optional tools by tool name", () => {
|
||||
setRegistry([
|
||||
{
|
||||
pluginId: "optional-demo",
|
||||
optional: true,
|
||||
source: "/tmp/optional-demo.js",
|
||||
factory: () => makeTool("optional_tool"),
|
||||
},
|
||||
]);
|
||||
|
||||
const tools = resolvePluginTools({
|
||||
context: createContext() as never,
|
||||
toolAllowlist: ["optional_tool"],
|
||||
});
|
||||
setOptionalDemoRegistry();
|
||||
const tools = resolveOptionalDemoTools(["optional_tool"]);
|
||||
|
||||
expect(tools.map((tool) => tool.name)).toEqual(["optional_tool"]);
|
||||
});
|
||||
|
||||
it("allows optional tools via plugin-scoped allowlist entries", () => {
|
||||
setRegistry([
|
||||
{
|
||||
pluginId: "optional-demo",
|
||||
optional: true,
|
||||
source: "/tmp/optional-demo.js",
|
||||
factory: () => makeTool("optional_tool"),
|
||||
},
|
||||
]);
|
||||
|
||||
const toolsByPlugin = resolvePluginTools({
|
||||
context: createContext() as never,
|
||||
toolAllowlist: ["optional-demo"],
|
||||
});
|
||||
const toolsByGroup = resolvePluginTools({
|
||||
context: createContext() as never,
|
||||
toolAllowlist: ["group:plugins"],
|
||||
});
|
||||
setOptionalDemoRegistry();
|
||||
const toolsByPlugin = resolveOptionalDemoTools(["optional-demo"]);
|
||||
const toolsByGroup = resolveOptionalDemoTools(["group:plugins"]);
|
||||
|
||||
expect(toolsByPlugin.map((tool) => tool.name)).toEqual(["optional_tool"]);
|
||||
expect(toolsByGroup.map((tool) => tool.name)).toEqual(["optional_tool"]);
|
||||
|
||||
Reference in New Issue
Block a user