fix(#585): render terminal tool output via real backend payload keys (#595)

The backend terminal tool (tools/terminal_tool.py) returns a single merged
'output' string plus 'exit_code' and 'error' — never 'stdout'/'stderr'.
ChatBubble was reading the non-existent 'stdout' key, so command output was
silently dropped and only the Duration footer showed.

- Parse 'output' as canonical; keep legacy 'stdout'/'stderr' fallback for
  backward compat.
- Route terminal output into mainOutput so it renders like other tool bubbles.
- Surface non-zero exit codes as an error line when no 'error' string is set.
- Remove dead 'stderr' render branch.
- Update ToolBubbleParsingTest to assert the real payload shape (incl. the
  exact issue-585 'echo hellp' sample) and add regression cases.

Co-authored-by: hy4ri <hy4ri@users.noreply.github.com>
This commit is contained in:
M57andhy4ri authored and GitHub committed 2026-07-14 12:01:08 +03:00
1 parent 47258bd8b0
commit 71c1bdded9
2 files changed
+88 -33

No files matched your search

@@ -11,25 +11,65 @@ class ToolBubbleParsingTest {
// ── Terminal ──────────────────────────────────────────────
@Test
fun testParseTerminal_withStdout() {
fun testParseTerminal_withOutput() {
val json =
"""{
"tool_id": "call_001",
"name": "terminal",
"args": {"command": "echo hellp"},
"result": {"output": "hellp", "exit_code": 0, "error": null},
"duration_s": 0.06
}"""
val parsed = parseToolOutput(json, "terminal", false)
assertNotNull(parsed)
assertEquals("terminal", parsed!!.toolName)
assertTrue(parsed.isTerminal)
assertEquals("hellp", parsed.stdout)
assertEquals("hellp", parsed.mainOutput)
assertEquals(0, parsed.exitCode)
assertEquals(0.06, parsed.durationSec!!, 0.01)
assertEquals("$ echo hellp", parsed.summaryText)
}
@Test
fun testParseTerminal_legacyStdout() {
val json =
"""{
"tool_id": "call_001b",
"name": "terminal",
"args": {"command": "npm run build"},
"result": {"stdout": "Build succeeded!", "exit_code": 0},
"duration_s": 5.2
}"""
val parsed = parseToolOutput(json, "terminal", false)
assertNotNull(parsed)
assertEquals("terminal", parsed!!.toolName)
assertTrue(parsed.isTerminal)
assertTrue(parsed!!.isTerminal)
assertEquals("Build succeeded!", parsed.stdout)
assertEquals("Build succeeded!", parsed.mainOutput)
assertEquals(0, parsed.exitCode)
assertEquals(5.2, parsed.durationSec!!, 0.01)
assertEquals("$ npm run build", parsed.summaryText)
}
@Test
fun testParseTerminal_floatExitCode_isNotDropped() {
// Backend emits exit_code as a FLOAT (0.0), not an int. asInt on a
// "0.0" primitive throws NumberFormatException, which previously
// nulled the whole parse -> raw JSON. Reproduces the live bug.
val json =
"""{
"tool_id": "call_001d",
"name": "terminal",
"args": {"command": "echo hi"},
"result": {"output": "hi", "exit_code": 0.0, "error": null}
}"""
val parsed = parseToolOutput(json, "terminal", false)
assertNotNull("float exit_code must not null the parse", parsed)
assertTrue(parsed!!.isTerminal)
assertEquals("hi", parsed.stdout)
assertEquals(0, parsed.exitCode)
}
@Test
fun testParseTerminal_withStderr() {
val json =
@@ -42,7 +82,7 @@ class ToolBubbleParsingTest {
val parsed = parseToolOutput(json, "terminal", false)
assertNotNull(parsed)
assertTrue(parsed!!.isTerminal)
assertEquals("Permission denied", parsed.stderr)
assertEquals("Permission denied", parsed.stdout)
assertEquals(1, parsed.exitCode)
assertEquals("$ rm -rf /", parsed.summaryText)
}