refactor: share plugin runtime load context

This commit is contained in:
Peter Steinberger
2026-04-06 15:26:12 +01:00
parent 9568cceee3
commit 58f4099a4f
14 changed files with 507 additions and 214 deletions

View File

@@ -0,0 +1,115 @@
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
const loadConfigMock = vi.fn();
const applyPluginAutoEnableMock = vi.fn();
const resolveAgentWorkspaceDirMock = vi.fn(() => "/resolved-workspace");
const resolveDefaultAgentIdMock = vi.fn(() => "default");
let resolvePluginRuntimeLoadContext: typeof import("./load-context.js").resolvePluginRuntimeLoadContext;
let buildPluginRuntimeLoadOptions: typeof import("./load-context.js").buildPluginRuntimeLoadOptions;
vi.mock("../../config/config.js", () => ({
loadConfig: () => loadConfigMock(),
}));
vi.mock("../../config/plugin-auto-enable.js", () => ({
applyPluginAutoEnable: (...args: unknown[]) => applyPluginAutoEnableMock(...args),
}));
vi.mock("../../agents/agent-scope.js", () => ({
resolveAgentWorkspaceDir: (...args: unknown[]) => resolveAgentWorkspaceDirMock(...args),
resolveDefaultAgentId: (...args: unknown[]) => resolveDefaultAgentIdMock(...args),
}));
describe("resolvePluginRuntimeLoadContext", () => {
beforeAll(async () => {
({ resolvePluginRuntimeLoadContext, buildPluginRuntimeLoadOptions } =
await import("./load-context.js"));
});
beforeEach(() => {
loadConfigMock.mockReset();
applyPluginAutoEnableMock.mockReset();
resolveAgentWorkspaceDirMock.mockClear();
resolveDefaultAgentIdMock.mockClear();
loadConfigMock.mockReturnValue({ plugins: {} });
applyPluginAutoEnableMock.mockImplementation((params: { config: unknown }) => ({
config: params.config,
changes: [],
autoEnabledReasons: {},
}));
});
it("builds the runtime plugin load context from the auto-enabled config", () => {
const rawConfig = { plugins: {} };
const resolvedConfig = {
plugins: {
entries: {
demo: { enabled: true },
},
},
};
const env = { HOME: "/tmp/openclaw-home" } as NodeJS.ProcessEnv;
applyPluginAutoEnableMock.mockReturnValue({
config: resolvedConfig,
changes: [],
autoEnabledReasons: {
demo: ["demo configured"],
},
});
const context = resolvePluginRuntimeLoadContext({
config: rawConfig,
env,
});
expect(context).toEqual(
expect.objectContaining({
rawConfig,
config: resolvedConfig,
activationSourceConfig: rawConfig,
env,
workspaceDir: "/resolved-workspace",
autoEnabledReasons: {
demo: ["demo configured"],
},
}),
);
expect(applyPluginAutoEnableMock).toHaveBeenCalledWith({
config: rawConfig,
env,
});
expect(resolveDefaultAgentIdMock).toHaveBeenCalledWith(resolvedConfig);
expect(resolveAgentWorkspaceDirMock).toHaveBeenCalledWith(resolvedConfig, "default");
});
it("builds plugin load options from the shared runtime context", () => {
const context = resolvePluginRuntimeLoadContext({
config: { plugins: {} },
env: { HOME: "/tmp/openclaw-home" } as NodeJS.ProcessEnv,
workspaceDir: "/explicit-workspace",
});
expect(
buildPluginRuntimeLoadOptions(context, {
cache: false,
activate: false,
onlyPluginIds: ["demo"],
}),
).toEqual(
expect.objectContaining({
config: context.config,
activationSourceConfig: context.activationSourceConfig,
autoEnabledReasons: context.autoEnabledReasons,
workspaceDir: "/explicit-workspace",
env: context.env,
logger: context.logger,
cache: false,
activate: false,
onlyPluginIds: ["demo"],
}),
);
});
});

View File

@@ -0,0 +1,83 @@
import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../../agents/agent-scope.js";
import type { OpenClawConfig } from "../../config/config.js";
import { loadConfig } from "../../config/config.js";
import { applyPluginAutoEnable } from "../../config/plugin-auto-enable.js";
import { createSubsystemLogger } from "../../logging.js";
import type { PluginLoadOptions } from "../loader.js";
import type { PluginLogger } from "../types.js";
const log = createSubsystemLogger("plugins");
export type PluginRuntimeLoadContext = {
rawConfig: OpenClawConfig;
config: OpenClawConfig;
activationSourceConfig: OpenClawConfig;
autoEnabledReasons: Readonly<Record<string, string[]>>;
workspaceDir: string | undefined;
env: NodeJS.ProcessEnv;
logger: PluginLogger;
};
export type PluginRuntimeResolvedLoadValues = Pick<
PluginLoadOptions,
"config" | "activationSourceConfig" | "autoEnabledReasons" | "workspaceDir" | "env" | "logger"
>;
export type PluginRuntimeLoadContextOptions = {
config?: OpenClawConfig;
activationSourceConfig?: OpenClawConfig;
env?: NodeJS.ProcessEnv;
workspaceDir?: string;
logger?: PluginLogger;
};
export function createPluginRuntimeLoaderLogger(): PluginLogger {
return {
info: (message) => log.info(message),
warn: (message) => log.warn(message),
error: (message) => log.error(message),
debug: (message) => log.debug(message),
};
}
export function resolvePluginRuntimeLoadContext(
options?: PluginRuntimeLoadContextOptions,
): PluginRuntimeLoadContext {
const env = options?.env ?? process.env;
const rawConfig = options?.config ?? loadConfig();
const autoEnabled = applyPluginAutoEnable({ config: rawConfig, env });
const config = autoEnabled.config;
const workspaceDir =
options?.workspaceDir ?? resolveAgentWorkspaceDir(config, resolveDefaultAgentId(config));
return {
rawConfig,
config,
activationSourceConfig: options?.activationSourceConfig ?? rawConfig,
autoEnabledReasons: autoEnabled.autoEnabledReasons,
workspaceDir,
env,
logger: options?.logger ?? createPluginRuntimeLoaderLogger(),
};
}
export function buildPluginRuntimeLoadOptions(
context: PluginRuntimeLoadContext,
overrides?: Partial<PluginLoadOptions>,
): PluginLoadOptions {
return buildPluginRuntimeLoadOptionsFromValues(context, overrides);
}
export function buildPluginRuntimeLoadOptionsFromValues(
values: PluginRuntimeResolvedLoadValues,
overrides?: Partial<PluginLoadOptions>,
): PluginLoadOptions {
return {
config: values.config,
activationSourceConfig: values.activationSourceConfig,
autoEnabledReasons: values.autoEnabledReasons,
workspaceDir: values.workspaceDir,
env: values.env,
logger: values.logger,
...overrides,
};
}

View File

@@ -1,13 +1,7 @@
import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../../agents/agent-scope.js";
import type { OpenClawConfig } from "../../config/config.js";
import { loadConfig } from "../../config/config.js";
import { applyPluginAutoEnable } from "../../config/plugin-auto-enable.js";
import { createSubsystemLogger } from "../../logging.js";
import { loadOpenClawPlugins } from "../loader.js";
import type { PluginRegistry } from "../registry.js";
import type { PluginLogger } from "../types.js";
const log = createSubsystemLogger("plugins");
import { buildPluginRuntimeLoadOptions, resolvePluginRuntimeLoadContext } from "./load-context.js";
export function loadPluginMetadataRegistrySnapshot(options?: {
config?: OpenClawConfig;
@@ -17,32 +11,16 @@ export function loadPluginMetadataRegistrySnapshot(options?: {
onlyPluginIds?: string[];
loadModules?: boolean;
}): PluginRegistry {
const env = options?.env ?? process.env;
const baseConfig = options?.config ?? loadConfig();
const autoEnabled = applyPluginAutoEnable({ config: baseConfig, env });
const resolvedConfig = autoEnabled.config;
const workspaceDir =
options?.workspaceDir ??
resolveAgentWorkspaceDir(resolvedConfig, resolveDefaultAgentId(resolvedConfig));
const logger: PluginLogger = {
info: (message) => log.info(message),
warn: (message) => log.warn(message),
error: (message) => log.error(message),
debug: (message) => log.debug(message),
};
const context = resolvePluginRuntimeLoadContext(options);
return loadOpenClawPlugins({
config: resolvedConfig,
activationSourceConfig: options?.activationSourceConfig ?? baseConfig,
autoEnabledReasons: autoEnabled.autoEnabledReasons,
workspaceDir,
env,
logger,
throwOnLoadError: true,
cache: false,
activate: false,
mode: "validate",
loadModules: options?.loadModules,
...(options?.onlyPluginIds?.length ? { onlyPluginIds: options.onlyPluginIds } : {}),
});
return loadOpenClawPlugins(
buildPluginRuntimeLoadOptions(context, {
throwOnLoadError: true,
cache: false,
activate: false,
mode: "validate",
loadModules: options?.loadModules,
...(options?.onlyPluginIds?.length ? { onlyPluginIds: options.onlyPluginIds } : {}),
}),
);
}

View File

@@ -0,0 +1,145 @@
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
const loadOpenClawPluginsMock = vi.fn();
const getActivePluginRegistryMock = vi.fn();
const resolveConfiguredChannelPluginIdsMock = vi.fn();
const resolveChannelPluginIdsMock = vi.fn();
const applyPluginAutoEnableMock = vi.fn();
const resolveAgentWorkspaceDirMock = vi.fn(() => "/resolved-workspace");
const resolveDefaultAgentIdMock = vi.fn(() => "default");
let ensurePluginRegistryLoaded: typeof import("./runtime-registry-loader.js").ensurePluginRegistryLoaded;
let resetPluginRegistryLoadedForTests: typeof import("./runtime-registry-loader.js").__testing.resetPluginRegistryLoadedForTests;
vi.mock("../loader.js", () => ({
loadOpenClawPlugins: (...args: unknown[]) => loadOpenClawPluginsMock(...args),
}));
vi.mock("../runtime.js", () => ({
getActivePluginRegistry: (...args: unknown[]) => getActivePluginRegistryMock(...args),
}));
vi.mock("../channel-plugin-ids.js", () => ({
resolveConfiguredChannelPluginIds: (...args: unknown[]) =>
resolveConfiguredChannelPluginIdsMock(...args),
resolveChannelPluginIds: (...args: unknown[]) => resolveChannelPluginIdsMock(...args),
}));
vi.mock("../../config/plugin-auto-enable.js", () => ({
applyPluginAutoEnable: (...args: unknown[]) => applyPluginAutoEnableMock(...args),
}));
vi.mock("../../agents/agent-scope.js", () => ({
resolveAgentWorkspaceDir: (...args: unknown[]) => resolveAgentWorkspaceDirMock(...args),
resolveDefaultAgentId: (...args: unknown[]) => resolveDefaultAgentIdMock(...args),
}));
describe("ensurePluginRegistryLoaded", () => {
beforeAll(async () => {
const mod = await import("./runtime-registry-loader.js");
ensurePluginRegistryLoaded = mod.ensurePluginRegistryLoaded;
resetPluginRegistryLoadedForTests = () => mod.__testing.resetPluginRegistryLoadedForTests();
});
beforeEach(() => {
loadOpenClawPluginsMock.mockReset();
getActivePluginRegistryMock.mockReset();
resolveConfiguredChannelPluginIdsMock.mockReset();
resolveChannelPluginIdsMock.mockReset();
applyPluginAutoEnableMock.mockReset();
resolveAgentWorkspaceDirMock.mockClear();
resolveDefaultAgentIdMock.mockClear();
resetPluginRegistryLoadedForTests();
getActivePluginRegistryMock.mockReturnValue({
plugins: [],
channels: [],
tools: [],
});
applyPluginAutoEnableMock.mockImplementation((params: { config: unknown }) => ({
config:
params.config && typeof params.config === "object"
? {
...params.config,
plugins: {
entries: {
demo: { enabled: true },
},
},
}
: params.config,
changes: [],
autoEnabledReasons: {
demo: ["demo configured"],
},
}));
});
it("uses the shared runtime load context for configured-channel loads", () => {
const rawConfig = { channels: { demo: { enabled: true } } };
const resolvedConfig = {
...rawConfig,
plugins: {
entries: {
demo: { enabled: true },
},
},
};
const env = { HOME: "/tmp/openclaw-home" } as NodeJS.ProcessEnv;
resolveConfiguredChannelPluginIdsMock.mockReturnValue(["demo-channel"]);
ensurePluginRegistryLoaded({
scope: "configured-channels",
config: rawConfig as never,
env,
activationSourceConfig: { plugins: { allow: ["demo-channel"] } } as never,
});
expect(resolveConfiguredChannelPluginIdsMock).toHaveBeenCalledWith(
expect.objectContaining({
config: resolvedConfig,
env,
workspaceDir: "/resolved-workspace",
}),
);
expect(applyPluginAutoEnableMock).toHaveBeenCalledWith({
config: rawConfig,
env,
});
expect(loadOpenClawPluginsMock).toHaveBeenCalledWith(
expect.objectContaining({
config: resolvedConfig,
activationSourceConfig: { plugins: { allow: ["demo-channel"] } },
autoEnabledReasons: {
demo: ["demo configured"],
},
workspaceDir: "/resolved-workspace",
onlyPluginIds: ["demo-channel"],
throwOnLoadError: true,
}),
);
});
it("does not cache scoped loads by explicit plugin ids", () => {
ensurePluginRegistryLoaded({
scope: "configured-channels",
config: {} as never,
onlyPluginIds: ["demo-a"],
});
ensurePluginRegistryLoaded({
scope: "configured-channels",
config: {} as never,
onlyPluginIds: ["demo-b"],
});
expect(loadOpenClawPluginsMock).toHaveBeenCalledTimes(2);
expect(loadOpenClawPluginsMock).toHaveBeenNthCalledWith(
1,
expect.objectContaining({ onlyPluginIds: ["demo-a"] }),
);
expect(loadOpenClawPluginsMock).toHaveBeenNthCalledWith(
2,
expect.objectContaining({ onlyPluginIds: ["demo-b"] }),
);
});
});

View File

@@ -1,17 +1,12 @@
import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../../agents/agent-scope.js";
import type { OpenClawConfig } from "../../config/config.js";
import { loadConfig } from "../../config/config.js";
import { applyPluginAutoEnable } from "../../config/plugin-auto-enable.js";
import { createSubsystemLogger } from "../../logging.js";
import {
resolveChannelPluginIds,
resolveConfiguredChannelPluginIds,
} from "../channel-plugin-ids.js";
import { loadOpenClawPlugins } from "../loader.js";
import { getActivePluginRegistry } from "../runtime.js";
import type { PluginLogger } from "../types.js";
import { buildPluginRuntimeLoadOptions, resolvePluginRuntimeLoadContext } from "./load-context.js";
const log = createSubsystemLogger("plugins");
let pluginRegistryLoaded: "none" | "configured-channels" | "channels" | "all" = "none";
export type PluginRegistryScope = "configured-channels" | "channels" | "all";
@@ -71,27 +66,20 @@ export function ensurePluginRegistryLoaded(options?: {
if (!scopedLoad && scopeRank(pluginRegistryLoaded) >= scopeRank(scope)) {
return;
}
const env = options?.env ?? process.env;
const baseConfig = options?.config ?? loadConfig();
const autoEnabled = applyPluginAutoEnable({ config: baseConfig, env });
const resolvedConfig = autoEnabled.config;
const workspaceDir = resolveAgentWorkspaceDir(
resolvedConfig,
resolveDefaultAgentId(resolvedConfig),
);
const context = resolvePluginRuntimeLoadContext(options);
const expectedChannelPluginIds = scopedLoad
? requestedPluginIds
: scope === "configured-channels"
? resolveConfiguredChannelPluginIds({
config: resolvedConfig,
workspaceDir,
env,
config: context.config,
workspaceDir: context.workspaceDir,
env: context.env,
})
: scope === "channels"
? resolveChannelPluginIds({
config: resolvedConfig,
workspaceDir,
env,
config: context.config,
workspaceDir: context.workspaceDir,
env: context.env,
})
: [];
const active = getActivePluginRegistry();
@@ -104,21 +92,12 @@ export function ensurePluginRegistryLoaded(options?: {
}
return;
}
const logger: PluginLogger = {
info: (msg) => log.info(msg),
warn: (msg) => log.warn(msg),
error: (msg) => log.error(msg),
debug: (msg) => log.debug(msg),
};
loadOpenClawPlugins({
config: resolvedConfig,
activationSourceConfig: options?.activationSourceConfig ?? baseConfig,
autoEnabledReasons: autoEnabled.autoEnabledReasons,
workspaceDir,
logger,
throwOnLoadError: true,
...(expectedChannelPluginIds.length > 0 ? { onlyPluginIds: expectedChannelPluginIds } : {}),
});
loadOpenClawPlugins(
buildPluginRuntimeLoadOptions(context, {
throwOnLoadError: true,
...(expectedChannelPluginIds.length > 0 ? { onlyPluginIds: expectedChannelPluginIds } : {}),
}),
);
if (!scopedLoad) {
pluginRegistryLoaded = scope;
}