refactor(tmux-subagent): introduce dependency injection for testability (#1267)

Co-authored-by: justsisyphus <justsisyphus@users.noreply.github.com>
This commit is contained in:
YeonGyu-Kim
2026-01-30 10:59:54 +09:00
committed by GitHub
parent 5f0b6d49f5
commit d3e2b36e3d
2 changed files with 49 additions and 27 deletions

View File

@@ -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<string>()
@@ -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')

View File

@@ -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<string, TrackedSession>()
private pendingSessions = new Set<string>()
private pollInterval?: ReturnType<typeof setInterval>
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,