/// import { afterEach, beforeEach, describe, expect, it, mock, spyOn } from "bun:test" import type { PluginInput } from "@opencode-ai/plugin" import { OhMyOpenCodeConfigSchema } from "./config/schema/oh-my-opencode-config" import { createManagers } from "./create-managers" import * as openclawRuntimeDispatch from "./openclaw/runtime-dispatch" import { createModelCacheState } from "./plugin-state" type CleanupRegistration = { shutdown: () => void | Promise } type CleanupSessionTeamRunsFn = typeof import("./features/team-mode/team-runtime/session-cleanup").cleanupSessionTeamRuns const markServerRunningInProcess = mock(() => {}) let backgroundManagerOptions: { onSubagentSessionCreated?: (event: { sessionID: string; parentID: string; title: string }) => Promise } | null = null const trackedPaneBySession = new Map() const registeredCleanupManagers: CleanupRegistration[] = [] const cleanupSessionTeamRunsMock = mock(async () => ({ cleanedTeamRunIds: [], removedLayoutTeamRunIds: [], errors: [], })) class MockBackgroundManager { constructor(config: { onSubagentSessionCreated?: (event: { sessionID: string; parentID: string; title: string }) => Promise }) { backgroundManagerOptions = config } } class MockSkillMcpManager { constructor(..._args: unknown[]) {} } class MockTmuxSessionManager { constructor(_ctx: PluginInput, _config: unknown) {} async cleanup(): Promise {} async onSessionCreated(event: { properties?: { info?: { id?: string } } }): Promise { const sessionID = event.properties?.info?.id if (sessionID) { trackedPaneBySession.set(sessionID, `%pane-${sessionID}`) } } getTrackedPaneId(sessionID: string): string | undefined { return trackedPaneBySession.get(sessionID) } } function createConfigHandler(): ReturnType { return async () => {} } function initTaskToastManager(): ReturnType { return {} as ReturnType } function registerManagerForCleanup(manager: CleanupRegistration): void { registeredCleanupManagers.push(manager) } function createDeps(): NonNullable[0]["deps"]> { return { BackgroundManagerClass: MockBackgroundManager as typeof import("./features/background-agent").BackgroundManager, SkillMcpManagerClass: MockSkillMcpManager as typeof import("./features/skill-mcp-manager").SkillMcpManager, TmuxSessionManagerClass: MockTmuxSessionManager as typeof import("./features/tmux-subagent").TmuxSessionManager, initTaskToastManagerFn: initTaskToastManager, registerManagerForCleanupFn: registerManagerForCleanup, cleanupSessionTeamRunsFn: cleanupSessionTeamRunsMock as CleanupSessionTeamRunsFn, createConfigHandlerFn: createConfigHandler, markServerRunningInProcessFn: markServerRunningInProcess, } } function createTmuxConfig(enabled: boolean) { return { enabled, layout: "main-vertical" as const, main_pane_size: 60, main_pane_min_width: 120, agent_pane_min_width: 40, isolation: "inline" as const, } } function createContext(directory: string): PluginInput { const shell = Object.assign( () => { throw new Error("shell should not be called in this test") }, { braces: () => [], escape: (input: string) => input, env() { return shell }, cwd() { return shell }, nothrow() { return shell }, throws() { return shell }, }, ) return { project: { id: "project-id", worktree: directory, time: { created: Date.now() }, }, directory, worktree: directory, serverUrl: new URL("http://localhost:4096"), $: shell, client: {} as PluginInput["client"], } } describe("createManagers", () => { let dispatchOpenClawEvent: ReturnType beforeEach(() => { dispatchOpenClawEvent = spyOn(openclawRuntimeDispatch, "dispatchOpenClawEvent") markServerRunningInProcess.mockClear() dispatchOpenClawEvent.mockReset() backgroundManagerOptions = null trackedPaneBySession.clear() registeredCleanupManagers.length = 0 cleanupSessionTeamRunsMock.mockClear() }) afterEach(() => { dispatchOpenClawEvent.mockRestore() }) it("#given tmux integration is disabled #when managers are created #then it does not mark the tmux server as running", () => { const args = { ctx: createContext("/tmp"), pluginConfig: OhMyOpenCodeConfigSchema.parse({}), tmuxConfig: createTmuxConfig(false), modelCacheState: createModelCacheState(), backgroundNotificationHookEnabled: false, deps: createDeps(), } createManagers(args) expect(markServerRunningInProcess).not.toHaveBeenCalled() }) it("#given tmux integration is enabled #when managers are created #then it marks the tmux server as running", () => { const args = { ctx: createContext("/tmp"), pluginConfig: OhMyOpenCodeConfigSchema.parse({}), tmuxConfig: createTmuxConfig(true), modelCacheState: createModelCacheState(), backgroundNotificationHookEnabled: false, deps: createDeps(), } createManagers(args) expect(markServerRunningInProcess).toHaveBeenCalledTimes(1) }) it("#given tmux is enabled but ctx.serverUrl is undefined #when managers are created #then it does NOT mark the server as running (issue #3894)", () => { // Vanilla `opencode` (no `opencode serve` / `opencode web`) leaves // ctx.serverUrl undefined. Marking the server as in-process running // would short-circuit isServerRunning() in createTeamLayout, letting // it spawn tmux panes whose `opencode attach` then fails because no // server is actually listening on the fallback port. const ctx = createContext("/tmp") const ctxWithoutServerUrl = { ...ctx, serverUrl: undefined as unknown as URL } const args = { ctx: ctxWithoutServerUrl, pluginConfig: OhMyOpenCodeConfigSchema.parse({}), tmuxConfig: createTmuxConfig(true), modelCacheState: createModelCacheState(), backgroundNotificationHookEnabled: false, deps: createDeps(), } createManagers(args) expect(markServerRunningInProcess).not.toHaveBeenCalled() }) it("#given openclaw is enabled #when the background session-created callback runs #then it dispatches openclaw with the tracked pane id", async () => { const args = { ctx: createContext("/tmp/project"), pluginConfig: OhMyOpenCodeConfigSchema.parse({ openclaw: { enabled: true, gateways: {}, hooks: {}, }, }), tmuxConfig: createTmuxConfig(true), modelCacheState: createModelCacheState(), backgroundNotificationHookEnabled: false, deps: createDeps(), } createManagers(args) await backgroundManagerOptions?.onSubagentSessionCreated?.({ sessionID: "ses-bg-1", parentID: "ses-parent", title: "child task", }) expect(dispatchOpenClawEvent).toHaveBeenCalledTimes(1) expect(dispatchOpenClawEvent).toHaveBeenCalledWith({ config: args.pluginConfig.openclaw, rawEvent: "session.created", context: { sessionId: "ses-bg-1", projectPath: "/tmp/project", tmuxPaneId: "%pane-ses-bg-1", }, }) }) it("#given team mode is enabled #when process cleanup runs #then session team runs are cleaned with tmux visualization dependencies", async () => { const args = { ctx: createContext("/tmp/project"), pluginConfig: OhMyOpenCodeConfigSchema.parse({ team_mode: { enabled: true, tmux_visualization: true, }, }), tmuxConfig: createTmuxConfig(true), modelCacheState: createModelCacheState(), backgroundNotificationHookEnabled: false, deps: createDeps(), } createManagers(args) await registeredCleanupManagers[0]?.shutdown() expect(cleanupSessionTeamRunsMock).toHaveBeenCalledTimes(1) const cleanupArgs = cleanupSessionTeamRunsMock.mock.calls[0]?.[0] expect(cleanupArgs).toMatchObject({ config: args.pluginConfig.team_mode, }) expect(cleanupArgs?.tmuxMgr).toBeInstanceOf(MockTmuxSessionManager) expect(cleanupArgs?.bgMgr).toBeInstanceOf(MockBackgroundManager) }) })