Files
openclaw/src/plugins/tools.optional.test.ts
Tak Hoffman ab37d8810d test: introduce planner-backed test runner, stabilize local builds (#54650)
* 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
2026-03-25 18:11:58 -05:00

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,
},
}),
);
});
});