From d3e2b36e3d4a2f0aae695c23acc956212562d87e Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Fri, 30 Jan 2026 10:59:54 +0900 Subject: [PATCH] refactor(tmux-subagent): introduce dependency injection for testability (#1267) Co-authored-by: justsisyphus --- src/features/tmux-subagent/manager.test.ts | 52 +++++++++++++--------- src/features/tmux-subagent/manager.ts | 24 +++++++--- 2 files changed, 49 insertions(+), 27 deletions(-) diff --git a/src/features/tmux-subagent/manager.test.ts b/src/features/tmux-subagent/manager.test.ts index 10ef9fa7b..2d4d797cf 100644 --- a/src/features/tmux-subagent/manager.test.ts +++ b/src/features/tmux-subagent/manager.test.ts @@ -2,6 +2,7 @@ import { describe, test, expect, mock, beforeEach } from 'bun:test' import type { TmuxConfig } from '../../config/schema' import type { WindowState, PaneAction } from './types' import type { ActionResult, ExecuteContext } from './action-executor' +import type { TmuxUtilDeps } from './manager' type ExecuteActionsResult = { success: boolean @@ -33,6 +34,11 @@ const mockExecuteAction = mock<( const mockIsInsideTmux = mock<() => boolean>(() => true) const mockGetCurrentPaneId = mock<() => string | undefined>(() => '%0') +const mockTmuxDeps: TmuxUtilDeps = { + isInsideTmux: mockIsInsideTmux, + getCurrentPaneId: mockGetCurrentPaneId, +} + mock.module('./pane-state-querier', () => ({ queryWindowState: mockQueryWindowState, paneExists: mockPaneExists, @@ -51,15 +57,19 @@ mock.module('./action-executor', () => ({ executeAction: mockExecuteAction, })) -mock.module('../../shared/tmux', () => ({ - isInsideTmux: mockIsInsideTmux, - getCurrentPaneId: mockGetCurrentPaneId, - POLL_INTERVAL_BACKGROUND_MS: 2000, - SESSION_TIMEOUT_MS: 600000, - SESSION_MISSING_GRACE_MS: 6000, - SESSION_READY_POLL_INTERVAL_MS: 100, - SESSION_READY_TIMEOUT_MS: 500, -})) +mock.module('../../shared/tmux', () => { + const { isInsideTmux, getCurrentPaneId } = require('../../shared/tmux/tmux-utils') + const { POLL_INTERVAL_BACKGROUND_MS, SESSION_TIMEOUT_MS, SESSION_MISSING_GRACE_MS } = require('../../shared/tmux/constants') + return { + isInsideTmux, + getCurrentPaneId, + POLL_INTERVAL_BACKGROUND_MS, + SESSION_TIMEOUT_MS, + SESSION_MISSING_GRACE_MS, + SESSION_READY_POLL_INTERVAL_MS: 100, + SESSION_READY_TIMEOUT_MS: 500, + } +}) const trackedSessions = new Set() @@ -148,7 +158,7 @@ describe('TmuxSessionManager', () => { } //#when - const manager = new TmuxSessionManager(ctx, config) + const manager = new TmuxSessionManager(ctx, config, mockTmuxDeps) //#then expect(manager).toBeDefined() @@ -168,7 +178,7 @@ describe('TmuxSessionManager', () => { } //#when - const manager = new TmuxSessionManager(ctx, config) + const manager = new TmuxSessionManager(ctx, config, mockTmuxDeps) //#then expect(manager).toBeDefined() @@ -188,7 +198,7 @@ describe('TmuxSessionManager', () => { } //#when - const manager = new TmuxSessionManager(ctx, config) + const manager = new TmuxSessionManager(ctx, config, mockTmuxDeps) //#then expect(manager).toBeDefined() @@ -210,7 +220,7 @@ describe('TmuxSessionManager', () => { main_pane_min_width: 80, agent_pane_min_width: 40, } - const manager = new TmuxSessionManager(ctx, config) + const manager = new TmuxSessionManager(ctx, config, mockTmuxDeps) const event = createSessionCreatedEvent( 'ses_child', 'ses_parent', @@ -271,7 +281,7 @@ describe('TmuxSessionManager', () => { main_pane_min_width: 80, agent_pane_min_width: 40, } - const manager = new TmuxSessionManager(ctx, config) + const manager = new TmuxSessionManager(ctx, config, mockTmuxDeps) //#when - first agent await manager.onSessionCreated( @@ -305,7 +315,7 @@ describe('TmuxSessionManager', () => { main_pane_min_width: 80, agent_pane_min_width: 40, } - const manager = new TmuxSessionManager(ctx, config) + const manager = new TmuxSessionManager(ctx, config, mockTmuxDeps) const event = createSessionCreatedEvent('ses_root', undefined, 'Root Session') //#when @@ -327,7 +337,7 @@ describe('TmuxSessionManager', () => { main_pane_min_width: 80, agent_pane_min_width: 40, } - const manager = new TmuxSessionManager(ctx, config) + const manager = new TmuxSessionManager(ctx, config, mockTmuxDeps) const event = createSessionCreatedEvent( 'ses_child', 'ses_parent', @@ -353,7 +363,7 @@ describe('TmuxSessionManager', () => { main_pane_min_width: 80, agent_pane_min_width: 40, } - const manager = new TmuxSessionManager(ctx, config) + const manager = new TmuxSessionManager(ctx, config, mockTmuxDeps) const event = { type: 'session.deleted', properties: { @@ -398,7 +408,7 @@ describe('TmuxSessionManager', () => { main_pane_min_width: 120, agent_pane_min_width: 40, } - const manager = new TmuxSessionManager(ctx, config) + const manager = new TmuxSessionManager(ctx, config, mockTmuxDeps) //#when await manager.onSessionCreated( @@ -450,7 +460,7 @@ describe('TmuxSessionManager', () => { main_pane_min_width: 80, agent_pane_min_width: 40, } - const manager = new TmuxSessionManager(ctx, config) + const manager = new TmuxSessionManager(ctx, config, mockTmuxDeps) await manager.onSessionCreated( createSessionCreatedEvent( @@ -487,7 +497,7 @@ describe('TmuxSessionManager', () => { main_pane_min_width: 80, agent_pane_min_width: 40, } - const manager = new TmuxSessionManager(ctx, config) + const manager = new TmuxSessionManager(ctx, config, mockTmuxDeps) //#when await manager.onSessionDeleted({ sessionID: 'ses_unknown' }) @@ -521,7 +531,7 @@ describe('TmuxSessionManager', () => { main_pane_min_width: 80, agent_pane_min_width: 40, } - const manager = new TmuxSessionManager(ctx, config) + const manager = new TmuxSessionManager(ctx, config, mockTmuxDeps) await manager.onSessionCreated( createSessionCreatedEvent('ses_1', 'ses_parent', 'Task 1') diff --git a/src/features/tmux-subagent/manager.ts b/src/features/tmux-subagent/manager.ts index 4bad83d16..94099608a 100644 --- a/src/features/tmux-subagent/manager.ts +++ b/src/features/tmux-subagent/manager.ts @@ -2,8 +2,8 @@ import type { PluginInput } from "@opencode-ai/plugin" import type { TmuxConfig } from "../../config/schema" import type { TrackedSession, CapacityConfig } from "./types" import { - isInsideTmux, - getCurrentPaneId, + isInsideTmux as defaultIsInsideTmux, + getCurrentPaneId as defaultGetCurrentPaneId, POLL_INTERVAL_BACKGROUND_MS, SESSION_MISSING_GRACE_MS, SESSION_READY_POLL_INTERVAL_MS, @@ -21,6 +21,16 @@ interface SessionCreatedEvent { properties?: { info?: { id?: string; parentID?: string; title?: string } } } +export interface TmuxUtilDeps { + isInsideTmux: () => boolean + getCurrentPaneId: () => string | undefined +} + +const defaultTmuxDeps: TmuxUtilDeps = { + isInsideTmux: defaultIsInsideTmux, + getCurrentPaneId: defaultGetCurrentPaneId, +} + const SESSION_TIMEOUT_MS = 10 * 60 * 1000 /** @@ -43,13 +53,15 @@ export class TmuxSessionManager { private sessions = new Map() private pendingSessions = new Set() private pollInterval?: ReturnType + private deps: TmuxUtilDeps - constructor(ctx: PluginInput, tmuxConfig: TmuxConfig) { + constructor(ctx: PluginInput, tmuxConfig: TmuxConfig, deps: TmuxUtilDeps = defaultTmuxDeps) { this.client = ctx.client this.tmuxConfig = tmuxConfig + this.deps = deps const defaultPort = process.env.OPENCODE_PORT ?? "4096" this.serverUrl = ctx.serverUrl?.toString() ?? `http://localhost:${defaultPort}` - this.sourcePaneId = getCurrentPaneId() + this.sourcePaneId = deps.getCurrentPaneId() log("[tmux-session-manager] initialized", { configEnabled: this.tmuxConfig.enabled, @@ -60,7 +72,7 @@ export class TmuxSessionManager { } private isEnabled(): boolean { - return this.tmuxConfig.enabled && isInsideTmux() + return this.tmuxConfig.enabled && this.deps.isInsideTmux() } private getCapacityConfig(): CapacityConfig { @@ -113,7 +125,7 @@ export class TmuxSessionManager { log("[tmux-session-manager] onSessionCreated called", { enabled, tmuxConfigEnabled: this.tmuxConfig.enabled, - isInsideTmux: isInsideTmux(), + isInsideTmux: this.deps.isInsideTmux(), eventType: event.type, infoId: event.properties?.info?.id, infoParentID: event.properties?.info?.parentID,