From 47e0fbad49adfbb76ed3684480d62d303db38945 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Tue, 28 Apr 2026 10:47:33 +0900 Subject: [PATCH] feat(plugin): register team-mode tools and expand registry tests --- src/plugin/tool-registry.team-mode.test.ts | 113 +++++++++++++++++++++ src/plugin/tool-registry.test.ts | 103 ++++++++++++++++++- src/plugin/tool-registry.ts | 78 +++++++++++++- 3 files changed, 288 insertions(+), 6 deletions(-) create mode 100644 src/plugin/tool-registry.team-mode.test.ts diff --git a/src/plugin/tool-registry.team-mode.test.ts b/src/plugin/tool-registry.team-mode.test.ts new file mode 100644 index 000000000..d858dee45 --- /dev/null +++ b/src/plugin/tool-registry.team-mode.test.ts @@ -0,0 +1,113 @@ +/// + +import { describe, expect, mock, test } from "bun:test" + +import { tool } from "@opencode-ai/plugin" + +import { OhMyOpenCodeConfigSchema } from "../config" +import type { OpencodeClient } from "../tools/delegate-task/types" +import { createToolRegistry } from "./tool-registry" + +const fakeTool = tool({ + description: "test tool", + args: {}, + async execute(): Promise { + return "ok" + }, +}) + +function createPluginConfig() { + return OhMyOpenCodeConfigSchema.parse({ + git_master: { + commit_footer: false, + include_co_authored_by: false, + git_env_prefix: "", + }, + team_mode: { + enabled: true, + }, + }) +} + +describe("team-mode tool registry wiring", () => { + test("passes ctx.client into every team tool factory", () => { + // given + const client = {} as OpencodeClient + const createTeamCreateTool = mock(() => fakeTool) + const createTeamDeleteTool = mock(() => fakeTool) + const createTeamShutdownRequestTool = mock(() => fakeTool) + const createTeamApproveShutdownTool = mock(() => fakeTool) + const createTeamRejectShutdownTool = mock(() => fakeTool) + const createTeamSendMessageTool = mock(() => fakeTool) + const createTeamTaskCreateTool = mock(() => fakeTool) + const createTeamTaskListTool = mock(() => fakeTool) + const createTeamTaskUpdateTool = mock(() => fakeTool) + const createTeamTaskGetTool = mock(() => fakeTool) + const createTeamStatusTool = mock(() => fakeTool) + const createTeamListTool = mock(() => fakeTool) + + // when + createToolRegistry({ + ctx: { directory: "/tmp/team-mode", client } as Parameters[0]["ctx"], + pluginConfig: createPluginConfig(), + managers: { + backgroundManager: {}, + tmuxSessionManager: {}, + skillMcpManager: {}, + } as Parameters[0]["managers"], + skillContext: { + mergedSkills: [], + availableSkills: [], + browserProvider: "playwright", + disabledSkills: new Set(), + }, + availableCategories: [], + toolFactories: { + builtinTools: { bash: fakeTool, read: fakeTool }, + createBackgroundTools: mock(() => ({})), + createCallOmoAgent: mock(() => fakeTool), + createLookAt: mock(() => fakeTool), + createSkillMcpTool: mock(() => fakeTool), + createSkillTool: mock(() => fakeTool), + createGrepTools: mock(() => ({})), + createGlobTools: mock(() => ({})), + createAstGrepTools: mock(() => ({})), + createSessionManagerTools: mock(() => ({})), + createDelegateTask: mock(() => fakeTool), + discoverCommandsSync: mock(() => []), + interactive_bash: fakeTool, + createTaskCreateTool: mock(() => fakeTool), + createTaskGetTool: mock(() => fakeTool), + createTaskList: mock(() => fakeTool), + createTaskUpdateTool: mock(() => fakeTool), + createHashlineEditTool: mock(() => fakeTool), + createTeamCreateTool, + createTeamDeleteTool, + createTeamShutdownRequestTool, + createTeamApproveShutdownTool, + createTeamRejectShutdownTool, + createTeamSendMessageTool, + createTeamTaskCreateTool, + createTeamTaskListTool, + createTeamTaskUpdateTool, + createTeamTaskGetTool, + createTeamStatusTool, + createTeamListTool, + }, + }) + + // then + expect(createTeamCreateTool).toHaveBeenCalledWith(expect.anything(), client, expect.anything(), expect.anything(), expect.anything()) + expect(createTeamDeleteTool).toHaveBeenCalledWith(expect.anything(), client, expect.anything(), expect.anything()) + expect(createTeamShutdownRequestTool).toHaveBeenCalledWith(expect.anything(), client) + expect(createTeamApproveShutdownTool).toHaveBeenCalledWith(expect.anything(), client) + expect(createTeamRejectShutdownTool).toHaveBeenCalledWith(expect.anything(), client) + expect(createTeamSendMessageTool).toHaveBeenCalledWith(expect.anything(), client) + expect(createTeamTaskCreateTool).toHaveBeenCalledWith(expect.anything(), client) + expect(createTeamTaskListTool).toHaveBeenCalledWith(expect.anything(), client) + expect(createTeamTaskUpdateTool).toHaveBeenCalledWith(expect.anything(), client) + expect(createTeamTaskGetTool).toHaveBeenCalledWith(expect.anything(), client) + expect(createTeamStatusTool).toHaveBeenCalledWith(expect.anything(), client, expect.anything()) + expect(createTeamListTool).toHaveBeenCalledWith(expect.anything(), client) + }) +}) diff --git a/src/plugin/tool-registry.test.ts b/src/plugin/tool-registry.test.ts index 5c0a42bfb..7fc2f1723 100644 --- a/src/plugin/tool-registry.test.ts +++ b/src/plugin/tool-registry.test.ts @@ -1,7 +1,7 @@ -import { beforeEach, describe, expect, mock, spyOn, test } from "bun:test" +const { beforeEach, describe, expect, mock, spyOn, test } = require("bun:test") import { tool } from "@opencode-ai/plugin" -import type { OhMyOpenCodeConfig } from "../config" +import { OhMyOpenCodeConfigSchema, type OhMyOpenCodeConfig } from "../config" import * as openclawRuntimeDispatch from "../openclaw/runtime-dispatch" import type { ToolsRecord } from "./types" @@ -28,6 +28,21 @@ const syncSessionCreatedCallbacks: Array< const trackedPaneBySession = new Map() let dispatchOpenClawEvent: ReturnType +const TEAM_TOOL_NAMES = [ + "team_create", + "team_delete", + "team_shutdown_request", + "team_approve_shutdown", + "team_reject_shutdown", + "team_send_message", + "team_task_create", + "team_task_list", + "team_task_update", + "team_task_get", + "team_status", + "team_list", +] as const + const { createToolRegistry, trimToolsToCap } = await import("./tool-registry") const toolFactories: NonNullable[0]["toolFactories"]> = { @@ -52,17 +67,33 @@ const toolFactories: NonNullable[0]["toolF createTaskList: mock(() => fakeTool), createTaskUpdateTool: mock(() => fakeTool), createHashlineEditTool: mock(() => fakeTool), + createTeamApproveShutdownTool: mock(() => fakeTool), + createTeamCreateTool: mock(() => fakeTool), + createTeamDeleteTool: mock(() => fakeTool), + createTeamRejectShutdownTool: mock(() => fakeTool), + createTeamShutdownRequestTool: mock(() => fakeTool), + createTeamSendMessageTool: mock(() => fakeTool), + createTeamTaskCreateTool: mock(() => fakeTool), + createTeamTaskGetTool: mock(() => fakeTool), + createTeamTaskListTool: mock(() => fakeTool), + createTeamTaskUpdateTool: mock(() => fakeTool), + createTeamStatusTool: mock(() => fakeTool), + createTeamListTool: mock(() => fakeTool), } -function createPluginConfig(overrides: Partial = {}): OhMyOpenCodeConfig { - return { +type PluginConfigOverrides = Omit, "team_mode"> & { + team_mode?: Partial> +} + +function createPluginConfig(overrides: PluginConfigOverrides = {}): OhMyOpenCodeConfig { + return OhMyOpenCodeConfigSchema.parse({ git_master: { commit_footer: false, include_co_authored_by: false, git_env_prefix: "", }, ...overrides, - } + }) } beforeEach(() => { @@ -146,6 +177,68 @@ describe("#given task_system configuration", () => { }) }) +describe("#given team_mode configuration", () => { + test("#when team_mode is enabled #then all 12 team tools are registered", () => { + syncSessionCreatedCallbacks.length = 0 + + const result = createToolRegistry({ + ctx: { directory: "/tmp" } as Parameters[0]["ctx"], + pluginConfig: createPluginConfig({ + team_mode: { + enabled: true, + }, + }), + managers: { + backgroundManager: {}, + tmuxSessionManager: {}, + skillMcpManager: {}, + } as Parameters[0]["managers"], + skillContext: { + mergedSkills: [], + availableSkills: [], + browserProvider: "playwright", + disabledSkills: new Set(), + }, + availableCategories: [], + toolFactories, + }) + + for (const teamToolName of TEAM_TOOL_NAMES) { + expect(result.filteredTools).toHaveProperty(teamToolName) + } + }) + + test("#when team_mode is disabled #then zero team tools are registered", () => { + syncSessionCreatedCallbacks.length = 0 + + const result = createToolRegistry({ + ctx: { directory: "/tmp" } as Parameters[0]["ctx"], + pluginConfig: createPluginConfig({ + team_mode: { + enabled: false, + }, + }), + managers: { + backgroundManager: {}, + tmuxSessionManager: {}, + skillMcpManager: {}, + } as Parameters[0]["managers"], + skillContext: { + mergedSkills: [], + availableSkills: [], + browserProvider: "playwright", + disabledSkills: new Set(), + }, + availableCategories: [], + toolFactories, + }) + + const registeredTeamToolNames = Object.keys(result.filteredTools).filter((toolName) => toolName.startsWith("team_")) + + expect(registeredTeamToolNames).toHaveLength(0) + }) +}) + describe("#given tmux integration is disabled", () => { test("#when system tmux is available #then interactive_bash remains registered", () => { syncSessionCreatedCallbacks.length = 0 diff --git a/src/plugin/tool-registry.ts b/src/plugin/tool-registry.ts index a3e46185a..582c12be5 100644 --- a/src/plugin/tool-registry.ts +++ b/src/plugin/tool-registry.ts @@ -6,6 +6,21 @@ import type { } from "../agents/dynamic-agent-prompt-builder" import type { OhMyOpenCodeConfig } from "../config" import { isInteractiveBashEnabled } from "../create-runtime-tmux-config" +import { + createTeamApproveShutdownTool, + createTeamCreateTool, + createTeamDeleteTool, + createTeamRejectShutdownTool, + createTeamShutdownRequestTool, +} from "../features/team-mode/tools/lifecycle" +import { createTeamSendMessageTool } from "../features/team-mode/tools/messaging" +import { createTeamListTool, createTeamStatusTool } from "../features/team-mode/tools/query" +import { + createTeamTaskCreateTool, + createTeamTaskGetTool, + createTeamTaskListTool, + createTeamTaskUpdateTool, +} from "../features/team-mode/tools/tasks" import * as openclawRuntimeDispatch from "../openclaw/runtime-dispatch" import type { PluginContext, ToolsRecord } from "./types" @@ -56,6 +71,18 @@ type ToolRegistryFactories = { createTaskList: typeof createTaskList createTaskUpdateTool: typeof createTaskUpdateTool createHashlineEditTool: typeof createHashlineEditTool + createTeamApproveShutdownTool: typeof createTeamApproveShutdownTool + createTeamCreateTool: typeof createTeamCreateTool + createTeamDeleteTool: typeof createTeamDeleteTool + createTeamRejectShutdownTool: typeof createTeamRejectShutdownTool + createTeamShutdownRequestTool: typeof createTeamShutdownRequestTool + createTeamSendMessageTool: typeof createTeamSendMessageTool + createTeamTaskCreateTool: typeof createTeamTaskCreateTool + createTeamTaskGetTool: typeof createTeamTaskGetTool + createTeamTaskListTool: typeof createTeamTaskListTool + createTeamTaskUpdateTool: typeof createTeamTaskUpdateTool + createTeamStatusTool: typeof createTeamStatusTool + createTeamListTool: typeof createTeamListTool } const defaultToolRegistryFactories: ToolRegistryFactories = { @@ -77,6 +104,18 @@ const defaultToolRegistryFactories: ToolRegistryFactories = { createTaskList, createTaskUpdateTool, createHashlineEditTool, + createTeamApproveShutdownTool, + createTeamCreateTool, + createTeamDeleteTool, + createTeamRejectShutdownTool, + createTeamShutdownRequestTool, + createTeamSendMessageTool, + createTeamTaskCreateTool, + createTeamTaskGetTool, + createTeamTaskListTool, + createTeamTaskUpdateTool, + createTeamStatusTool, + createTeamListTool, } export type ToolRegistryResult = { @@ -178,6 +217,8 @@ export function createToolRegistry(args: { ) const lookAt = isMultimodalLookerEnabled ? factories.createLookAt(ctx) : null + const getSisyphusJuniorModelOverride = (agentOverride?: { model?: string }): string | undefined => agentOverride?.model + const delegateTask = factories.createDelegateTask({ manager: managers.backgroundManager, client: ctx.client, @@ -185,9 +226,10 @@ export function createToolRegistry(args: { userCategories: pluginConfig.categories, agentOverrides: pluginConfig.agents, gitMasterConfig: pluginConfig.git_master, - sisyphusJuniorModel: pluginConfig.agents?.["sisyphus-junior"]?.model, + sisyphusJuniorModel: getSisyphusJuniorModelOverride(pluginConfig.agents?.["sisyphus-junior"]), browserProvider: skillContext.browserProvider, disabledSkills: skillContext.disabledSkills, + teamModeEnabled: pluginConfig.team_mode?.enabled ?? false, availableCategories, availableSkills: skillContext.availableSkills, sisyphusAgentConfig: pluginConfig.sisyphus_agent, @@ -243,6 +285,7 @@ export function createToolRegistry(args: { getSessionID: getSessionIDForMcp, gitMasterConfig: pluginConfig.git_master, browserProvider: skillContext.browserProvider, + teamModeEnabled: pluginConfig.team_mode?.enabled ?? false, nativeSkills: "skills" in ctx ? (ctx as { skills: SkillLoadOptions["nativeSkills"] }).skills : undefined, }) @@ -261,6 +304,38 @@ export function createToolRegistry(args: { ? { edit: factories.createHashlineEditTool(ctx) } : {} + const teamModeToolsRecord: Record = pluginConfig.team_mode?.enabled + ? { + team_create: factories.createTeamCreateTool( + pluginConfig.team_mode, + ctx.client, + managers.backgroundManager, + managers.tmuxSessionManager, + { + userCategories: pluginConfig.categories, + sisyphusJuniorModel: getSisyphusJuniorModelOverride(pluginConfig.agents?.["sisyphus-junior"]), + agentOverrides: pluginConfig.agents, + }, + ), + team_delete: factories.createTeamDeleteTool( + pluginConfig.team_mode, + ctx.client, + managers.backgroundManager, + managers.tmuxSessionManager, + ), + team_shutdown_request: factories.createTeamShutdownRequestTool(pluginConfig.team_mode, ctx.client), + team_approve_shutdown: factories.createTeamApproveShutdownTool(pluginConfig.team_mode, ctx.client), + team_reject_shutdown: factories.createTeamRejectShutdownTool(pluginConfig.team_mode, ctx.client), + team_send_message: factories.createTeamSendMessageTool(pluginConfig.team_mode, ctx.client), + team_task_create: factories.createTeamTaskCreateTool(pluginConfig.team_mode, ctx.client), + team_task_list: factories.createTeamTaskListTool(pluginConfig.team_mode, ctx.client), + team_task_update: factories.createTeamTaskUpdateTool(pluginConfig.team_mode, ctx.client), + team_task_get: factories.createTeamTaskGetTool(pluginConfig.team_mode, ctx.client), + team_status: factories.createTeamStatusTool(pluginConfig.team_mode, ctx.client, managers.backgroundManager), + team_list: factories.createTeamListTool(pluginConfig.team_mode, ctx.client), + } + : {} + const allTools: Record = { ...factories.builtinTools, ...factories.createGrepTools(ctx), @@ -274,6 +349,7 @@ export function createToolRegistry(args: { skill_mcp: skillMcpTool, skill: skillTool, ...(interactiveBashEnabled ? { interactive_bash: factories.interactive_bash } : {}), + ...teamModeToolsRecord, ...taskToolsRecord, ...hashlineToolsRecord, }