mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-18 21:40:53 +00:00
290 lines
8.8 KiB
TypeScript
290 lines
8.8 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
|
import type { PluginRegistry } from "../plugins/registry.js";
|
|
import type { PluginRuntime } from "../plugins/runtime/types.js";
|
|
import type { PluginDiagnostic } from "../plugins/types.js";
|
|
import type { GatewayRequestContext, GatewayRequestOptions } from "./server-methods/types.js";
|
|
|
|
const loadOpenClawPlugins = vi.hoisted(() => vi.fn());
|
|
type HandleGatewayRequestOptions = GatewayRequestOptions & {
|
|
extraHandlers?: Record<string, unknown>;
|
|
};
|
|
const handleGatewayRequest = vi.hoisted(() =>
|
|
vi.fn(async (_opts: HandleGatewayRequestOptions) => {}),
|
|
);
|
|
|
|
vi.mock("../plugins/loader.js", () => ({
|
|
loadOpenClawPlugins,
|
|
}));
|
|
|
|
vi.mock("./server-methods.js", () => ({
|
|
handleGatewayRequest,
|
|
}));
|
|
|
|
const createRegistry = (diagnostics: PluginDiagnostic[]): PluginRegistry => ({
|
|
plugins: [],
|
|
tools: [],
|
|
hooks: [],
|
|
typedHooks: [],
|
|
channels: [],
|
|
channelSetups: [],
|
|
commands: [],
|
|
providers: [],
|
|
speechProviders: [],
|
|
mediaUnderstandingProviders: [],
|
|
imageGenerationProviders: [],
|
|
webSearchProviders: [],
|
|
gatewayHandlers: {},
|
|
httpRoutes: [],
|
|
cliRegistrars: [],
|
|
services: [],
|
|
diagnostics,
|
|
});
|
|
|
|
type ServerPluginsModule = typeof import("./server-plugins.js");
|
|
|
|
function createTestContext(label: string): GatewayRequestContext {
|
|
return { label } as unknown as GatewayRequestContext;
|
|
}
|
|
|
|
function getLastDispatchedContext(): GatewayRequestContext | undefined {
|
|
const call = handleGatewayRequest.mock.calls.at(-1)?.[0];
|
|
return call?.context;
|
|
}
|
|
|
|
async function importServerPluginsModule(): Promise<ServerPluginsModule> {
|
|
return import("./server-plugins.js");
|
|
}
|
|
|
|
async function createSubagentRuntime(
|
|
serverPlugins: ServerPluginsModule,
|
|
): Promise<PluginRuntime["subagent"]> {
|
|
const log = {
|
|
info: vi.fn(),
|
|
warn: vi.fn(),
|
|
error: vi.fn(),
|
|
debug: vi.fn(),
|
|
};
|
|
loadOpenClawPlugins.mockReturnValue(createRegistry([]));
|
|
serverPlugins.loadGatewayPlugins({
|
|
cfg: {},
|
|
workspaceDir: "/tmp",
|
|
log,
|
|
coreGatewayHandlers: {},
|
|
baseMethods: [],
|
|
});
|
|
const call = loadOpenClawPlugins.mock.calls.at(-1)?.[0] as
|
|
| { runtimeOptions?: { allowGatewaySubagentBinding?: boolean } }
|
|
| undefined;
|
|
if (call?.runtimeOptions?.allowGatewaySubagentBinding !== true) {
|
|
throw new Error("Expected loadGatewayPlugins to opt into gateway subagent binding");
|
|
}
|
|
const runtimeModule = await import("../plugins/runtime/index.js");
|
|
return runtimeModule.createPluginRuntime({ allowGatewaySubagentBinding: true }).subagent;
|
|
}
|
|
|
|
beforeEach(async () => {
|
|
loadOpenClawPlugins.mockReset();
|
|
handleGatewayRequest.mockReset();
|
|
const runtimeModule = await import("../plugins/runtime/index.js");
|
|
runtimeModule.clearGatewaySubagentRuntime();
|
|
handleGatewayRequest.mockImplementation(async (opts: HandleGatewayRequestOptions) => {
|
|
switch (opts.req.method) {
|
|
case "agent":
|
|
opts.respond(true, { runId: "run-1" });
|
|
return;
|
|
case "agent.wait":
|
|
opts.respond(true, { status: "ok" });
|
|
return;
|
|
case "sessions.get":
|
|
opts.respond(true, { messages: [] });
|
|
return;
|
|
case "sessions.delete":
|
|
opts.respond(true, {});
|
|
return;
|
|
default:
|
|
opts.respond(true, {});
|
|
}
|
|
});
|
|
});
|
|
|
|
afterEach(async () => {
|
|
const runtimeModule = await import("../plugins/runtime/index.js");
|
|
runtimeModule.clearGatewaySubagentRuntime();
|
|
vi.resetModules();
|
|
});
|
|
|
|
describe("loadGatewayPlugins", () => {
|
|
test("logs plugin errors with details", async () => {
|
|
const { loadGatewayPlugins } = await importServerPluginsModule();
|
|
const diagnostics: PluginDiagnostic[] = [
|
|
{
|
|
level: "error",
|
|
pluginId: "telegram",
|
|
source: "/tmp/telegram/index.ts",
|
|
message: "failed to load plugin: boom",
|
|
},
|
|
];
|
|
loadOpenClawPlugins.mockReturnValue(createRegistry(diagnostics));
|
|
|
|
const log = {
|
|
info: vi.fn(),
|
|
warn: vi.fn(),
|
|
error: vi.fn(),
|
|
debug: vi.fn(),
|
|
};
|
|
|
|
loadGatewayPlugins({
|
|
cfg: {},
|
|
workspaceDir: "/tmp",
|
|
log,
|
|
coreGatewayHandlers: {},
|
|
baseMethods: [],
|
|
});
|
|
|
|
expect(log.error).toHaveBeenCalledWith(
|
|
"[plugins] failed to load plugin: boom (plugin=telegram, source=/tmp/telegram/index.ts)",
|
|
);
|
|
expect(log.warn).not.toHaveBeenCalled();
|
|
});
|
|
|
|
test("provides subagent runtime with sessions.get method aliases", async () => {
|
|
const { loadGatewayPlugins } = await importServerPluginsModule();
|
|
loadOpenClawPlugins.mockReturnValue(createRegistry([]));
|
|
|
|
const log = {
|
|
info: vi.fn(),
|
|
warn: vi.fn(),
|
|
error: vi.fn(),
|
|
debug: vi.fn(),
|
|
};
|
|
|
|
loadGatewayPlugins({
|
|
cfg: {},
|
|
workspaceDir: "/tmp",
|
|
log,
|
|
coreGatewayHandlers: {},
|
|
baseMethods: [],
|
|
});
|
|
|
|
const call = loadOpenClawPlugins.mock.calls.at(-1)?.[0] as
|
|
| { runtimeOptions?: { allowGatewaySubagentBinding?: boolean } }
|
|
| undefined;
|
|
expect(call?.runtimeOptions?.allowGatewaySubagentBinding).toBe(true);
|
|
const runtimeModule = await import("../plugins/runtime/index.js");
|
|
const subagent = runtimeModule.createPluginRuntime({
|
|
allowGatewaySubagentBinding: true,
|
|
}).subagent;
|
|
expect(typeof subagent?.getSessionMessages).toBe("function");
|
|
expect(typeof subagent?.getSession).toBe("function");
|
|
});
|
|
|
|
test("can prefer setup-runtime channel plugins during startup loads", async () => {
|
|
const { loadGatewayPlugins } = await importServerPluginsModule();
|
|
loadOpenClawPlugins.mockReturnValue(createRegistry([]));
|
|
|
|
const log = {
|
|
info: vi.fn(),
|
|
warn: vi.fn(),
|
|
error: vi.fn(),
|
|
debug: vi.fn(),
|
|
};
|
|
|
|
loadGatewayPlugins({
|
|
cfg: {},
|
|
workspaceDir: "/tmp",
|
|
log,
|
|
coreGatewayHandlers: {},
|
|
baseMethods: [],
|
|
preferSetupRuntimeForChannelPlugins: true,
|
|
});
|
|
|
|
expect(loadOpenClawPlugins).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
preferSetupRuntimeForChannelPlugins: true,
|
|
}),
|
|
);
|
|
});
|
|
|
|
test("can suppress duplicate diagnostics when reloading full runtime plugins", async () => {
|
|
const { loadGatewayPlugins } = await importServerPluginsModule();
|
|
const diagnostics: PluginDiagnostic[] = [
|
|
{
|
|
level: "error",
|
|
pluginId: "telegram",
|
|
source: "/tmp/telegram/index.ts",
|
|
message: "failed to load plugin: boom",
|
|
},
|
|
];
|
|
loadOpenClawPlugins.mockReturnValue(createRegistry(diagnostics));
|
|
|
|
const log = {
|
|
info: vi.fn(),
|
|
warn: vi.fn(),
|
|
error: vi.fn(),
|
|
debug: vi.fn(),
|
|
};
|
|
|
|
loadGatewayPlugins({
|
|
cfg: {},
|
|
workspaceDir: "/tmp",
|
|
log,
|
|
coreGatewayHandlers: {},
|
|
baseMethods: [],
|
|
logDiagnostics: false,
|
|
});
|
|
|
|
expect(log.error).not.toHaveBeenCalled();
|
|
expect(log.info).not.toHaveBeenCalled();
|
|
});
|
|
|
|
test("shares fallback context across module reloads for existing runtimes", async () => {
|
|
const first = await importServerPluginsModule();
|
|
const runtime = await createSubagentRuntime(first);
|
|
|
|
const staleContext = createTestContext("stale");
|
|
first.setFallbackGatewayContext(staleContext);
|
|
await runtime.run({ sessionKey: "s-1", message: "hello" });
|
|
expect(getLastDispatchedContext()).toBe(staleContext);
|
|
|
|
vi.resetModules();
|
|
const reloaded = await importServerPluginsModule();
|
|
const freshContext = createTestContext("fresh");
|
|
reloaded.setFallbackGatewayContext(freshContext);
|
|
|
|
await runtime.run({ sessionKey: "s-1", message: "hello again" });
|
|
expect(getLastDispatchedContext()).toBe(freshContext);
|
|
});
|
|
|
|
test("uses updated fallback context after context replacement", async () => {
|
|
const serverPlugins = await importServerPluginsModule();
|
|
const runtime = await createSubagentRuntime(serverPlugins);
|
|
const firstContext = createTestContext("before-restart");
|
|
const secondContext = createTestContext("after-restart");
|
|
|
|
serverPlugins.setFallbackGatewayContext(firstContext);
|
|
await runtime.run({ sessionKey: "s-2", message: "before restart" });
|
|
expect(getLastDispatchedContext()).toBe(firstContext);
|
|
|
|
serverPlugins.setFallbackGatewayContext(secondContext);
|
|
await runtime.run({ sessionKey: "s-2", message: "after restart" });
|
|
expect(getLastDispatchedContext()).toBe(secondContext);
|
|
});
|
|
|
|
test("reflects fallback context object mutation at dispatch time", async () => {
|
|
const serverPlugins = await importServerPluginsModule();
|
|
const runtime = await createSubagentRuntime(serverPlugins);
|
|
const context = { marker: "before-mutation" } as GatewayRequestContext & {
|
|
marker: string;
|
|
};
|
|
|
|
serverPlugins.setFallbackGatewayContext(context);
|
|
context.marker = "after-mutation";
|
|
|
|
await runtime.run({ sessionKey: "s-3", message: "mutated context" });
|
|
const dispatched = getLastDispatchedContext() as
|
|
| (GatewayRequestContext & { marker: string })
|
|
| undefined;
|
|
expect(dispatched?.marker).toBe("after-mutation");
|
|
});
|
|
});
|