mirror of
https://github.com/moltbot/moltbot.git
synced 2026-04-20 13:13:06 +00:00
test: keep openclaw tools registration policy pure
This commit is contained in:
@@ -1,57 +1,29 @@
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { createOpenClawTools } from "./openclaw-tools.js";
|
||||
import {
|
||||
fastOpenClawToolFactoryMocks,
|
||||
stubOpenClawCoreTool,
|
||||
} from "./test-helpers/fast-openclaw-tools-shell.js";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { collectPresentOpenClawTools } from "./openclaw-tools.registration.js";
|
||||
import { textResult, type AnyAgentTool } from "./tools/common.js";
|
||||
|
||||
function asConfig(value: unknown): OpenClawConfig {
|
||||
return value as OpenClawConfig;
|
||||
function stubAgentTool(name: string): AnyAgentTool {
|
||||
return {
|
||||
label: name,
|
||||
name,
|
||||
description: `${name} stub`,
|
||||
parameters: { type: "object", properties: {} },
|
||||
async execute() {
|
||||
return textResult("ok", {});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe("openclaw tools image generation registration", () => {
|
||||
afterEach(() => {
|
||||
fastOpenClawToolFactoryMocks.createImageGenerateTool.mockReset();
|
||||
fastOpenClawToolFactoryMocks.createVideoGenerateTool.mockReset();
|
||||
it("registers image_generate when an image-generation tool is present", () => {
|
||||
const imageGenerateTool = stubAgentTool("image_generate");
|
||||
|
||||
expect(collectPresentOpenClawTools([imageGenerateTool])).toEqual([imageGenerateTool]);
|
||||
});
|
||||
|
||||
it("registers image_generate when the image tool factory returns a tool", () => {
|
||||
const imageGenerateTool = stubOpenClawCoreTool("image_generate");
|
||||
fastOpenClawToolFactoryMocks.createImageGenerateTool.mockReturnValue(imageGenerateTool);
|
||||
|
||||
const config = asConfig({
|
||||
agents: {
|
||||
defaults: {
|
||||
imageGenerationModel: {
|
||||
primary: "openai/gpt-image-1",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const tools = createOpenClawTools({
|
||||
config,
|
||||
agentDir: "/tmp/openclaw-agent-main",
|
||||
});
|
||||
|
||||
expect(tools).toContain(imageGenerateTool);
|
||||
expect(fastOpenClawToolFactoryMocks.createImageGenerateTool).toHaveBeenCalledWith({
|
||||
config,
|
||||
agentDir: "/tmp/openclaw-agent-main",
|
||||
workspaceDir: expect.any(String),
|
||||
sandbox: undefined,
|
||||
fsPolicy: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it("omits image_generate when the image tool factory returns null", () => {
|
||||
fastOpenClawToolFactoryMocks.createImageGenerateTool.mockReturnValue(null);
|
||||
|
||||
const tools = createOpenClawTools({
|
||||
config: asConfig({}),
|
||||
agentDir: "/tmp/openclaw-agent-main",
|
||||
});
|
||||
|
||||
expect(tools.map((tool) => tool.name)).not.toContain("image_generate");
|
||||
it("omits image_generate when the image-generation tool is absent", () => {
|
||||
expect(collectPresentOpenClawTools([null]).map((tool) => tool.name)).not.toContain(
|
||||
"image_generate",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,22 +1,18 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import "./test-helpers/fast-openclaw-tools-shell.js";
|
||||
import { createOpenClawTools } from "./openclaw-tools.js";
|
||||
|
||||
function readToolByName() {
|
||||
return new Map(createOpenClawTools().map((tool) => [tool.name, tool]));
|
||||
}
|
||||
import {
|
||||
isOpenClawOwnerOnlyCoreToolName,
|
||||
OPENCLAW_OWNER_ONLY_CORE_TOOL_NAMES,
|
||||
} from "./tools/owner-only-tools.js";
|
||||
|
||||
describe("createOpenClawTools owner authorization", () => {
|
||||
it("marks owner-only core tools in raw registration", () => {
|
||||
const tools = readToolByName();
|
||||
expect(tools.get("cron")?.ownerOnly).toBe(true);
|
||||
expect(tools.get("gateway")?.ownerOnly).toBe(true);
|
||||
expect(tools.get("nodes")?.ownerOnly).toBe(true);
|
||||
it("marks owner-only core tool names", () => {
|
||||
expect(OPENCLAW_OWNER_ONLY_CORE_TOOL_NAMES).toEqual(["cron", "gateway", "nodes"]);
|
||||
expect(isOpenClawOwnerOnlyCoreToolName("cron")).toBe(true);
|
||||
expect(isOpenClawOwnerOnlyCoreToolName("gateway")).toBe(true);
|
||||
expect(isOpenClawOwnerOnlyCoreToolName("nodes")).toBe(true);
|
||||
});
|
||||
|
||||
it("keeps canvas non-owner-only in raw registration", () => {
|
||||
const tools = readToolByName();
|
||||
expect(tools.get("canvas")).toBeDefined();
|
||||
expect(tools.get("canvas")?.ownerOnly).not.toBe(true);
|
||||
it("keeps canvas non-owner-only", () => {
|
||||
expect(isOpenClawOwnerOnlyCoreToolName("canvas")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
7
src/agents/openclaw-tools.registration.ts
Normal file
7
src/agents/openclaw-tools.registration.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import type { AnyAgentTool } from "./tools/common.js";
|
||||
|
||||
export function collectPresentOpenClawTools(
|
||||
candidates: readonly (AnyAgentTool | null | undefined)[],
|
||||
): AnyAgentTool[] {
|
||||
return candidates.filter((tool): tool is AnyAgentTool => tool !== null && tool !== undefined);
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import { normalizeDeliveryContext } from "../utils/delivery-context.js";
|
||||
import type { GatewayMessageChannel } from "../utils/message-channel.js";
|
||||
import { resolveAgentWorkspaceDir, resolveSessionAgentId } from "./agent-scope.js";
|
||||
import { resolveOpenClawPluginToolsForOptions } from "./openclaw-plugin-tools.js";
|
||||
import { collectPresentOpenClawTools } from "./openclaw-tools.registration.js";
|
||||
import type { SandboxFsBridge } from "./sandbox/fs-bridge.js";
|
||||
import type { SpawnedToolContext } from "./spawned-context.js";
|
||||
import type { ToolFsPolicy } from "./tool-fs-policy.js";
|
||||
@@ -235,9 +236,7 @@ export function createOpenClawTools(
|
||||
agentChannel: options?.agentChannel,
|
||||
config: options?.config,
|
||||
}),
|
||||
...(imageGenerateTool ? [imageGenerateTool] : []),
|
||||
...(musicGenerateTool ? [musicGenerateTool] : []),
|
||||
...(videoGenerateTool ? [videoGenerateTool] : []),
|
||||
...collectPresentOpenClawTools([imageGenerateTool, musicGenerateTool, videoGenerateTool]),
|
||||
createGatewayTool({
|
||||
agentSessionKey: options?.agentSessionKey,
|
||||
config: options?.config,
|
||||
@@ -293,10 +292,7 @@ export function createOpenClawTools(
|
||||
config: resolvedConfig,
|
||||
sandboxed: options?.sandboxed,
|
||||
}),
|
||||
...(webSearchTool ? [webSearchTool] : []),
|
||||
...(webFetchTool ? [webFetchTool] : []),
|
||||
...(imageTool ? [imageTool] : []),
|
||||
...(pdfTool ? [pdfTool] : []),
|
||||
...collectPresentOpenClawTools([webSearchTool, webFetchTool, imageTool, pdfTool]),
|
||||
];
|
||||
|
||||
if (options?.disablePluginTools) {
|
||||
|
||||
@@ -1,60 +1,29 @@
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { createOpenClawTools } from "./openclaw-tools.js";
|
||||
import {
|
||||
fastOpenClawToolFactoryMocks,
|
||||
stubOpenClawCoreTool,
|
||||
} from "./test-helpers/fast-openclaw-tools-shell.js";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { collectPresentOpenClawTools } from "./openclaw-tools.registration.js";
|
||||
import { textResult, type AnyAgentTool } from "./tools/common.js";
|
||||
|
||||
function asConfig(value: unknown): OpenClawConfig {
|
||||
return value as OpenClawConfig;
|
||||
function stubAgentTool(name: string): AnyAgentTool {
|
||||
return {
|
||||
label: name,
|
||||
name,
|
||||
description: `${name} stub`,
|
||||
parameters: { type: "object", properties: {} },
|
||||
async execute() {
|
||||
return textResult("ok", {});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe("openclaw tools video generation registration", () => {
|
||||
afterEach(() => {
|
||||
fastOpenClawToolFactoryMocks.createImageGenerateTool.mockReset();
|
||||
fastOpenClawToolFactoryMocks.createVideoGenerateTool.mockReset();
|
||||
it("registers video_generate when a video-generation tool is present", () => {
|
||||
const videoGenerateTool = stubAgentTool("video_generate");
|
||||
|
||||
expect(collectPresentOpenClawTools([videoGenerateTool])).toEqual([videoGenerateTool]);
|
||||
});
|
||||
|
||||
it("registers video_generate when the video tool factory returns a tool", () => {
|
||||
const videoGenerateTool = stubOpenClawCoreTool("video_generate");
|
||||
fastOpenClawToolFactoryMocks.createVideoGenerateTool.mockReturnValue(videoGenerateTool);
|
||||
|
||||
const config = asConfig({
|
||||
agents: {
|
||||
defaults: {
|
||||
videoGenerationModel: {
|
||||
primary: "qwen/wan2.6-t2v",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const tools = createOpenClawTools({
|
||||
config,
|
||||
agentDir: "/tmp/openclaw-agent-main",
|
||||
agentSessionKey: "agent:main:main",
|
||||
});
|
||||
|
||||
expect(tools).toContain(videoGenerateTool);
|
||||
expect(fastOpenClawToolFactoryMocks.createVideoGenerateTool).toHaveBeenCalledWith({
|
||||
config,
|
||||
agentDir: "/tmp/openclaw-agent-main",
|
||||
agentSessionKey: "agent:main:main",
|
||||
requesterOrigin: undefined,
|
||||
workspaceDir: expect.any(String),
|
||||
sandbox: undefined,
|
||||
fsPolicy: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it("omits video_generate when the video tool factory returns null", () => {
|
||||
fastOpenClawToolFactoryMocks.createVideoGenerateTool.mockReturnValue(null);
|
||||
|
||||
const tools = createOpenClawTools({
|
||||
config: asConfig({}),
|
||||
agentDir: "/tmp/openclaw-agent-main",
|
||||
});
|
||||
|
||||
expect(tools.map((tool) => tool.name)).not.toContain("video_generate");
|
||||
it("omits video_generate when the video-generation tool is absent", () => {
|
||||
expect(collectPresentOpenClawTools([null]).map((tool) => tool.name)).not.toContain(
|
||||
"video_generate",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,115 +0,0 @@
|
||||
import { vi } from "vitest";
|
||||
|
||||
export type StubbedCoreTool = {
|
||||
name: string;
|
||||
description: string;
|
||||
parameters: { type: "object"; properties: Record<string, unknown> };
|
||||
ownerOnly?: boolean;
|
||||
execute: (...args: unknown[]) => unknown;
|
||||
};
|
||||
|
||||
export function stubOpenClawCoreTool(name: string, options?: { ownerOnly?: boolean }) {
|
||||
return {
|
||||
name,
|
||||
description: `${name} stub`,
|
||||
parameters: { type: "object", properties: {} },
|
||||
execute: vi.fn() as unknown as (...args: unknown[]) => unknown,
|
||||
...(options?.ownerOnly ? { ownerOnly: true } : {}),
|
||||
} satisfies StubbedCoreTool;
|
||||
}
|
||||
|
||||
export const fastOpenClawToolFactoryMocks = {
|
||||
createImageGenerateTool: vi.fn<(...args: unknown[]) => StubbedCoreTool | null>(() => null),
|
||||
createVideoGenerateTool: vi.fn<(...args: unknown[]) => StubbedCoreTool | null>(() => null),
|
||||
};
|
||||
|
||||
vi.mock("../tools/agents-list-tool.js", () => ({
|
||||
createAgentsListTool: () => stubOpenClawCoreTool("agents_list"),
|
||||
}));
|
||||
|
||||
vi.mock("../tools/canvas-tool.js", () => ({
|
||||
createCanvasTool: () => stubOpenClawCoreTool("canvas"),
|
||||
}));
|
||||
|
||||
vi.mock("../tools/cron-tool.js", () => ({
|
||||
createCronTool: () => stubOpenClawCoreTool("cron", { ownerOnly: true }),
|
||||
}));
|
||||
|
||||
vi.mock("../tools/gateway-tool.js", () => ({
|
||||
createGatewayTool: () => stubOpenClawCoreTool("gateway", { ownerOnly: true }),
|
||||
}));
|
||||
|
||||
vi.mock("../tools/image-generate-tool.js", () => ({
|
||||
createImageGenerateTool: (...args: unknown[]) =>
|
||||
fastOpenClawToolFactoryMocks.createImageGenerateTool(...args),
|
||||
}));
|
||||
|
||||
vi.mock("../tools/image-tool.js", () => ({
|
||||
createImageTool: () => null,
|
||||
}));
|
||||
|
||||
vi.mock("../tools/message-tool.js", () => ({
|
||||
createMessageTool: () => stubOpenClawCoreTool("message"),
|
||||
}));
|
||||
|
||||
vi.mock("../tools/music-generate-tool.js", () => ({
|
||||
createMusicGenerateTool: () => null,
|
||||
}));
|
||||
|
||||
vi.mock("../tools/nodes-tool.js", () => ({
|
||||
createNodesTool: () => stubOpenClawCoreTool("nodes", { ownerOnly: true }),
|
||||
}));
|
||||
|
||||
vi.mock("../tools/pdf-tool.js", () => ({
|
||||
createPdfTool: () => null,
|
||||
}));
|
||||
|
||||
vi.mock("../tools/session-status-tool.js", () => ({
|
||||
createSessionStatusTool: () => stubOpenClawCoreTool("session_status"),
|
||||
}));
|
||||
|
||||
vi.mock("../tools/sessions-history-tool.js", () => ({
|
||||
createSessionsHistoryTool: () => stubOpenClawCoreTool("sessions_history"),
|
||||
}));
|
||||
|
||||
vi.mock("../tools/sessions-list-tool.js", () => ({
|
||||
createSessionsListTool: () => stubOpenClawCoreTool("sessions_list"),
|
||||
}));
|
||||
|
||||
vi.mock("../tools/sessions-send-tool.js", () => ({
|
||||
createSessionsSendTool: () => stubOpenClawCoreTool("sessions_send"),
|
||||
}));
|
||||
|
||||
vi.mock("../tools/sessions-spawn-tool.js", () => ({
|
||||
createSessionsSpawnTool: () => stubOpenClawCoreTool("sessions_spawn"),
|
||||
}));
|
||||
|
||||
vi.mock("../tools/sessions-yield-tool.js", () => ({
|
||||
createSessionsYieldTool: () => stubOpenClawCoreTool("sessions_yield"),
|
||||
}));
|
||||
|
||||
vi.mock("../tools/subagents-tool.js", () => ({
|
||||
createSubagentsTool: () => stubOpenClawCoreTool("subagents"),
|
||||
}));
|
||||
|
||||
vi.mock("../tools/tts-tool.js", () => ({
|
||||
createTtsTool: () => stubOpenClawCoreTool("tts"),
|
||||
}));
|
||||
|
||||
vi.mock("../tools/update-plan-tool.js", () => ({
|
||||
createUpdatePlanTool: () => stubOpenClawCoreTool("update_plan"),
|
||||
}));
|
||||
|
||||
vi.mock("../tools/video-generate-tool.js", () => ({
|
||||
createVideoGenerateTool: (...args: unknown[]) =>
|
||||
fastOpenClawToolFactoryMocks.createVideoGenerateTool(...args),
|
||||
}));
|
||||
|
||||
vi.mock("../tools/web-tools.js", () => ({
|
||||
createWebSearchTool: () => null,
|
||||
createWebFetchTool: () => null,
|
||||
}));
|
||||
|
||||
vi.mock("../openclaw-plugin-tools.js", () => ({
|
||||
resolveOpenClawPluginToolsForOptions: () => [],
|
||||
}));
|
||||
@@ -15,6 +15,7 @@ import { optionalStringEnum, stringEnum } from "../schema/typebox.js";
|
||||
import { CRON_TOOL_DISPLAY_SUMMARY } from "../tool-description-presets.js";
|
||||
import { type AnyAgentTool, jsonResult, readStringParam } from "./common.js";
|
||||
import { callGatewayTool, readGatewayCallOptions, type GatewayCallOptions } from "./gateway.js";
|
||||
import { isOpenClawOwnerOnlyCoreToolName } from "./owner-only-tools.js";
|
||||
import { resolveInternalSessionKey, resolveMainSessionAlias } from "./sessions-helpers.js";
|
||||
|
||||
// We spell out job/patch properties so that LLMs know what fields to send.
|
||||
@@ -387,7 +388,7 @@ export function createCronTool(opts?: CronToolOptions, deps?: CronToolDeps): Any
|
||||
return {
|
||||
label: "Cron",
|
||||
name: "cron",
|
||||
ownerOnly: true,
|
||||
ownerOnly: isOpenClawOwnerOnlyCoreToolName("cron"),
|
||||
displaySummary: CRON_TOOL_DISPLAY_SUMMARY,
|
||||
description: `Manage Gateway cron jobs (status/list/add/update/remove/run/runs) and send wake events. Use this for reminders, "check back later" requests, delayed follow-ups, and recurring tasks. Do not emulate scheduling with exec sleep or process polling.
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ import { normalizeOptionalString, readStringValue } from "../../shared/string-co
|
||||
import { stringEnum } from "../schema/typebox.js";
|
||||
import { type AnyAgentTool, jsonResult, readStringParam } from "./common.js";
|
||||
import { callGatewayTool, readGatewayCallOptions } from "./gateway.js";
|
||||
import { isOpenClawOwnerOnlyCoreToolName } from "./owner-only-tools.js";
|
||||
|
||||
const log = createSubsystemLogger("gateway-tool");
|
||||
|
||||
@@ -163,7 +164,7 @@ export function createGatewayTool(opts?: {
|
||||
return {
|
||||
label: "Gateway",
|
||||
name: "gateway",
|
||||
ownerOnly: true,
|
||||
ownerOnly: isOpenClawOwnerOnlyCoreToolName("gateway"),
|
||||
description:
|
||||
"Restart, inspect a specific config schema path, apply config, or update the gateway in-place (SIGUSR1). Use config.schema.lookup with a targeted dot path before config edits. Use config.patch for safe partial config updates (merges with existing). Use config.apply only when replacing entire config. Config writes hot-reload when possible and restart when required. Always pass a human-readable completion message via the `note` parameter so the system can deliver it to the user after restart.",
|
||||
parameters: GatewayToolSchema,
|
||||
|
||||
@@ -14,6 +14,7 @@ import { callGatewayTool, readGatewayCallOptions } from "./gateway.js";
|
||||
import { executeNodeCommandAction, type NodeCommandAction } from "./nodes-tool-commands.js";
|
||||
import { executeNodeMediaAction, MEDIA_INVOKE_ACTIONS } from "./nodes-tool-media.js";
|
||||
import { resolveNodeId } from "./nodes-utils.js";
|
||||
import { isOpenClawOwnerOnlyCoreToolName } from "./owner-only-tools.js";
|
||||
|
||||
const NODES_TOOL_ACTIONS = [
|
||||
"status",
|
||||
@@ -149,7 +150,7 @@ export function createNodesTool(options?: {
|
||||
return {
|
||||
label: "Nodes",
|
||||
name: "nodes",
|
||||
ownerOnly: true,
|
||||
ownerOnly: isOpenClawOwnerOnlyCoreToolName("nodes"),
|
||||
description:
|
||||
"Discover and control paired nodes (status/describe/pairing/notify/camera/photos/screen/location/notifications/invoke).",
|
||||
parameters: NodesToolSchema,
|
||||
|
||||
9
src/agents/tools/owner-only-tools.ts
Normal file
9
src/agents/tools/owner-only-tools.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export const OPENCLAW_OWNER_ONLY_CORE_TOOL_NAMES = ["cron", "gateway", "nodes"] as const;
|
||||
|
||||
const OPENCLAW_OWNER_ONLY_CORE_TOOL_NAME_SET: ReadonlySet<string> = new Set(
|
||||
OPENCLAW_OWNER_ONLY_CORE_TOOL_NAMES,
|
||||
);
|
||||
|
||||
export function isOpenClawOwnerOnlyCoreToolName(toolName: string): boolean {
|
||||
return OPENCLAW_OWNER_ONLY_CORE_TOOL_NAME_SET.has(toolName);
|
||||
}
|
||||
Reference in New Issue
Block a user