mirror of
https://github.com/openclaw/openclaw.git
synced 2026-02-19 18:39:20 -05:00
refactor(plugins): reuse plugin service runtime context
This commit is contained in:
127
src/plugins/services.test.ts
Normal file
127
src/plugins/services.test.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { createEmptyPluginRegistry } from "./registry.js";
|
||||
import type { OpenClawPluginService, OpenClawPluginServiceContext } from "./types.js";
|
||||
|
||||
const mockedLogger = vi.hoisted(() => ({
|
||||
info: vi.fn<(msg: string) => void>(),
|
||||
warn: vi.fn<(msg: string) => void>(),
|
||||
error: vi.fn<(msg: string) => void>(),
|
||||
debug: vi.fn<(msg: string) => void>(),
|
||||
}));
|
||||
|
||||
vi.mock("../logging/subsystem.js", () => ({
|
||||
createSubsystemLogger: () => mockedLogger,
|
||||
}));
|
||||
|
||||
import { STATE_DIR } from "../config/paths.js";
|
||||
import { startPluginServices } from "./services.js";
|
||||
|
||||
function createRegistry(services: OpenClawPluginService[]) {
|
||||
const registry = createEmptyPluginRegistry();
|
||||
for (const service of services) {
|
||||
registry.services.push({ pluginId: "plugin:test", service, source: "test" });
|
||||
}
|
||||
return registry;
|
||||
}
|
||||
|
||||
describe("startPluginServices", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("starts services and stops them in reverse order", async () => {
|
||||
const starts: string[] = [];
|
||||
const stops: string[] = [];
|
||||
const contexts: OpenClawPluginServiceContext[] = [];
|
||||
|
||||
const serviceA: OpenClawPluginService = {
|
||||
id: "service-a",
|
||||
start: (ctx) => {
|
||||
starts.push("a");
|
||||
contexts.push(ctx);
|
||||
},
|
||||
stop: () => {
|
||||
stops.push("a");
|
||||
},
|
||||
};
|
||||
const serviceB: OpenClawPluginService = {
|
||||
id: "service-b",
|
||||
start: (ctx) => {
|
||||
starts.push("b");
|
||||
contexts.push(ctx);
|
||||
},
|
||||
};
|
||||
const serviceC: OpenClawPluginService = {
|
||||
id: "service-c",
|
||||
start: (ctx) => {
|
||||
starts.push("c");
|
||||
contexts.push(ctx);
|
||||
},
|
||||
stop: () => {
|
||||
stops.push("c");
|
||||
},
|
||||
};
|
||||
|
||||
const config = {} as Parameters<typeof startPluginServices>[0]["config"];
|
||||
const handle = await startPluginServices({
|
||||
registry: createRegistry([serviceA, serviceB, serviceC]),
|
||||
config,
|
||||
workspaceDir: "/tmp/workspace",
|
||||
});
|
||||
await handle.stop();
|
||||
|
||||
expect(starts).toEqual(["a", "b", "c"]);
|
||||
expect(stops).toEqual(["c", "a"]);
|
||||
expect(contexts).toHaveLength(3);
|
||||
for (const ctx of contexts) {
|
||||
expect(ctx.config).toBe(config);
|
||||
expect(ctx.workspaceDir).toBe("/tmp/workspace");
|
||||
expect(ctx.stateDir).toBe(STATE_DIR);
|
||||
expect(ctx.logger).toBeDefined();
|
||||
expect(typeof ctx.logger.info).toBe("function");
|
||||
expect(typeof ctx.logger.warn).toBe("function");
|
||||
expect(typeof ctx.logger.error).toBe("function");
|
||||
}
|
||||
});
|
||||
|
||||
it("logs start/stop failures and continues", async () => {
|
||||
const stopOk = vi.fn();
|
||||
const stopThrows = vi.fn(() => {
|
||||
throw new Error("stop failed");
|
||||
});
|
||||
|
||||
const handle = await startPluginServices({
|
||||
registry: createRegistry([
|
||||
{
|
||||
id: "service-start-fail",
|
||||
start: () => {
|
||||
throw new Error("start failed");
|
||||
},
|
||||
stop: vi.fn(),
|
||||
},
|
||||
{
|
||||
id: "service-ok",
|
||||
start: () => undefined,
|
||||
stop: stopOk,
|
||||
},
|
||||
{
|
||||
id: "service-stop-fail",
|
||||
start: () => undefined,
|
||||
stop: stopThrows,
|
||||
},
|
||||
]),
|
||||
config: {} as Parameters<typeof startPluginServices>[0]["config"],
|
||||
});
|
||||
|
||||
await handle.stop();
|
||||
|
||||
expect(mockedLogger.error).toHaveBeenCalledWith(
|
||||
expect.stringContaining("plugin service failed (service-start-fail):"),
|
||||
);
|
||||
expect(mockedLogger.warn).toHaveBeenCalledWith(
|
||||
expect.stringContaining("plugin service stop failed (service-stop-fail):"),
|
||||
);
|
||||
expect(stopOk).toHaveBeenCalledOnce();
|
||||
expect(stopThrows).toHaveBeenCalledOnce();
|
||||
});
|
||||
});
|
||||
@@ -2,9 +2,31 @@ import type { OpenClawConfig } from "../config/config.js";
|
||||
import { STATE_DIR } from "../config/paths.js";
|
||||
import { createSubsystemLogger } from "../logging/subsystem.js";
|
||||
import type { PluginRegistry } from "./registry.js";
|
||||
import type { OpenClawPluginServiceContext, PluginLogger } from "./types.js";
|
||||
|
||||
const log = createSubsystemLogger("plugins");
|
||||
|
||||
function createPluginLogger(): PluginLogger {
|
||||
return {
|
||||
info: (msg) => log.info(msg),
|
||||
warn: (msg) => log.warn(msg),
|
||||
error: (msg) => log.error(msg),
|
||||
debug: (msg) => log.debug(msg),
|
||||
};
|
||||
}
|
||||
|
||||
function createServiceContext(params: {
|
||||
config: OpenClawConfig;
|
||||
workspaceDir?: string;
|
||||
}): OpenClawPluginServiceContext {
|
||||
return {
|
||||
config: params.config,
|
||||
workspaceDir: params.workspaceDir,
|
||||
stateDir: STATE_DIR,
|
||||
logger: createPluginLogger(),
|
||||
};
|
||||
}
|
||||
|
||||
export type PluginServicesHandle = {
|
||||
stop: () => Promise<void>;
|
||||
};
|
||||
@@ -18,37 +40,18 @@ export async function startPluginServices(params: {
|
||||
id: string;
|
||||
stop?: () => void | Promise<void>;
|
||||
}> = [];
|
||||
const serviceContext = createServiceContext({
|
||||
config: params.config,
|
||||
workspaceDir: params.workspaceDir,
|
||||
});
|
||||
|
||||
for (const entry of params.registry.services) {
|
||||
const service = entry.service;
|
||||
try {
|
||||
await service.start({
|
||||
config: params.config,
|
||||
workspaceDir: params.workspaceDir,
|
||||
stateDir: STATE_DIR,
|
||||
logger: {
|
||||
info: (msg) => log.info(msg),
|
||||
warn: (msg) => log.warn(msg),
|
||||
error: (msg) => log.error(msg),
|
||||
debug: (msg) => log.debug(msg),
|
||||
},
|
||||
});
|
||||
await service.start(serviceContext);
|
||||
running.push({
|
||||
id: service.id,
|
||||
stop: service.stop
|
||||
? () =>
|
||||
service.stop?.({
|
||||
config: params.config,
|
||||
workspaceDir: params.workspaceDir,
|
||||
stateDir: STATE_DIR,
|
||||
logger: {
|
||||
info: (msg) => log.info(msg),
|
||||
warn: (msg) => log.warn(msg),
|
||||
error: (msg) => log.error(msg),
|
||||
debug: (msg) => log.debug(msg),
|
||||
},
|
||||
})
|
||||
: undefined,
|
||||
stop: service.stop ? () => service.stop?.(serviceContext) : undefined,
|
||||
});
|
||||
} catch (err) {
|
||||
log.error(`plugin service failed (${service.id}): ${String(err)}`);
|
||||
|
||||
Reference in New Issue
Block a user