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>(), child: vi.fn(() => mockedLogger), })); 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(); registry.services = services.map((service) => ({ pluginId: "plugin:test", service, source: "test", rootDir: "/plugins/test-plugin", })) as typeof registry.services; return registry; } function createServiceConfig() { return {} as Parameters[0]["config"]; } function expectServiceContext( ctx: OpenClawPluginServiceContext, config: Parameters[0]["config"], ) { expect(ctx.config).toBe(config); expect(ctx.workspaceDir).toBe("/tmp/workspace"); expect(ctx.stateDir).toBe(STATE_DIR); expectServiceLogger(ctx); } function expectServiceLogger(ctx: OpenClawPluginServiceContext) { 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"); } function expectServiceContexts( contexts: OpenClawPluginServiceContext[], config: Parameters[0]["config"], ) { expect(contexts).not.toHaveLength(0); contexts.forEach((ctx) => { expectServiceContext(ctx, config); }); } function createTrackingService( id: string, params: { starts?: string[]; stops?: string[]; contexts?: OpenClawPluginServiceContext[]; failOnStart?: boolean; failOnStop?: boolean; stopSpy?: () => void; } = {}, ): OpenClawPluginService { return { id, start: (ctx) => { if (params.failOnStart) { throw new Error("start failed"); } params.starts?.push(id.at(-1) ?? id); params.contexts?.push(ctx); }, stop: params.stopSpy ? () => { params.stopSpy?.(); } : params.stops || params.failOnStop ? () => { if (params.failOnStop) { throw new Error("stop failed"); } params.stops?.push(id.at(-1) ?? id); } : undefined, }; } 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 config = createServiceConfig(); const handle = await startPluginServices({ registry: createRegistry([ createTrackingService("service-a", { starts, stops, contexts }), createTrackingService("service-b", { starts, contexts }), createTrackingService("service-c", { starts, stops, contexts }), ]), config, workspaceDir: "/tmp/workspace", }); await handle.stop(); expect(starts).toEqual(["a", "b", "c"]); expect(stops).toEqual(["c", "a"]); expect(contexts).toHaveLength(3); expectServiceContexts(contexts, config); }); 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([ createTrackingService("service-start-fail", { failOnStart: true, stopSpy: vi.fn(), }), createTrackingService("service-ok", { stopSpy: stopOk }), createTrackingService("service-stop-fail", { stopSpy: stopThrows }), ]), config: createServiceConfig(), }); await handle.stop(); expect(mockedLogger.error).toHaveBeenCalledWith( expect.stringContaining( "plugin service failed (service-start-fail, plugin=plugin:test, root=/plugins/test-plugin):", ), ); expect(mockedLogger.warn).toHaveBeenCalledWith( expect.stringContaining("plugin service stop failed (service-stop-fail):"), ); expect(stopOk).toHaveBeenCalledOnce(); expect(stopThrows).toHaveBeenCalledOnce(); }); });