test: run suite without split runner
This commit is contained in:
@@ -1,19 +1,7 @@
|
||||
/// <reference types="bun-types" />
|
||||
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import type { ToolContext } from "@opencode-ai/plugin/tool"
|
||||
import { interactive_bash } from "./tools"
|
||||
|
||||
const mockContext = {
|
||||
sessionID: "test-session",
|
||||
messageID: "test-message",
|
||||
agent: "test-agent",
|
||||
directory: "/project",
|
||||
worktree: "/project",
|
||||
abort: new AbortController().signal,
|
||||
metadata: () => {},
|
||||
ask: async () => {},
|
||||
} satisfies ToolContext
|
||||
import { executeInteractiveBash } from "./tools"
|
||||
|
||||
describe("interactive_bash", () => {
|
||||
test("#given kill-server command #when executed #then returns a strong prohibition without running tmux", async () => {
|
||||
@@ -21,7 +9,7 @@ describe("interactive_bash", () => {
|
||||
const args = { tmux_command: "kill-server" }
|
||||
|
||||
// when
|
||||
const output = await interactive_bash.execute(args, mockContext)
|
||||
const output = await executeInteractiveBash(args)
|
||||
|
||||
// then
|
||||
expect(output).toContain("Error: 'kill-server' is prohibited in interactive_bash.")
|
||||
@@ -34,7 +22,7 @@ describe("interactive_bash", () => {
|
||||
const args = { tmux_command: "-L omo-socket kill-server" }
|
||||
|
||||
// when
|
||||
const output = await interactive_bash.execute(args, mockContext)
|
||||
const output = await executeInteractiveBash(args)
|
||||
|
||||
// then
|
||||
expect(output).toContain("Error: 'kill-server' is prohibited in interactive_bash.")
|
||||
|
||||
@@ -144,74 +144,80 @@ tmux kill-session -t <session-name>
|
||||
If you created an omo-* session, kill only that exact session. Do not retry kill-server with Bash or any other tool.`
|
||||
}
|
||||
|
||||
type InteractiveBashArgs = {
|
||||
tmux_command: string
|
||||
}
|
||||
|
||||
export async function executeInteractiveBash(args: InteractiveBashArgs): Promise<string> {
|
||||
try {
|
||||
const tmuxPath = getCachedTmuxPath() ?? "tmux"
|
||||
|
||||
const parts = tokenizeCommand(args.tmux_command)
|
||||
|
||||
if (parts.length === 0) {
|
||||
return "Error: Empty tmux command"
|
||||
}
|
||||
|
||||
const subcommandIndex = findSubcommandIndex(parts)
|
||||
const rawSubcommand = subcommandIndex === -1 ? "" : parts[subcommandIndex]
|
||||
const subcommand = rawSubcommand.toLowerCase()
|
||||
|
||||
if (PROHIBITED_TMUX_SUBCOMMANDS.includes(subcommand)) {
|
||||
return buildProhibitedTmuxCommandMessage(rawSubcommand)
|
||||
}
|
||||
|
||||
if (BLOCKED_TMUX_SUBCOMMANDS.includes(subcommand)) {
|
||||
return buildBlockedTmuxCommandMessage(rawSubcommand, parts)
|
||||
}
|
||||
|
||||
const proc = spawnWithWindowsHide([...resolveTmuxExecutable(tmuxPath), ...parts], {
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
})
|
||||
|
||||
const timeoutPromise = new Promise<never>((_, reject) => {
|
||||
const id = setTimeout(() => {
|
||||
const timeoutError = new Error(`Timeout after ${DEFAULT_TIMEOUT_MS}ms`)
|
||||
try {
|
||||
proc.kill()
|
||||
// Fire-and-forget: wait for process exit in background to avoid zombies
|
||||
void proc.exited.catch(() => {})
|
||||
} catch {
|
||||
// Ignore kill errors; we'll still reject with timeoutError below
|
||||
}
|
||||
reject(timeoutError)
|
||||
}, DEFAULT_TIMEOUT_MS)
|
||||
proc.exited
|
||||
.then(() => clearTimeout(id))
|
||||
.catch(() => clearTimeout(id))
|
||||
})
|
||||
|
||||
// Read stdout and stderr in parallel to avoid race conditions
|
||||
const [stdout, stderr, exitCode] = await Promise.race([
|
||||
Promise.all([
|
||||
new Response(proc.stdout).text(),
|
||||
new Response(proc.stderr).text(),
|
||||
proc.exited,
|
||||
]),
|
||||
timeoutPromise,
|
||||
])
|
||||
|
||||
// Check exitCode properly - return error even if stderr is empty
|
||||
if (exitCode !== 0) {
|
||||
const errorMsg = stderr.trim() || `Command failed with exit code ${exitCode}`
|
||||
return `Error: ${errorMsg}`
|
||||
}
|
||||
|
||||
return stdout || "(no output)"
|
||||
} catch (e) {
|
||||
return `Error: ${e instanceof Error ? e.message : String(e)}`
|
||||
}
|
||||
}
|
||||
|
||||
export const interactive_bash: ToolDefinition = tool({
|
||||
description: INTERACTIVE_BASH_DESCRIPTION,
|
||||
args: {
|
||||
tmux_command: tool.schema.string().describe("The tmux command to execute (without 'tmux' prefix)"),
|
||||
},
|
||||
execute: async (args) => {
|
||||
try {
|
||||
const tmuxPath = getCachedTmuxPath() ?? "tmux"
|
||||
|
||||
const parts = tokenizeCommand(args.tmux_command)
|
||||
|
||||
if (parts.length === 0) {
|
||||
return "Error: Empty tmux command"
|
||||
}
|
||||
|
||||
const subcommandIndex = findSubcommandIndex(parts)
|
||||
const rawSubcommand = subcommandIndex === -1 ? "" : parts[subcommandIndex]
|
||||
const subcommand = rawSubcommand.toLowerCase()
|
||||
|
||||
if (PROHIBITED_TMUX_SUBCOMMANDS.includes(subcommand)) {
|
||||
return buildProhibitedTmuxCommandMessage(rawSubcommand)
|
||||
}
|
||||
|
||||
if (BLOCKED_TMUX_SUBCOMMANDS.includes(subcommand)) {
|
||||
return buildBlockedTmuxCommandMessage(rawSubcommand, parts)
|
||||
}
|
||||
|
||||
const proc = spawnWithWindowsHide([...resolveTmuxExecutable(tmuxPath), ...parts], {
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
})
|
||||
|
||||
const timeoutPromise = new Promise<never>((_, reject) => {
|
||||
const id = setTimeout(() => {
|
||||
const timeoutError = new Error(`Timeout after ${DEFAULT_TIMEOUT_MS}ms`)
|
||||
try {
|
||||
proc.kill()
|
||||
// Fire-and-forget: wait for process exit in background to avoid zombies
|
||||
void proc.exited.catch(() => {})
|
||||
} catch {
|
||||
// Ignore kill errors; we'll still reject with timeoutError below
|
||||
}
|
||||
reject(timeoutError)
|
||||
}, DEFAULT_TIMEOUT_MS)
|
||||
proc.exited
|
||||
.then(() => clearTimeout(id))
|
||||
.catch(() => clearTimeout(id))
|
||||
})
|
||||
|
||||
// Read stdout and stderr in parallel to avoid race conditions
|
||||
const [stdout, stderr, exitCode] = await Promise.race([
|
||||
Promise.all([
|
||||
new Response(proc.stdout).text(),
|
||||
new Response(proc.stderr).text(),
|
||||
proc.exited,
|
||||
]),
|
||||
timeoutPromise,
|
||||
])
|
||||
|
||||
// Check exitCode properly - return error even if stderr is empty
|
||||
if (exitCode !== 0) {
|
||||
const errorMsg = stderr.trim() || `Command failed with exit code ${exitCode}`
|
||||
return `Error: ${errorMsg}`
|
||||
}
|
||||
|
||||
return stdout || "(no output)"
|
||||
} catch (e) {
|
||||
return `Error: ${e instanceof Error ? e.message : String(e)}`
|
||||
}
|
||||
},
|
||||
execute: executeInteractiveBash,
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user