mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-29 10:50:58 +00:00
* test: stabilize ci and local vitest workers * test: introduce planner-backed test runner * test: address planner review follow-ups * test: derive planner budgets from host capabilities * test: restore planner filter helper import * test: align planner explain output with execution * test: keep low profile as serial alias * test: restrict explicit planner file targets * test: clean planner exits and pnpm launch * test: tighten wrapper flag validation * ci: gate heavy fanout on check * test: key shard assignments by unit identity * ci(bun): shard vitest lanes further * test: restore ci overlap and stabilize planner tests * test: relax planner output worker assertions * test: reset plugin runtime state in optional tools suite * ci: split macos node and swift jobs * test: honor no-isolate top-level concurrency budgets * ci: fix macos swift format lint * test: cap max-profile top-level concurrency * ci: shard macos node checks * ci: use four macos node shards * test: normalize explain targets before classification
198 lines
5.4 KiB
TypeScript
198 lines
5.4 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
type MockRegistryToolEntry = {
|
|
pluginId: string;
|
|
optional: boolean;
|
|
source: string;
|
|
factory: (ctx: unknown) => unknown;
|
|
};
|
|
|
|
const loadOpenClawPluginsMock = vi.fn();
|
|
|
|
vi.mock("./loader.js", () => ({
|
|
loadOpenClawPlugins: (params: unknown) => loadOpenClawPluginsMock(params),
|
|
}));
|
|
|
|
let resolvePluginTools: typeof import("./tools.js").resolvePluginTools;
|
|
let resetPluginRuntimeStateForTest: typeof import("./runtime.js").resetPluginRuntimeStateForTest;
|
|
|
|
function makeTool(name: string) {
|
|
return {
|
|
name,
|
|
description: `${name} tool`,
|
|
parameters: { type: "object", properties: {} },
|
|
async execute() {
|
|
return { content: [{ type: "text", text: "ok" }] };
|
|
},
|
|
};
|
|
}
|
|
|
|
function createContext() {
|
|
return {
|
|
config: {
|
|
plugins: {
|
|
enabled: true,
|
|
allow: ["optional-demo", "message", "multi"],
|
|
load: { paths: ["/tmp/plugin.js"] },
|
|
},
|
|
},
|
|
workspaceDir: "/tmp",
|
|
};
|
|
}
|
|
|
|
function setRegistry(entries: MockRegistryToolEntry[]) {
|
|
const registry = {
|
|
tools: entries,
|
|
diagnostics: [] as Array<{
|
|
level: string;
|
|
pluginId: string;
|
|
source: string;
|
|
message: string;
|
|
}>,
|
|
};
|
|
loadOpenClawPluginsMock.mockReturnValue(registry);
|
|
return registry;
|
|
}
|
|
|
|
function setMultiToolRegistry() {
|
|
return setRegistry([
|
|
{
|
|
pluginId: "multi",
|
|
optional: false,
|
|
source: "/tmp/multi.js",
|
|
factory: () => [makeTool("message"), makeTool("other_tool")],
|
|
},
|
|
]);
|
|
}
|
|
|
|
function resolveWithConflictingCoreName(options?: { suppressNameConflicts?: boolean }) {
|
|
return resolvePluginTools({
|
|
context: createContext() as never,
|
|
existingToolNames: new Set(["message"]),
|
|
...(options?.suppressNameConflicts ? { suppressNameConflicts: true } : {}),
|
|
});
|
|
}
|
|
|
|
function setOptionalDemoRegistry() {
|
|
setRegistry([
|
|
{
|
|
pluginId: "optional-demo",
|
|
optional: true,
|
|
source: "/tmp/optional-demo.js",
|
|
factory: () => makeTool("optional_tool"),
|
|
},
|
|
]);
|
|
}
|
|
|
|
function resolveOptionalDemoTools(toolAllowlist?: string[]) {
|
|
return resolvePluginTools({
|
|
context: createContext() as never,
|
|
...(toolAllowlist ? { toolAllowlist } : {}),
|
|
});
|
|
}
|
|
|
|
describe("resolvePluginTools optional tools", () => {
|
|
beforeEach(async () => {
|
|
vi.resetModules();
|
|
loadOpenClawPluginsMock.mockClear();
|
|
({ resetPluginRuntimeStateForTest } = await import("./runtime.js"));
|
|
resetPluginRuntimeStateForTest();
|
|
({ resolvePluginTools } = await import("./tools.js"));
|
|
});
|
|
|
|
it("skips optional tools without explicit allowlist", () => {
|
|
setOptionalDemoRegistry();
|
|
const tools = resolveOptionalDemoTools();
|
|
|
|
expect(tools).toHaveLength(0);
|
|
});
|
|
|
|
it("allows optional tools by tool name", () => {
|
|
setOptionalDemoRegistry();
|
|
const tools = resolveOptionalDemoTools(["optional_tool"]);
|
|
|
|
expect(tools.map((tool) => tool.name)).toEqual(["optional_tool"]);
|
|
});
|
|
|
|
it("allows optional tools via plugin-scoped allowlist entries", () => {
|
|
setOptionalDemoRegistry();
|
|
const toolsByPlugin = resolveOptionalDemoTools(["optional-demo"]);
|
|
const toolsByGroup = resolveOptionalDemoTools(["group:plugins"]);
|
|
|
|
expect(toolsByPlugin.map((tool) => tool.name)).toEqual(["optional_tool"]);
|
|
expect(toolsByGroup.map((tool) => tool.name)).toEqual(["optional_tool"]);
|
|
});
|
|
|
|
it("rejects plugin id collisions with core tool names", () => {
|
|
const registry = setRegistry([
|
|
{
|
|
pluginId: "message",
|
|
optional: false,
|
|
source: "/tmp/message.js",
|
|
factory: () => makeTool("optional_tool"),
|
|
},
|
|
]);
|
|
|
|
const tools = resolvePluginTools({
|
|
context: createContext() as never,
|
|
existingToolNames: new Set(["message"]),
|
|
});
|
|
|
|
expect(tools).toHaveLength(0);
|
|
expect(registry.diagnostics).toHaveLength(1);
|
|
expect(registry.diagnostics[0]?.message).toContain("plugin id conflicts with core tool name");
|
|
});
|
|
|
|
it("skips conflicting tool names but keeps other tools", () => {
|
|
const registry = setMultiToolRegistry();
|
|
const tools = resolveWithConflictingCoreName();
|
|
|
|
expect(tools.map((tool) => tool.name)).toEqual(["other_tool"]);
|
|
expect(registry.diagnostics).toHaveLength(1);
|
|
expect(registry.diagnostics[0]?.message).toContain("plugin tool name conflict");
|
|
});
|
|
|
|
it("suppresses conflict diagnostics when requested", () => {
|
|
const registry = setMultiToolRegistry();
|
|
const tools = resolveWithConflictingCoreName({ suppressNameConflicts: true });
|
|
|
|
expect(tools.map((tool) => tool.name)).toEqual(["other_tool"]);
|
|
expect(registry.diagnostics).toHaveLength(0);
|
|
});
|
|
|
|
it("forwards an explicit env to plugin loading", () => {
|
|
setOptionalDemoRegistry();
|
|
const env = { OPENCLAW_HOME: "/srv/openclaw-home" } as NodeJS.ProcessEnv;
|
|
|
|
resolvePluginTools({
|
|
context: createContext() as never,
|
|
env,
|
|
toolAllowlist: ["optional_tool"],
|
|
});
|
|
|
|
expect(loadOpenClawPluginsMock).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
env,
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("forwards gateway subagent binding to plugin runtime options", () => {
|
|
setOptionalDemoRegistry();
|
|
|
|
resolvePluginTools({
|
|
context: createContext() as never,
|
|
allowGatewaySubagentBinding: true,
|
|
toolAllowlist: ["optional_tool"],
|
|
});
|
|
|
|
expect(loadOpenClawPluginsMock).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
runtimeOptions: {
|
|
allowGatewaySubagentBinding: true,
|
|
},
|
|
}),
|
|
);
|
|
});
|
|
});
|