refactor(core): dedupe infra, media, pairing, and plugin helpers

This commit is contained in:
Peter Steinberger
2026-03-02 21:31:18 +00:00
parent 91dd89313a
commit 34daed1d1e
11 changed files with 301 additions and 340 deletions

View File

@@ -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);

View File

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

View File

@@ -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;
}

View File

@@ -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"]);