feat(chat): show read and write file contents in code viewer card (#1238)

This commit is contained in:
m57 authored and GitHub committed 2026-09-23 14:00:55 +03:00
1 parent 0eb8d9a8c1
commit 4a732870d7
11 files changed
+681 -346

No files matched your search

@@ -0,0 +1,95 @@
package com.m57.hermescontrol.ui.chat
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import com.m57.hermescontrol.theme.HermesControlTheme
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
@MediumTest
class FileToolBubbleTest {
@get:Rule
val composeRule = createComposeRule()
@Test
fun tappingReadFileBubbleShowsFileViewCardWithSyntaxViewer() {
val payload =
"""
{
"args": {"path": "/repo/src/Main.kt", "offset": 1, "limit": 2},
"result": {"content": "1|fun main() {\n2| println(\"hi\")\n3|}"}
}
""".trimIndent()
showBubble("read_file", payload)
composeRule.onNodeWithText("Read Main.kt L1-2", substring = true).performClick()
composeRule.onNodeWithTag("file_view_card", useUnmergedTree = true).assertExists()
composeRule.onNodeWithText("/repo/src/Main.kt", useUnmergedTree = true).assertExists()
composeRule.onNodeWithText("3 lines", useUnmergedTree = true).assertExists()
}
@Test
fun tappingWriteFileBubbleShowsFileViewCardWhenNoDiff() {
val payload =
"""
{
"args": {
"path": "/repo/src/Config.json",
"content": "{\n \"enabled\": true\n}"
},
"result": {"verified": true, "success": true}
}
""".trimIndent()
showBubble("write_file", payload)
composeRule.onNodeWithText("Config.json", substring = true).performClick()
composeRule.onNodeWithTag("file_view_card", useUnmergedTree = true).assertExists()
composeRule.onNodeWithText("/repo/src/Config.json", useUnmergedTree = true).assertExists()
composeRule.onNodeWithText("3 lines", useUnmergedTree = true).assertExists()
}
@Test
fun tappingWriteFileWithDiffShowsDiffViewCard() {
val payload =
"""
{
"args": {"path": "/repo/src/Utils.kt"},
"result": {
"inline_diff": "--- a/Utils.kt\n+++ b/Utils.kt\n@@ -1,1 +1,1 @@\n-old\n+new"
}
}
""".trimIndent()
showBubble("write_file", payload)
composeRule.onNodeWithText("Utils.kt", substring = true).performClick()
composeRule.onNodeWithTag("diff_view_card", useUnmergedTree = true).assertExists()
composeRule.onNodeWithText("Utils.kt", useUnmergedTree = true).assertExists()
}
private fun showBubble(
name: String,
content: String,
) {
composeRule.setContent {
HermesControlTheme {
ToolBubble(
ChatMessage(
role = MessageRole.TOOL,
content = content,
toolName = name,
toolStatus = ToolStatus.COMPLETED,
),
)
}
}
}
}
@@ -0,0 +1,78 @@
package com.m57.hermescontrol.ui.chat.components
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import org.junit.Assert.assertEquals
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
@MediumTest
class FileViewCardTest {
@get:Rule
val composeTestRule = createComposeRule()
@Test
fun shortFile_displaysPathLineCountAndFullContentWithoutExpandButton() {
val content = "fun hello() = println(\"world\")\nval x = 42"
var copiedText = ""
composeTestRule.setContent {
FileViewCard(
content = content,
filePath = "src/main/Test.kt",
onCopy = { copiedText = it },
)
}
composeTestRule.onNodeWithTag("file_view_card").assertIsDisplayed()
composeTestRule.onNodeWithText("src/main/Test.kt").assertIsDisplayed()
composeTestRule.onNodeWithText("2 lines").assertIsDisplayed()
composeTestRule.onNodeWithText("Show full file (2 lines)").assertDoesNotExist()
composeTestRule.onNodeWithTag("file_view_card").assertIsDisplayed()
}
@Test
fun longFile_showsExpandButtonAndTogglesFullFile() {
val lines = (1..25).map { "line $it: println($it)" }
val content = lines.joinToString("\n")
composeTestRule.setContent {
FileViewCard(
content = content,
filePath = "src/Big.kt",
)
}
composeTestRule.onNodeWithText("Show full file (25 lines)").assertIsDisplayed()
composeTestRule.onNodeWithText("Show full file (25 lines)").performClick()
composeTestRule.onNodeWithText("Collapse file").assertIsDisplayed()
composeTestRule.onNodeWithText("Collapse file").performClick()
composeTestRule.onNodeWithText("Show full file (25 lines)").assertIsDisplayed()
}
@Test
fun copyButton_invokesCallbackWithFullContent() {
val content = "1|fun main() = Unit"
var copied = ""
composeTestRule.setContent {
FileViewCard(
content = content,
filePath = "src/App.kt",
onCopy = { copied = it },
)
}
composeTestRule.onNodeWithText("src/App.kt").assertIsDisplayed()
composeTestRule.onNodeWithContentDescription("Copy file content").performClick()
assertEquals(content, copied)
}
}