feat(chat): rewire tool bubbles onto the desktop engine (stage 3) (#765)
* feat(chat): rewire tool bubbles onto the desktop engine (stage 3)
parseToolOutput is now a thin adapter: it normalizes the payload shape
(args/result sub-objects, legacy top-level fallback, name resolution)
and delegates all per-tool formatting to ToolViewBuilder.
- ToolResultParser.kt: 1325 lines of bespoke formatters → 45-line adapter
- ParsedToolData deleted; ToolBubble renders ToolView directly:
collapsed = emoji + engine title (+countLabel) + subtitle line;
expanded = error → $ command / stdout / stderr / exit code →
DiffViewCard → search hits → detail → duration footer
- Gson-compat shims, formatTodoToolOutput, toIntOrNull and all
per-tool formatters died with the old parser (engine is idiomatic kotlinx)
- Kept mobile-only UI: progress preview, SecurityRiskChip, emoji
prefixes, raw-JSON toggle, DiffViewCard, copy
- ToolViewBuilder fixes found during the rewire: numberValue now accepts
numeric strings (TS Number() semantics — gateway ships exit_code as
string/float), memory title keeps its target suffix
ToolBubbleParsingTest rewritten as adapter-contract tests (14 cases);
per-tool behavior lives in ToolViewBuilderTest. Verified: ktlint +
full testDebugUnitTest.
* fix(chat): dedupe tool name in collapsed bubble summary
The header row already shows the tool name, but the collapsed summary
showed emoji + engine title — and for ~11 tools the engine title is just
the generic humanized name (fact_store -> "Fact Store", todo -> "Todo"),
so the bubble displayed the tool identity twice.
composeToolSummaryLines now drops the title when it equals the generic
humanized name and merges the subtitle into the emoji line instead
("🧠 Fact added (ID: 5)"), restoring the pre-engine one-line style.
Descriptive titles ("Searched \"cats\"", "Ran ls", "Read a.kt L10-14")
stay on line one with the subtitle on line two. A lone emoji with no
subtitle renders nothing.
ToolViewBuilder.genericTitleFor exposes the generic title for the
comparison. 9 new unit tests.
---------
Co-authored-by: hy4ri <hy4ri@users.noreply.github.com>
This commit is contained in:
1 parent
100a82dd4f
commit
982f8aec4e
6 files changed
+507
-1766
No files matched your search
@@ -1,351 +1,166 @@
|
||||
package com.m57.hermescontrol.ui.chat
|
||||
|
||||
import com.m57.hermescontrol.ui.chat.components.DiffLineType
|
||||
import com.m57.hermescontrol.ui.chat.components.parseDiffText
|
||||
import com.m57.hermescontrol.ui.chat.tool.ToolViewStatus
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertNotNull
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
/**
|
||||
* Adapter-contract tests for [parseToolOutput] — the thin wrapper over the
|
||||
* desktop-ported engine. Shape normalization lives here; per-tool content
|
||||
* behavior is covered by ToolViewBuilderTest in ui.chat.tool.
|
||||
*/
|
||||
class ToolBubbleParsingTest {
|
||||
// ── Terminal ──────────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
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)
|
||||
fun `parses new-format payload with args and result`() {
|
||||
val view =
|
||||
parseToolOutput(
|
||||
"""{"args":{"command":"ls"},"result":{"output":"wiring.tsx","exit_code":0}}""",
|
||||
"terminal",
|
||||
false,
|
||||
)
|
||||
|
||||
assertNotNull(view)
|
||||
assertEquals("wiring.tsx", view!!.stdout)
|
||||
assertEquals(0, view.exitCode)
|
||||
assertEquals(ToolViewStatus.SUCCESS, view.status)
|
||||
assertEquals("Ran ls", view.title)
|
||||
}
|
||||
|
||||
@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)
|
||||
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)
|
||||
fun `float exit code does not null the parse`() {
|
||||
val view = parseToolOutput("""{"result":{"output":"ok","exit_code":0.0}}""", "terminal", false)
|
||||
|
||||
assertNotNull(view)
|
||||
assertEquals(0, view!!.exitCode)
|
||||
}
|
||||
|
||||
@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)
|
||||
fun `string exit code parses like the desktop engine`() {
|
||||
val view = parseToolOutput("""{"result":{"output":"ok","exit_code":"0.0"}}""", "terminal", false)
|
||||
|
||||
assertNotNull(view)
|
||||
assertEquals(0, view!!.exitCode)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testParseTerminal_withStderr() {
|
||||
val json =
|
||||
"""{
|
||||
"tool_id": "call_002",
|
||||
"name": "terminal",
|
||||
"args": {"command": "rm -rf /"},
|
||||
"result": {"stderr": "Permission denied", "exit_code": 1}
|
||||
}"""
|
||||
val parsed = parseToolOutput(json, "terminal", false)
|
||||
assertNotNull(parsed)
|
||||
assertTrue(parsed!!.isTerminal)
|
||||
assertEquals("Permission denied", parsed.stdout)
|
||||
assertEquals(1, parsed.exitCode)
|
||||
assertEquals("$ rm -rf /", parsed.summaryText)
|
||||
}
|
||||
fun `non-zero exit without output becomes an error`() {
|
||||
val view = parseToolOutput("""{"result":{"exit_code":1}}""", "terminal", false)
|
||||
|
||||
// ── File tools ────────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
fun testParseReadFile() {
|
||||
val json =
|
||||
"""{
|
||||
"tool_id": "call_003",
|
||||
"name": "read_file",
|
||||
"args": {"path": "/opt/hermes/config.yaml"},
|
||||
"result": {"content": "port: 8080\nhost: 0.0.0.0"}
|
||||
}"""
|
||||
val parsed = parseToolOutput(json, "read_file", false)
|
||||
assertNotNull(parsed)
|
||||
assertFalse(parsed!!.isTerminal)
|
||||
assertEquals("📄 /opt/hermes/config.yaml", parsed.summaryText)
|
||||
assertEquals("port: 8080\nhost: 0.0.0.0", parsed.mainOutput)
|
||||
assertNotNull(view)
|
||||
assertEquals(ToolViewStatus.ERROR, view!!.status)
|
||||
assertEquals("Command failed with exit code 1.", view.error)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testParseWriteFile() {
|
||||
val json =
|
||||
"""{
|
||||
"tool_id": "call_004",
|
||||
"name": "write_file",
|
||||
"args": {"path": "/tmp/output.txt"},
|
||||
"result": {"content": "File written successfully"}
|
||||
}"""
|
||||
val parsed = parseToolOutput(json, "write_file", false)
|
||||
assertNotNull(parsed)
|
||||
assertEquals("✏️ /tmp/output.txt", parsed!!.summaryText)
|
||||
fun `terminal error field surfaces`() {
|
||||
val view = parseToolOutput("""{"result":{"error":"Invalid command: frobnicate"}}""", "terminal", false)
|
||||
|
||||
assertNotNull(view)
|
||||
assertEquals(ToolViewStatus.ERROR, view!!.status)
|
||||
assertTrue(view.error!!.contains("Invalid command"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testParsePatch() {
|
||||
val json =
|
||||
"""{
|
||||
"tool_id": "call_005",
|
||||
"name": "patch",
|
||||
"args": {"path": "src/main.kt"},
|
||||
"result": {"success": true}
|
||||
}"""
|
||||
val parsed = parseToolOutput(json, "patch", false)
|
||||
assertNotNull(parsed)
|
||||
assertEquals("🔧 src/main.kt", parsed!!.summaryText)
|
||||
fun `old-format payload without result sub-object parses`() {
|
||||
val view = parseToolOutput("""{"name":"terminal","output":"hi","exit_code":0}""", null, false)
|
||||
|
||||
assertNotNull(view)
|
||||
assertEquals("hi", view!!.stdout)
|
||||
assertEquals(0, view.exitCode)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testParseSearchFiles() {
|
||||
val json =
|
||||
"""{
|
||||
"tool_id": "call_006",
|
||||
"name": "search_files",
|
||||
"args": {"pattern": "*.kt", "path": "/opt/hermes-mobile"},
|
||||
"result": {"matches_data": ["file1.kt", "file2.kt"]}
|
||||
}"""
|
||||
val parsed = parseToolOutput(json, "search_files", false)
|
||||
assertNotNull(parsed)
|
||||
assertEquals("🔍 *.kt", parsed!!.summaryText)
|
||||
}
|
||||
fun `tool name resolves from payload when message has none`() {
|
||||
val view =
|
||||
parseToolOutput(
|
||||
"""{"tool_id":"call_010","name":"terminal","args":{"command":"sleep 10"}}""",
|
||||
null,
|
||||
false,
|
||||
)
|
||||
|
||||
// ── Web / Browser ─────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
fun testParseWebSearch() {
|
||||
val json =
|
||||
"""{
|
||||
"tool_id": "call_007",
|
||||
"name": "web_search",
|
||||
"args": {"query": "Kotlin Coroutines guide"},
|
||||
"result": {"results_data": ["..."]}
|
||||
}"""
|
||||
val parsed = parseToolOutput(json, "web_search", false)
|
||||
assertNotNull(parsed)
|
||||
assertEquals("🌐 Kotlin Coroutines guide", parsed!!.summaryText)
|
||||
assertNotNull(view)
|
||||
assertTrue(view!!.title, view.title.startsWith("Ran "))
|
||||
assertTrue(view.title.contains("sleep 10"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testParseBrowserNavigate() {
|
||||
val json =
|
||||
"""{
|
||||
"tool_id": "call_008",
|
||||
"name": "browser_navigate",
|
||||
"args": {"url": "https://example.com"},
|
||||
"result": {"title": "Example Domain"}
|
||||
}"""
|
||||
val parsed = parseToolOutput(json, "browser_navigate", false)
|
||||
assertNotNull(parsed)
|
||||
assertEquals("🌍 https://example.com", parsed!!.summaryText)
|
||||
fun `running flag drives running status and pending title`() {
|
||||
val view = parseToolOutput("""{"args":{"command":"sleep 10"}}""", "terminal", true)
|
||||
|
||||
assertNotNull(view)
|
||||
assertEquals(ToolViewStatus.RUNNING, view!!.status)
|
||||
assertTrue(view.title, view.title.startsWith("Running"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testParseClarify() {
|
||||
val json =
|
||||
"""{
|
||||
"tool_id": "call_009",
|
||||
"name": "clarify",
|
||||
"args": {"question": "Which environment?"},
|
||||
"result": {"user_response": "staging"}
|
||||
}"""
|
||||
val parsed = parseToolOutput(json, "clarify", false)
|
||||
assertNotNull(parsed)
|
||||
assertEquals("💬 Which environment?", parsed!!.summaryText)
|
||||
}
|
||||
fun `web_search payload yields hits and count`() {
|
||||
val view =
|
||||
parseToolOutput(
|
||||
"""{"args":{"search_term":"cats"},"result":{"data":{"web":[
|
||||
{"title":"Cat Facts","url":"https://cats.example/1","description":"all about cats"},
|
||||
{"title":"More Cats","url":"https://cats.example/2","description":"even more"}
|
||||
]}}}""",
|
||||
"web_search",
|
||||
false,
|
||||
)
|
||||
|
||||
// ── Running state ─────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
fun testParseRunningState() {
|
||||
val json =
|
||||
"""{"tool_id": "call_010", "name": "terminal", "args": {"command": "sleep 10"}}"""
|
||||
val parsed = parseToolOutput(json, "terminal", true)
|
||||
assertNotNull(parsed)
|
||||
assertTrue(parsed!!.isRunning)
|
||||
}
|
||||
|
||||
// ── Unknown tool fallback ─────────────────────────────────
|
||||
|
||||
@Test
|
||||
fun testParseUnknownTool_noSummary() {
|
||||
val json =
|
||||
"""{
|
||||
"tool_id": "call_011",
|
||||
"name": "weird_tool",
|
||||
"args": {"input": "data"},
|
||||
"result": {"output": "processed"}
|
||||
}"""
|
||||
val parsed = parseToolOutput(json, "weird_tool", false)
|
||||
assertNotNull(parsed)
|
||||
assertNull(parsed!!.summaryText) // no known config → no summary
|
||||
assertEquals("processed", parsed.mainOutput)
|
||||
}
|
||||
|
||||
// ── Non-JSON content fallback ─────────────────────────────
|
||||
|
||||
@Test
|
||||
fun testParseNonJson_returnsNull() {
|
||||
val parsed = parseToolOutput("just plain text", "terminal", false)
|
||||
assertNull(parsed)
|
||||
}
|
||||
|
||||
// ── Empty args ────────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
fun testParseTerminal_withoutArgs() {
|
||||
val json =
|
||||
"""{
|
||||
"tool_id": "call_012",
|
||||
"name": "terminal",
|
||||
"result": {"stdout": "done", "exit_code": 0}
|
||||
}"""
|
||||
val parsed = parseToolOutput(json, "terminal", false)
|
||||
assertNotNull(parsed)
|
||||
assertTrue(parsed!!.isTerminal)
|
||||
assertEquals("done", parsed.stdout)
|
||||
assertEquals(0, parsed.exitCode)
|
||||
assertNull(parsed.summaryText) // no args → no summary
|
||||
}
|
||||
|
||||
// ── Diff parsing & Patch tool tests ───────────────────────
|
||||
|
||||
@Test
|
||||
fun testParseDiffText_unifiedDiff() {
|
||||
val rawDiff =
|
||||
"""
|
||||
--- app/src/Main.kt
|
||||
+++ app/src/Main.kt
|
||||
@@ -1,3 +1,3 @@
|
||||
fun main() {
|
||||
- println("hello")
|
||||
+ println("hello world")
|
||||
}
|
||||
""".trimIndent()
|
||||
|
||||
val result = parseDiffText(rawDiff)
|
||||
assertEquals("app/src/Main.kt", result.filePath)
|
||||
assertEquals(1, result.additionsCount)
|
||||
assertEquals(1, result.deletionsCount)
|
||||
|
||||
val addedLine = result.lines.find { it.type == DiffLineType.ADDED }
|
||||
assertNotNull(addedLine)
|
||||
assertEquals("+ println(\"hello world\")", addedLine!!.text)
|
||||
assertNotNull(view)
|
||||
assertEquals(2, view!!.searchHits?.size)
|
||||
assertEquals("2 results", view.countLabel)
|
||||
assertEquals("Searched \"cats\"", view.title)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testParseToolOutput_patchReplaceMode() {
|
||||
val json =
|
||||
"""{
|
||||
"tool_id": "call_patch_1",
|
||||
"name": "patch",
|
||||
"args": {
|
||||
"path": "app/config.json",
|
||||
"old_string": "debug=false",
|
||||
"new_string": "debug=true"
|
||||
},
|
||||
"result": {
|
||||
"output": "--- app/config.json\n+++ app/config.json\n@@ -1,1 +1,1 @@\n-debug=false\n+debug=true"
|
||||
}
|
||||
}"""
|
||||
val parsed = parseToolOutput(json, "patch", false)
|
||||
assertNotNull(parsed)
|
||||
assertEquals("app/config.json", parsed!!.diffPath)
|
||||
val diffOutput = parsed.diffOutput
|
||||
assertNotNull(diffOutput)
|
||||
assertTrue(diffOutput!!.contains("-debug=false"))
|
||||
assertTrue(diffOutput.contains("+debug=true"))
|
||||
fun `patch payload extracts diff`() {
|
||||
val view =
|
||||
parseToolOutput(
|
||||
"""{"args":{"path":"/repo/src/wiring.tsx"},"result":{"inline_diff":""" +
|
||||
""""--- a/wiring.tsx\n+++ b/wiring.tsx\n@@ -1,1 +1,1 @@\n-old\n+new"}}""",
|
||||
"patch",
|
||||
false,
|
||||
)
|
||||
|
||||
assertNotNull(view)
|
||||
assertNotNull(view!!.inlineDiff)
|
||||
assertEquals("/repo/src/wiring.tsx", view.diffPath)
|
||||
assertEquals(1, view.diffStats?.added)
|
||||
assertEquals(1, view.diffStats?.removed)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testParseToolOutput_editTool() {
|
||||
val json =
|
||||
"""{
|
||||
"tool_id": "call_edit_1",
|
||||
"name": "edit",
|
||||
"args": {
|
||||
"path": "app/src/Main.kt",
|
||||
"old_string": "val x = 1",
|
||||
"new_string": "val x = 2"
|
||||
},
|
||||
"result": {
|
||||
"output": "--- app/src/Main.kt\n+++ app/src/Main.kt\n@@ -1,1 +1,1 @@\n-val x = 1\n+val x = 2"
|
||||
}
|
||||
}"""
|
||||
val parsed = parseToolOutput(json, "edit", false)
|
||||
assertNotNull(parsed)
|
||||
assertEquals("app/src/Main.kt", parsed!!.diffPath)
|
||||
val editDiff = parsed.diffOutput
|
||||
assertNotNull(editDiff)
|
||||
assertTrue(editDiff!!.contains("-val x = 1"))
|
||||
assertTrue(editDiff.contains("+val x = 2"))
|
||||
fun `unknown tool gets an engine summary not raw json`() {
|
||||
val view =
|
||||
parseToolOutput(
|
||||
"""{"result":{"data":{"title":"Build report","completed":true}}}""",
|
||||
"weird_tool",
|
||||
false,
|
||||
)
|
||||
|
||||
assertNotNull(view)
|
||||
assertEquals("Weird Tool", view!!.title)
|
||||
assertTrue(view.detail, view.detail.contains("Title: Build report"))
|
||||
assertTrue(view.detail.contains("Completed: true"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testParseSkillManage_createAndPatch() {
|
||||
val createJson =
|
||||
"""{
|
||||
"tool_id": "call_sm_1",
|
||||
"name": "skill_manage",
|
||||
"args": {"action": "patch", "name": "our-workflow"},
|
||||
"result": {"success": true, "message": "Patched skill our-workflow"}
|
||||
}"""
|
||||
val parsed = parseToolOutput(createJson, "skill_manage", false)
|
||||
assertNotNull(parsed)
|
||||
assertEquals("⚡ Skill Patched: our-workflow", parsed!!.summaryText)
|
||||
assertEquals("✅ Patched skill our-workflow", parsed.mainOutput)
|
||||
fun `non-json content returns null`() {
|
||||
assertNull(parseToolOutput("just plain text", "terminal", false))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testParseMemory_selfImprovementBadge() {
|
||||
val memJson =
|
||||
"""{
|
||||
"tool_id": "call_mem_1",
|
||||
"name": "memory",
|
||||
"args": {"action": "add", "target": "user"},
|
||||
"result": {"success": true, "usage": "1,200 / 2,200 chars"}
|
||||
}"""
|
||||
val parsed = parseToolOutput(memJson, "memory", false)
|
||||
assertNotNull(parsed)
|
||||
assertEquals("🧠 Memory Saved (user)", parsed!!.summaryText)
|
||||
fun `non-object json returns null`() {
|
||||
assertNull(parseToolOutput("[1,2,3]", "terminal", false))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `memory summary keeps its target`() {
|
||||
val view =
|
||||
parseToolOutput("""{"args":{"action":"add","target":"user"},"result":{"success":true}}""", "memory", false)
|
||||
|
||||
assertNotNull(view)
|
||||
assertEquals("Memory Saved (user)", view!!.title)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
package com.m57.hermescontrol.ui.chat
|
||||
|
||||
import com.m57.hermescontrol.ui.chat.tool.ToolView
|
||||
import com.m57.hermescontrol.ui.chat.tool.ToolViewStatus
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Test
|
||||
|
||||
/**
|
||||
* Contract tests for [composeToolSummaryLines] — the collapsed-summary
|
||||
* deduplication: the header row already shows the tool name, so a generic
|
||||
* engine title ("Fact Store") must not appear a second time in the summary.
|
||||
*/
|
||||
class ToolSummaryLinesTest {
|
||||
private fun view(
|
||||
title: String,
|
||||
subtitle: String = "",
|
||||
countLabel: String? = null,
|
||||
) = ToolView(
|
||||
status = ToolViewStatus.SUCCESS,
|
||||
title = title,
|
||||
subtitle = subtitle,
|
||||
countLabel = countLabel,
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `generic title merges into the emoji line`() {
|
||||
val lines = composeToolSummaryLines(view("Fact Store", "Fact added (ID: 5)"), "fact_store", "🧠")
|
||||
|
||||
assertEquals("🧠 Fact added (ID: 5)", lines!!.first)
|
||||
assertNull(lines.second)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `todo shows counts on one line`() {
|
||||
val lines = composeToolSummaryLines(view("Todo", "2 items (1 pending, 1 completed)"), "todo", "✅")
|
||||
|
||||
assertEquals("✅ 2 items (1 pending, 1 completed)", lines!!.first)
|
||||
assertNull(lines.second)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `descriptive title stays with subtitle on line two`() {
|
||||
val lines = composeToolSummaryLines(view("Searched \"cats\"", "Query: cats", "3 results"), "web_search", "🌐")
|
||||
|
||||
assertEquals("🌐 Searched \"cats\" (3 results)", lines!!.first)
|
||||
assertEquals("Query: cats", lines.second)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `terminal title stays with output preview on line two`() {
|
||||
val lines = composeToolSummaryLines(view("Ran ls", "wiring.tsx"), "terminal", "💻")
|
||||
|
||||
assertEquals("💻 Ran ls", lines!!.first)
|
||||
assertEquals("wiring.tsx", lines.second)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `read file title stays with path on line two`() {
|
||||
val lines = composeToolSummaryLines(view("Read a.kt L10-14", "/repo/src/a.kt"), "read_file", "📄")
|
||||
|
||||
assertEquals("📄 Read a.kt L10-14", lines!!.first)
|
||||
assertEquals("/repo/src/a.kt", lines.second)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `generic title with no subtitle and no count is hidden`() {
|
||||
assertNull(composeToolSummaryLines(view("Fact Store"), "fact_store", "🧠"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `unknown tool generic title merges with engine first line`() {
|
||||
val lines = composeToolSummaryLines(view("Weird Tool", "- Title: Build report"), "weird_tool", "🔧")
|
||||
|
||||
assertEquals("🔧 - Title: Build report", lines!!.first)
|
||||
assertNull(lines.second)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `null tool name resolves to the generic tool title`() {
|
||||
val lines = composeToolSummaryLines(view("Tool", "raw payload text"), null, "🔧")
|
||||
|
||||
assertEquals("🔧 raw payload text", lines!!.first)
|
||||
assertNull(lines.second)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `subtitle identical to title is not duplicated`() {
|
||||
val lines = composeToolSummaryLines(view("Fact Store", "Fact Store"), "fact_store", "🧠")
|
||||
|
||||
assertNull(lines)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user