diff --git a/src/index.test.ts b/src/index.test.ts index 0082d1e2c..e9d4baee3 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it, mock } from "bun:test" +import { afterAll, beforeEach, describe, expect, it, mock } from "bun:test" describe("experimental.session.compacting handler", () => { function createCompactingHandler(hooks: { @@ -217,3 +217,181 @@ describe("look_at tool conditional registration", () => { }) }) }) + +const mockInitConfigContext = mock(() => {}) +const mockDetectExternalSkillPlugin = mock(() => ({ detected: false, pluginName: null })) +const mockGetSkillPluginConflictWarning = mock(() => "") +const mockInjectServerAuthIntoClient = mock(() => {}) +const mockLogLegacyPluginStartupWarning = mock(() => {}) +const mockLoadPluginConfig = mock(() => ({})) +const mockIsTmuxIntegrationEnabled = mock( + (pluginConfig: { tmux?: { enabled?: boolean } | undefined }) => pluginConfig.tmux?.enabled ?? false, +) +const mockIsInteractiveBashEnabled = mock(() => false) +const mockCreateRuntimeTmuxConfig = mock(() => ({ + enabled: false, + layout: "tiled" as const, + main_pane_size: 60, + main_pane_min_width: 80, + agent_pane_min_width: 40, + isolation: "inline" as const, +})) +const mockCreateManagers = mock(() => ({ + backgroundManager: { shutdown: async () => {} }, + skillMcpManager: { disconnectAll: async () => {} }, + configHandler: async () => {}, +})) +const mockCreateTools = mock(async () => ({ + mergedSkills: [], + availableSkills: [], + filteredTools: {}, +})) +const mockCreateHooks = mock(() => ({ + disposeHooks: () => {}, + compactionContextInjector: undefined, + compactionTodoPreserver: undefined, + claudeCodeHooks: undefined, +})) +const mockCreatePluginDispose = mock(() => async () => {}) +const mockCreatePluginInterface = mock(() => ({})) +const mockInitializeOpenClaw = mock(async () => {}) +const mockStartTmuxCheck = mock(() => {}) + +mock.module("./cli/config-manager/config-context", () => ({ + initConfigContext: mockInitConfigContext, +})) + +mock.module("./shared/external-plugin-detector", () => ({ + detectExternalSkillPlugin: mockDetectExternalSkillPlugin, + getSkillPluginConflictWarning: mockGetSkillPluginConflictWarning, +})) + +mock.module("./shared", () => ({ + injectServerAuthIntoClient: mockInjectServerAuthIntoClient, + log: mock(() => {}), + logLegacyPluginStartupWarning: mockLogLegacyPluginStartupWarning, +})) + +mock.module("./plugin-config", () => ({ + loadPluginConfig: mockLoadPluginConfig, +})) + +mock.module("./create-runtime-tmux-config", () => ({ + createRuntimeTmuxConfig: mockCreateRuntimeTmuxConfig, + isTmuxIntegrationEnabled: mockIsTmuxIntegrationEnabled, + isInteractiveBashEnabled: mockIsInteractiveBashEnabled, +})) + +mock.module("./create-managers", () => ({ + createManagers: mockCreateManagers, +})) + +mock.module("./create-tools", () => ({ + createTools: mockCreateTools, +})) + +mock.module("./create-hooks", () => ({ + createHooks: mockCreateHooks, +})) + +mock.module("./plugin-dispose", () => ({ + createPluginDispose: mockCreatePluginDispose, +})) + +mock.module("./plugin-interface", () => ({ + createPluginInterface: mockCreatePluginInterface, +})) + +mock.module("./plugin-state", () => ({ + createModelCacheState: mock(() => ({})), +})) + +mock.module("./shared/first-message-variant", () => ({ + createFirstMessageVariantGate: mock(() => ({ + shouldOverride: () => false, + markApplied: () => {}, + markSessionCreated: () => {}, + clear: () => {}, + })), +})) + +mock.module("./openclaw", () => ({ + initializeOpenClaw: mockInitializeOpenClaw, +})) + +mock.module("./tools/interactive-bash", () => ({ + interactive_bash: {}, + startBackgroundCheck: mockStartTmuxCheck, +})) + +mock.module("./tools/lsp/client", () => ({ + lspManager: { + cleanupTempDirectoryClients: async () => {}, + }, +})) + +const { default: OhMyOpenCodePlugin } = await import("./index") + +describe("OhMyOpenCodePlugin", () => { + beforeEach(() => { + mockInitConfigContext.mockClear() + mockDetectExternalSkillPlugin.mockClear() + mockGetSkillPluginConflictWarning.mockClear() + mockInjectServerAuthIntoClient.mockClear() + mockLogLegacyPluginStartupWarning.mockClear() + mockLoadPluginConfig.mockClear() + mockIsTmuxIntegrationEnabled.mockClear() + mockIsInteractiveBashEnabled.mockClear() + mockCreateRuntimeTmuxConfig.mockClear() + mockCreateManagers.mockClear() + mockCreateTools.mockClear() + mockCreateHooks.mockClear() + mockCreatePluginDispose.mockClear() + mockCreatePluginInterface.mockClear() + mockInitializeOpenClaw.mockClear() + mockStartTmuxCheck.mockClear() + }) + + afterAll(() => { + mock.restore() + }) + + it("starts openclaw during plugin bootstrap when openclaw config exists", async () => { + // given + const openclawConfig = { + enabled: true, + gateways: {}, + hooks: {}, + replyListener: { + discordBotToken: "discord-token", + }, + } + mockLoadPluginConfig.mockReturnValue({ + openclaw: openclawConfig, + }) + + // when + await OhMyOpenCodePlugin({ + directory: "/tmp/project", + client: {}, + } as Parameters[0]) + + // then + expect(mockInitializeOpenClaw).toHaveBeenCalledTimes(1) + expect(mockInitializeOpenClaw).toHaveBeenCalledWith(openclawConfig) + }) + + it("does not start openclaw when openclaw config is absent", async () => { + // given + mockLoadPluginConfig.mockReturnValue({}) + + // when + await OhMyOpenCodePlugin({ + directory: "/tmp/project", + client: {}, + } as Parameters[0]) + + // then + expect(mockInitializeOpenClaw).not.toHaveBeenCalled() + }) +}) diff --git a/src/index.ts b/src/index.ts index 4ad88a870..f13faf1ec 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,6 +7,7 @@ import { createHooks } from "./create-hooks" import { createManagers } from "./create-managers" import { createRuntimeTmuxConfig, isTmuxIntegrationEnabled } from "./create-runtime-tmux-config" import { createTools } from "./create-tools" +import { initializeOpenClaw } from "./openclaw" import { createPluginInterface } from "./plugin-interface" import { createPluginDispose, type PluginDispose } from "./plugin-dispose" @@ -15,8 +16,8 @@ import { createModelCacheState } from "./plugin-state" import { createFirstMessageVariantGate } from "./shared/first-message-variant" import { injectServerAuthIntoClient, log, logLegacyPluginStartupWarning } from "./shared" import { detectExternalSkillPlugin, getSkillPluginConflictWarning } from "./shared/external-plugin-detector" +import { startBackgroundCheck as startTmuxCheck } from "./tools/interactive-bash" import { lspManager } from "./tools/lsp/client" -import { startTmuxCheck } from "./tools" let activePluginDispose: PluginDispose | null = null @@ -36,6 +37,9 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => { await activePluginDispose?.() const pluginConfig = loadPluginConfig(ctx.directory, ctx) + if (pluginConfig.openclaw) { + await initializeOpenClaw(pluginConfig.openclaw) + } const tmuxIntegrationEnabled = isTmuxIntegrationEnabled(pluginConfig) if (tmuxIntegrationEnabled) { startTmuxCheck()