Files
openclaw/src/plugins/services.test.ts
2026-03-28 04:02:13 +00:00

157 lines
4.5 KiB
TypeScript

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<typeof startPluginServices>[0]["config"];
}
function expectServiceContext(
ctx: OpenClawPluginServiceContext,
config: Parameters<typeof startPluginServices>[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<typeof startPluginServices>[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();
});
});