diff --git a/packages/opencode/src/tool/bash.ts b/packages/opencode/src/tool/bash.ts index b9e0f8a1c3..52aaf7e1bc 100644 --- a/packages/opencode/src/tool/bash.ts +++ b/packages/opencode/src/tool/bash.ts @@ -16,7 +16,6 @@ import { Shell } from "@/shell/shell" import { BashArity } from "@/permission/arity" -const MAX_OUTPUT_LENGTH = Flag.OPENCODE_EXPERIMENTAL_BASH_MAX_OUTPUT_LENGTH || 30_000 const DEFAULT_TIMEOUT = Flag.OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS || 2 * 60 * 1000 export const log = Log.create({ service: "bash-tool" }) @@ -172,15 +171,13 @@ export const BashTool = Tool.define("bash", async () => { }) const append = (chunk: Buffer) => { - if (output.length <= MAX_OUTPUT_LENGTH) { - output += chunk.toString() - ctx.metadata({ - metadata: { - output, - description: params.description, - }, - }) - } + output += chunk.toString() + ctx.metadata({ + metadata: { + output, + description: params.description, + }, + }) } proc.stdout?.on("data", append) @@ -228,12 +225,7 @@ export const BashTool = Tool.define("bash", async () => { }) }) - let resultMetadata: String[] = [""] - - if (output.length > MAX_OUTPUT_LENGTH) { - output = output.slice(0, MAX_OUTPUT_LENGTH) - resultMetadata.push(`bash tool truncated output as it exceeded ${MAX_OUTPUT_LENGTH} char limit`) - } + const resultMetadata: string[] = [] if (timedOut) { resultMetadata.push(`bash tool terminated command after exceeding timeout ${timeout} ms`) @@ -243,9 +235,8 @@ export const BashTool = Tool.define("bash", async () => { resultMetadata.push("User aborted the command") } - if (resultMetadata.length > 1) { - resultMetadata.push("") - output += "\n\n" + resultMetadata.join("\n") + if (resultMetadata.length > 0) { + output += "\n\n\n" + resultMetadata.join("\n") + "\n" } return { diff --git a/packages/opencode/test/tool/bash.test.ts b/packages/opencode/test/tool/bash.test.ts index 2eb17a9fc9..b58858f11d 100644 --- a/packages/opencode/test/tool/bash.test.ts +++ b/packages/opencode/test/tool/bash.test.ts @@ -4,6 +4,7 @@ import { BashTool } from "../../src/tool/bash" import { Instance } from "../../src/project/instance" import { tmpdir } from "../fixture/fixture" import type { PermissionNext } from "../../src/permission/next" +import { Truncate } from "../../src/tool/truncation" const ctx = { sessionID: "test", @@ -230,3 +231,91 @@ describe("tool.bash permissions", () => { }) }) }) + +describe("tool.bash truncation", () => { + test("truncates output exceeding line limit", async () => { + await Instance.provide({ + directory: projectRoot, + fn: async () => { + const bash = await BashTool.init() + const lineCount = Truncate.MAX_LINES + 500 + const result = await bash.execute( + { + command: `seq 1 ${lineCount}`, + description: "Generate lines exceeding limit", + }, + ctx, + ) + expect((result.metadata as any).truncated).toBe(true) + expect(result.output).toContain("truncated") + expect(result.output).toContain("Full output written to:") + }, + }) + }) + + test("truncates output exceeding byte limit", async () => { + await Instance.provide({ + directory: projectRoot, + fn: async () => { + const bash = await BashTool.init() + const byteCount = Truncate.MAX_BYTES + 10000 + const result = await bash.execute( + { + command: `head -c ${byteCount} /dev/zero | tr '\\0' 'a'`, + description: "Generate bytes exceeding limit", + }, + ctx, + ) + expect((result.metadata as any).truncated).toBe(true) + expect(result.output).toContain("truncated") + expect(result.output).toContain("Full output written to:") + }, + }) + }) + + test("does not truncate small output", async () => { + await Instance.provide({ + directory: projectRoot, + fn: async () => { + const bash = await BashTool.init() + const result = await bash.execute( + { + command: "echo hello", + description: "Echo hello", + }, + ctx, + ) + expect((result.metadata as any).truncated).toBe(false) + expect(result.output).toBe("hello\n") + }, + }) + }) + + test("full output is saved to file when truncated", async () => { + await Instance.provide({ + directory: projectRoot, + fn: async () => { + const bash = await BashTool.init() + const lineCount = Truncate.MAX_LINES + 100 + const result = await bash.execute( + { + command: `seq 1 ${lineCount}`, + description: "Generate lines for file check", + }, + ctx, + ) + expect((result.metadata as any).truncated).toBe(true) + + const match = result.output.match(/Full output written to: (.+)/) + expect(match).toBeTruthy() + + const filepath = match![1].split("\n")[0] + const saved = await Bun.file(filepath).text() + const lines = saved.trim().split("\n") + expect(lines.length).toBe(lineCount) + expect(lines[0]).toBe("1") + expect(lines[lineCount - 1]).toBe(String(lineCount)) + }, + }) + }) +})