fix: follow up cmux timeout and interactive_bash runtime regressions
This commit is contained in:
@@ -1,4 +1,7 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { chmodSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"
|
||||
import { tmpdir } from "node:os"
|
||||
import { join } from "node:path"
|
||||
import {
|
||||
createCmuxNotificationAdapter,
|
||||
type CmuxNotifyCommandResult,
|
||||
@@ -139,4 +142,48 @@ describe("cmux notification adapter", () => {
|
||||
expect(secondDelivered).toBe(false)
|
||||
expect(callCount).toBe(1)
|
||||
})
|
||||
|
||||
test("returns promptly on timeout when cmux process ignores TERM", async () => {
|
||||
if (process.platform === "win32") {
|
||||
return
|
||||
}
|
||||
|
||||
const tempDirectory = mkdtempSync(join(tmpdir(), "cmux-notify-timeout-"))
|
||||
const fakeCmuxPath = join(tempDirectory, "cmux")
|
||||
const slowCmuxScript = `#!/bin/sh
|
||||
if [ "$1" = "notify" ]; then
|
||||
trap '' TERM
|
||||
/bin/sleep 1
|
||||
exit 0
|
||||
fi
|
||||
|
||||
exit 1
|
||||
`
|
||||
|
||||
writeFileSync(fakeCmuxPath, slowCmuxScript)
|
||||
chmodSync(fakeCmuxPath, 0o755)
|
||||
|
||||
const runtime = createResolvedMultiplexer()
|
||||
runtime.cmux.path = fakeCmuxPath
|
||||
|
||||
try {
|
||||
const adapter = createCmuxNotificationAdapter({
|
||||
runtime,
|
||||
environment: {
|
||||
PATH: tempDirectory,
|
||||
},
|
||||
timeoutMs: 40,
|
||||
})
|
||||
|
||||
const startedAt = Date.now()
|
||||
const delivered = await adapter.send("OpenCode", "Task complete")
|
||||
const elapsedMs = Date.now() - startedAt
|
||||
|
||||
expect(delivered).toBe(false)
|
||||
expect(adapter.hasDowngraded()).toBe(true)
|
||||
expect(elapsedMs).toBeLessThan(500)
|
||||
} finally {
|
||||
rmSync(tempDirectory, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -83,7 +83,20 @@ async function runCmuxNotifyCommand(input: {
|
||||
clearTimeout(timeoutHandle)
|
||||
}
|
||||
|
||||
const exitCode = timedOut ? null : await proc.exited.catch(() => null)
|
||||
if (timedOut) {
|
||||
// Do not await stdout/stderr after timeout: a process that ignores TERM
|
||||
// may keep pipes open and block fallback completion.
|
||||
void proc.exited.catch(() => {})
|
||||
|
||||
return {
|
||||
exitCode: null,
|
||||
stdout: "",
|
||||
stderr: "",
|
||||
timedOut: true,
|
||||
}
|
||||
}
|
||||
|
||||
const exitCode = await proc.exited.catch(() => null)
|
||||
const stdout = await new Response(proc.stdout).text().catch(() => "")
|
||||
const stderr = await new Response(proc.stderr).text().catch(() => "")
|
||||
|
||||
|
||||
@@ -83,6 +83,10 @@ describe("tmux-path-resolver probe environment", () => {
|
||||
})
|
||||
|
||||
test("probeCmuxNotificationCapability returns promptly after probe timeout", async () => {
|
||||
if (process.platform === "win32") {
|
||||
return
|
||||
}
|
||||
|
||||
const tempDirectory = mkdtempSync(join(tmpdir(), "tmux-path-resolver-timeout-"))
|
||||
const fakeCmuxPath = join(tempDirectory, "cmux")
|
||||
const slowCmuxScript = `#!/bin/sh
|
||||
|
||||
66
src/tools/interactive-bash/tools.test.ts
Normal file
66
src/tools/interactive-bash/tools.test.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { afterEach, describe, expect, test } from "bun:test"
|
||||
import {
|
||||
resetResolvedMultiplexerRuntimeForTesting,
|
||||
setResolvedMultiplexerRuntime,
|
||||
type ResolvedMultiplexer,
|
||||
} from "../../shared/tmux"
|
||||
import { createInteractiveBashTool, interactive_bash } from "./tools"
|
||||
|
||||
const mockToolContext = {
|
||||
sessionID: "test-session",
|
||||
messageID: "msg-1",
|
||||
agent: "test-agent",
|
||||
abort: new AbortController().signal,
|
||||
}
|
||||
|
||||
function createTmuxEnabledRuntime(): ResolvedMultiplexer {
|
||||
return {
|
||||
platform: process.platform,
|
||||
mode: "tmux-only",
|
||||
paneBackend: "tmux",
|
||||
notificationBackend: "desktop",
|
||||
tmux: {
|
||||
path: "/usr/bin/tmux",
|
||||
reachable: true,
|
||||
insideEnvironment: true,
|
||||
paneId: "%1",
|
||||
explicitDisable: false,
|
||||
},
|
||||
cmux: {
|
||||
path: null,
|
||||
reachable: false,
|
||||
notifyCapable: false,
|
||||
socketPath: undefined,
|
||||
endpointType: "missing",
|
||||
workspaceId: undefined,
|
||||
surfaceId: undefined,
|
||||
hintStrength: "none",
|
||||
explicitDisable: false,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
describe("interactive_bash runtime resolution", () => {
|
||||
afterEach(() => {
|
||||
resetResolvedMultiplexerRuntimeForTesting()
|
||||
})
|
||||
|
||||
test("createInteractiveBashTool without runtime resolves current runtime on execute", async () => {
|
||||
resetResolvedMultiplexerRuntimeForTesting()
|
||||
const tool = createInteractiveBashTool()
|
||||
setResolvedMultiplexerRuntime(createTmuxEnabledRuntime())
|
||||
|
||||
const result = await tool.execute({ tmux_command: "capture-pane -p" }, mockToolContext)
|
||||
|
||||
expect(result).not.toContain("pane control is unavailable")
|
||||
})
|
||||
|
||||
test("interactive_bash singleton resolves current runtime on execute", async () => {
|
||||
resetResolvedMultiplexerRuntimeForTesting()
|
||||
setResolvedMultiplexerRuntime(createTmuxEnabledRuntime())
|
||||
|
||||
const result = await interactive_bash.execute({ tmux_command: "capture-pane -p" }, mockToolContext)
|
||||
|
||||
expect(result).not.toContain("pane control is unavailable")
|
||||
})
|
||||
})
|
||||
@@ -54,9 +54,7 @@ export function tokenizeCommand(cmd: string): string[] {
|
||||
}
|
||||
|
||||
export function createInteractiveBashTool(
|
||||
runtime: ResolvedMultiplexer =
|
||||
getResolvedMultiplexerRuntime()
|
||||
?? createDisabledMultiplexerRuntime(),
|
||||
runtime?: ResolvedMultiplexer,
|
||||
): ToolDefinition {
|
||||
return tool({
|
||||
description: INTERACTIVE_BASH_DESCRIPTION,
|
||||
@@ -65,8 +63,13 @@ export function createInteractiveBashTool(
|
||||
},
|
||||
execute: async (args) => {
|
||||
try {
|
||||
if (runtime.paneBackend !== "tmux") {
|
||||
return `Error: interactive_bash is TMUX-only and pane control is unavailable in '${runtime.mode}' runtime.`
|
||||
const resolvedRuntime =
|
||||
runtime
|
||||
?? getResolvedMultiplexerRuntime()
|
||||
?? createDisabledMultiplexerRuntime()
|
||||
|
||||
if (resolvedRuntime.paneBackend !== "tmux") {
|
||||
return `Error: interactive_bash is TMUX-only and pane control is unavailable in '${resolvedRuntime.mode}' runtime.`
|
||||
}
|
||||
|
||||
const tmuxPath = await getTmuxPath()
|
||||
|
||||
Reference in New Issue
Block a user