diff --git a/src/openclaw/__tests__/tmux.test.ts b/src/openclaw/__tests__/tmux.test.ts
index 790a1bbe0..c7c856eeb 100644
--- a/src/openclaw/__tests__/tmux.test.ts
+++ b/src/openclaw/__tests__/tmux.test.ts
@@ -1,13 +1,153 @@
-import { describe, expect, test } from "bun:test"
-import { analyzePaneContent } from "../tmux"
+///
+
+import { afterAll, beforeAll, beforeEach, describe, expect, mock, test } from "bun:test"
+
+type MockTmuxCommandResult = {
+ success: boolean
+ output: string
+ stdout: string
+ stderr: string
+ exitCode: number
+}
+
+const runTmuxCommandMock = mock(
+ async (): Promise => ({
+ success: true,
+ output: "",
+ stdout: "",
+ stderr: "",
+ exitCode: 0,
+ }),
+)
+
+const getTmuxPathMock = mock(async (): Promise => "/mock/tmux")
+
+let tmuxModule: typeof import("../tmux")
+
+beforeAll(async () => {
+ mock.module("../../shared/tmux/runner", () => ({
+ runTmuxCommand: runTmuxCommandMock,
+ }))
+
+ mock.module("../../tools/interactive-bash/tmux-path-resolver", () => ({
+ getTmuxPath: getTmuxPathMock,
+ }))
+
+ tmuxModule = await import("../tmux")
+})
+
+beforeEach(() => {
+ runTmuxCommandMock.mockReset()
+ getTmuxPathMock.mockReset()
+ getTmuxPathMock.mockResolvedValue("/mock/tmux")
+})
+
+afterAll(() => {
+ mock.restore()
+})
describe("openclaw tmux helpers", () => {
test("analyzePaneContent recognizes the opencode welcome prompt", () => {
+ // given
const content = "opencode\nAsk anything...\nRun /help"
- expect(analyzePaneContent(content).confidence).toBeGreaterThanOrEqual(1)
+
+ // when
+ const result = tmuxModule.analyzePaneContent(content)
+
+ // then
+ expect(result.confidence).toBe(1)
})
test("analyzePaneContent returns zero confidence for empty content", () => {
- expect(analyzePaneContent(null).confidence).toBe(0)
+ // given
+ const content = null
+
+ // when
+ const result = tmuxModule.analyzePaneContent(content)
+
+ // then
+ expect(result.confidence).toBe(0)
+ })
+
+ test("isTmuxAvailable delegates version checks through runTmuxCommand", async () => {
+ // given
+ runTmuxCommandMock.mockResolvedValue({
+ success: true,
+ output: "tmux 3.5a",
+ stdout: "tmux 3.5a",
+ stderr: "",
+ exitCode: 0,
+ })
+
+ // when
+ const result = await tmuxModule.isTmuxAvailable()
+
+ // then
+ expect(result).toBe(true)
+ expect(getTmuxPathMock).toHaveBeenCalledTimes(1)
+ expect(runTmuxCommandMock).toHaveBeenCalledTimes(1)
+ expect(runTmuxCommandMock).toHaveBeenCalledWith("/mock/tmux", ["-V"])
+ })
+
+ test("getTmuxSessionName delegates session lookup through runTmuxCommand", async () => {
+ // given
+ runTmuxCommandMock.mockResolvedValue({
+ success: true,
+ output: "team-mode\n",
+ stdout: "team-mode\n",
+ stderr: "",
+ exitCode: 0,
+ })
+
+ // when
+ const result = await tmuxModule.getTmuxSessionName()
+
+ // then
+ expect(result).toBe("team-mode")
+ expect(runTmuxCommandMock).toHaveBeenCalledWith("/mock/tmux", ["display-message", "-p", "#S"])
+ })
+
+ test("captureTmuxPane delegates pane capture through runTmuxCommand", async () => {
+ // given
+ runTmuxCommandMock.mockResolvedValue({
+ success: true,
+ output: "pane output\n",
+ stdout: "pane output\n",
+ stderr: "",
+ exitCode: 0,
+ })
+
+ // when
+ const result = await tmuxModule.captureTmuxPane("%42", 30)
+
+ // then
+ expect(result).toBe("pane output")
+ expect(runTmuxCommandMock).toHaveBeenCalledWith("/mock/tmux", ["capture-pane", "-p", "-t", "%42", "-S", "-30"])
+ })
+
+ test("sendToPane delegates literal text and Enter through runTmuxCommand", async () => {
+ // given
+ runTmuxCommandMock.mockResolvedValue({
+ success: true,
+ output: "",
+ stdout: "",
+ stderr: "",
+ exitCode: 0,
+ })
+
+ // when
+ const result = await tmuxModule.sendToPane("%42", "hello", true)
+
+ // then
+ expect(result).toBe(true)
+ expect(runTmuxCommandMock).toHaveBeenCalledTimes(2)
+ expect(runTmuxCommandMock.mock.calls[0]).toEqual([
+ "/mock/tmux",
+ ["send-keys", "-t", "%42", "-l", "--", "hello"],
+ ])
+ expect(runTmuxCommandMock.mock.calls[1]).toEqual([
+ "/mock/tmux",
+ ["send-keys", "-t", "%42", "Enter"],
+ ])
})
})
diff --git a/src/openclaw/tmux.ts b/src/openclaw/tmux.ts
index 47b04c45a..d7dfaff49 100644
--- a/src/openclaw/tmux.ts
+++ b/src/openclaw/tmux.ts
@@ -1,4 +1,14 @@
-import { spawn } from "../shared/bun-spawn-shim"
+import { runTmuxCommand } from "../shared/tmux/runner"
+import { getTmuxPath } from "../tools/interactive-bash/tmux-path-resolver"
+
+async function runOpenClawTmuxCommand(args: string[]) {
+ const tmuxPath = await getTmuxPath()
+ if (!tmuxPath) {
+ return null
+ }
+
+ return runTmuxCommand(tmuxPath, args)
+}
export function getCurrentTmuxSession(): string | null {
const env = process.env.TMUX
@@ -9,15 +19,9 @@ export function getCurrentTmuxSession(): string | null {
export async function getTmuxSessionName(): Promise {
try {
- const proc = spawn(["tmux", "display-message", "-p", "#S"], {
- stdout: "pipe",
- stderr: "ignore",
- })
- const outputPromise = new Response(proc.stdout).text()
- await proc.exited
- const output = await outputPromise
- if (proc.exitCode !== 0) return null
- return output.trim() || null
+ const result = await runOpenClawTmuxCommand(["display-message", "-p", "#S"])
+ if (!result?.success) return null
+ return result.output.trim() || null
} catch {
return null
}
@@ -25,18 +29,9 @@ export async function getTmuxSessionName(): Promise {
export async function captureTmuxPane(paneId: string, lines = 15): Promise {
try {
- const proc = spawn(
- ["tmux", "capture-pane", "-p", "-t", paneId, "-S", `-${lines}`],
- {
- stdout: "pipe",
- stderr: "ignore",
- },
- )
- const outputPromise = new Response(proc.stdout).text()
- await proc.exited
- const output = await outputPromise
- if (proc.exitCode !== 0) return null
- return output.trim() || null
+ const result = await runOpenClawTmuxCommand(["capture-pane", "-p", "-t", paneId, "-S", `-${lines}`])
+ if (!result?.success) return null
+ return result.output.trim() || null
} catch {
return null
}
@@ -44,21 +39,13 @@ export async function captureTmuxPane(paneId: string, lines = 15): Promise {
try {
- const literalProc = spawn(["tmux", "send-keys", "-t", paneId, "-l", "--", text], {
- stdout: "ignore",
- stderr: "ignore",
- })
- await literalProc.exited
- if (literalProc.exitCode !== 0) return false
+ const literalResult = await runOpenClawTmuxCommand(["send-keys", "-t", paneId, "-l", "--", text])
+ if (!literalResult?.success) return false
if (!confirm) return true
- const enterProc = spawn(["tmux", "send-keys", "-t", paneId, "Enter"], {
- stdout: "ignore",
- stderr: "ignore",
- })
- await enterProc.exited
- return enterProc.exitCode === 0
+ const enterResult = await runOpenClawTmuxCommand(["send-keys", "-t", paneId, "Enter"])
+ return enterResult?.success ?? false
} catch {
return false
}
@@ -66,12 +53,8 @@ export async function sendToPane(paneId: string, text: string, confirm = true):
export async function isTmuxAvailable(): Promise {
try {
- const proc = spawn(["tmux", "-V"], {
- stdout: "ignore",
- stderr: "ignore",
- })
- await proc.exited
- return proc.exitCode === 0
+ const result = await runOpenClawTmuxCommand(["-V"])
+ return result?.success ?? false
} catch {
return false
}