test: run suite without split runner

This commit is contained in:
YeonGyu-Kim
2026-05-15 16:26:57 +09:00
parent 150ccefa05
commit d8f52aae7f
34 changed files with 627 additions and 889 deletions
+3 -15
View File
@@ -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.")
+71 -65
View File
@@ -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,
})