feat(chat): add configurable message stats (#1134)
This commit is contained in:
20 files changed
+606
-44
No files matched your search
+165
-1
@@ -1,12 +1,18 @@
|
||||
package com.m57.hermescontrol.ui.chat.fullbleed
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.test.assertCountEquals
|
||||
import androidx.compose.ui.test.assertIsDisplayed
|
||||
import androidx.compose.ui.test.getUnclippedBoundsInRoot
|
||||
import androidx.compose.ui.test.junit4.createComposeRule
|
||||
import androidx.compose.ui.test.onAllNodesWithTag
|
||||
import androidx.compose.ui.test.onNodeWithTag
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.filters.MediumTest
|
||||
import com.m57.hermescontrol.ui.chat.ChatMessage
|
||||
@@ -44,7 +50,17 @@ class FullBleedChatListTest {
|
||||
content: String = "content-$id",
|
||||
toolStatus: ToolStatus? = null,
|
||||
isStreaming: Boolean = false,
|
||||
) = ChatMessage(id = id, role = role, content = content, toolStatus = toolStatus, isStreaming = isStreaming)
|
||||
tokenCount: Int? = null,
|
||||
tps: Double? = null,
|
||||
) = ChatMessage(
|
||||
id = id,
|
||||
role = role,
|
||||
content = content,
|
||||
toolStatus = toolStatus,
|
||||
isStreaming = isStreaming,
|
||||
tokenCount = tokenCount,
|
||||
tps = tps,
|
||||
)
|
||||
|
||||
private fun render(
|
||||
messages: List<ChatMessage>,
|
||||
@@ -52,6 +68,10 @@ class FullBleedChatListTest {
|
||||
streamingState: StreamingState = StreamingState(streamingMessage = streamingMessage),
|
||||
isAgentTyping: Boolean = streamingMessage?.isStreaming == true,
|
||||
clarify: Boolean = false,
|
||||
messageStatsEnabled: Boolean = false,
|
||||
showUserMessageTokens: Boolean = true,
|
||||
showAssistantMessageTokens: Boolean = true,
|
||||
showTokensPerSecond: Boolean = true,
|
||||
) {
|
||||
composeTestRule.setContent {
|
||||
val listState = LazyListState()
|
||||
@@ -62,6 +82,10 @@ class FullBleedChatListTest {
|
||||
searchState = ChatSearchState(),
|
||||
typingEffectEnabled = false,
|
||||
typingEffectDelayMs = 30,
|
||||
messageStatsEnabled = messageStatsEnabled,
|
||||
showUserMessageTokens = showUserMessageTokens,
|
||||
showAssistantMessageTokens = showAssistantMessageTokens,
|
||||
showTokensPerSecond = showTokensPerSecond,
|
||||
isLoading = false,
|
||||
isLoadingOlder = false,
|
||||
listState = listState,
|
||||
@@ -133,6 +157,146 @@ class FullBleedChatListTest {
|
||||
composeTestRule.onNodeWithTag("fullbleed_finish_time").assertDoesNotExist()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun messageStats_masterOff_hidesAllMetadata() {
|
||||
render(
|
||||
messages =
|
||||
listOf(
|
||||
msg("u1", MessageRole.USER, tokenCount = 123),
|
||||
msg("a1", MessageRole.ASSISTANT, tokenCount = 456, tps = 42.5),
|
||||
),
|
||||
)
|
||||
composeTestRule.onNodeWithTag("bubble_token_count").assertDoesNotExist()
|
||||
composeTestRule.onNodeWithTag("fullbleed_token_count").assertDoesNotExist()
|
||||
composeTestRule.onNodeWithTag("fullbleed_tps").assertDoesNotExist()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun messageStats_userTokensOnly_showsOnlyUserTokens() {
|
||||
render(
|
||||
messages =
|
||||
listOf(
|
||||
msg("u1", MessageRole.USER, tokenCount = 123),
|
||||
msg("a1", MessageRole.ASSISTANT, tokenCount = 456, tps = 42.5),
|
||||
),
|
||||
messageStatsEnabled = true,
|
||||
showAssistantMessageTokens = false,
|
||||
showTokensPerSecond = false,
|
||||
)
|
||||
composeTestRule.onNodeWithTag("bubble_token_count").assertIsDisplayed()
|
||||
composeTestRule.onNodeWithTag("fullbleed_token_count").assertDoesNotExist()
|
||||
composeTestRule.onNodeWithTag("fullbleed_tps").assertDoesNotExist()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun messageStats_assistantTokensOnly_showsOnlyAssistantTokens() {
|
||||
render(
|
||||
messages =
|
||||
listOf(
|
||||
msg("u1", MessageRole.USER, tokenCount = 123),
|
||||
msg("a1", MessageRole.ASSISTANT, tokenCount = 456, tps = 42.5),
|
||||
),
|
||||
messageStatsEnabled = true,
|
||||
showUserMessageTokens = false,
|
||||
showTokensPerSecond = false,
|
||||
)
|
||||
composeTestRule.onNodeWithTag("bubble_token_count").assertDoesNotExist()
|
||||
composeTestRule.onNodeWithTag("fullbleed_token_count").assertIsDisplayed()
|
||||
composeTestRule.onNodeWithTag("fullbleed_tps").assertDoesNotExist()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun messageStats_tpsOnly_showsOnlyAssistantTps() {
|
||||
render(
|
||||
messages =
|
||||
listOf(
|
||||
msg("u1", MessageRole.USER, tokenCount = 123),
|
||||
msg("a1", MessageRole.ASSISTANT, tokenCount = 456, tps = 42.5),
|
||||
),
|
||||
messageStatsEnabled = true,
|
||||
showUserMessageTokens = false,
|
||||
showAssistantMessageTokens = false,
|
||||
)
|
||||
composeTestRule.onNodeWithTag("bubble_token_count").assertDoesNotExist()
|
||||
composeTestRule.onNodeWithTag("fullbleed_token_count").assertDoesNotExist()
|
||||
composeTestRule.onNodeWithTag("fullbleed_tps").assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun messageStats_allEnabled_showsUserTokensAssistantTokensAndTps() {
|
||||
render(
|
||||
messages =
|
||||
listOf(
|
||||
msg("u1", MessageRole.USER, tokenCount = 123),
|
||||
msg("a1", MessageRole.ASSISTANT, tokenCount = 456, tps = 42.5),
|
||||
),
|
||||
messageStatsEnabled = true,
|
||||
)
|
||||
composeTestRule.onNodeWithTag("bubble_token_count").assertIsDisplayed()
|
||||
composeTestRule.onNodeWithTag("fullbleed_token_count").assertIsDisplayed()
|
||||
composeTestRule.onNodeWithTag("fullbleed_tps").assertIsDisplayed()
|
||||
composeTestRule.onAllNodesWithTag("fullbleed_agent_header").assertCountEquals(0)
|
||||
composeTestRule.onNodeWithTag("fullbleed_finish_time").assertDoesNotExist()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun messageStats_nullAndZeroValues_areHidden() {
|
||||
render(
|
||||
messages =
|
||||
listOf(
|
||||
msg("u1", MessageRole.USER, tokenCount = 0),
|
||||
msg("a1", MessageRole.ASSISTANT, tokenCount = 0, tps = 0.0),
|
||||
),
|
||||
messageStatsEnabled = true,
|
||||
)
|
||||
composeTestRule.onNodeWithTag("bubble_token_count").assertDoesNotExist()
|
||||
composeTestRule.onNodeWithTag("fullbleed_token_count").assertDoesNotExist()
|
||||
composeTestRule.onNodeWithTag("fullbleed_tps").assertDoesNotExist()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun messageStats_streamingAssistant_hidesFinalMetadata() {
|
||||
render(
|
||||
messages = listOf(msg("u1", MessageRole.USER)),
|
||||
streamingMessage =
|
||||
msg(
|
||||
"s1",
|
||||
MessageRole.ASSISTANT,
|
||||
content = "",
|
||||
isStreaming = true,
|
||||
tokenCount = 456,
|
||||
tps = 42.5,
|
||||
),
|
||||
isAgentTyping = true,
|
||||
messageStatsEnabled = true,
|
||||
)
|
||||
composeTestRule.onNodeWithTag("fullbleed_token_count").assertDoesNotExist()
|
||||
composeTestRule.onNodeWithTag("fullbleed_tps").assertDoesNotExist()
|
||||
composeTestRule.onNodeWithTag("typing_indicator").assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun assistantStats_wrapInsideConstrainedWidth() {
|
||||
composeTestRule.setContent {
|
||||
Box(
|
||||
modifier = Modifier.width(220.dp).testTag("stats_host"),
|
||||
) {
|
||||
FullBleedAgentMessage(
|
||||
message = msg("a1", MessageRole.ASSISTANT, tokenCount = 123456789, tps = 9876.5),
|
||||
messageStatsEnabled = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
composeTestRule.waitForIdle()
|
||||
val host = composeTestRule.onNodeWithTag("stats_host").getUnclippedBoundsInRoot()
|
||||
val token = composeTestRule.onNodeWithTag("fullbleed_token_count").getUnclippedBoundsInRoot()
|
||||
val tps = composeTestRule.onNodeWithTag("fullbleed_tps").getUnclippedBoundsInRoot()
|
||||
assert(token.left >= host.left)
|
||||
assert(token.right <= host.right)
|
||||
assert(tps.left >= host.left)
|
||||
assert(tps.right <= host.right)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun streaming_and_clarify_stillRender() {
|
||||
render(
|
||||
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
package com.m57.hermescontrol.ui.settings.components
|
||||
|
||||
import androidx.compose.ui.test.assertIsEnabled
|
||||
import androidx.compose.ui.test.assertIsNotEnabled
|
||||
import androidx.compose.ui.test.assertIsOff
|
||||
import androidx.compose.ui.test.assertIsOn
|
||||
import androidx.compose.ui.test.junit4.createComposeRule
|
||||
import androidx.compose.ui.test.onNodeWithTag
|
||||
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 ChatSectionTest {
|
||||
@get:Rule
|
||||
val composeTestRule = createComposeRule()
|
||||
|
||||
@Test
|
||||
fun messageStatsMasterOff_keepsChildrenVisibleCheckedAndDisabled() {
|
||||
composeTestRule.setContent {
|
||||
ChatSection(
|
||||
typingEffectEnabled = false,
|
||||
onTypingEffectEnabledChange = {},
|
||||
typingEffectDelayMs = 30,
|
||||
onTypingEffectDelayMsChange = {},
|
||||
messageStatsEnabled = false,
|
||||
showUserMessageTokens = true,
|
||||
showAssistantMessageTokens = true,
|
||||
showTokensPerSecond = true,
|
||||
)
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithTag("settings_message_stats").assertIsOff().assertIsEnabled()
|
||||
composeTestRule.onNodeWithTag("settings_user_message_tokens").assertIsOn().assertIsNotEnabled()
|
||||
composeTestRule.onNodeWithTag("settings_assistant_message_tokens").assertIsOn().assertIsNotEnabled()
|
||||
composeTestRule.onNodeWithTag("settings_tps").assertIsOn().assertIsNotEnabled()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun messageStatsMasterOn_enablesChildrenAndRoutesCallbacks() {
|
||||
var masterChanges = 0
|
||||
var userChanges = 0
|
||||
var assistantChanges = 0
|
||||
var tpsChanges = 0
|
||||
composeTestRule.setContent {
|
||||
ChatSection(
|
||||
typingEffectEnabled = false,
|
||||
onTypingEffectEnabledChange = {},
|
||||
typingEffectDelayMs = 30,
|
||||
onTypingEffectDelayMsChange = {},
|
||||
messageStatsEnabled = true,
|
||||
onMessageStatsEnabledChange = { masterChanges++ },
|
||||
showUserMessageTokens = true,
|
||||
onUserMessageTokensChange = { userChanges++ },
|
||||
showAssistantMessageTokens = true,
|
||||
onAssistantMessageTokensChange = { assistantChanges++ },
|
||||
showTokensPerSecond = true,
|
||||
onTokensPerSecondChange = { tpsChanges++ },
|
||||
)
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithTag("settings_user_message_tokens").assertIsEnabled().performClick()
|
||||
composeTestRule.onNodeWithTag("settings_assistant_message_tokens").assertIsEnabled().performClick()
|
||||
composeTestRule.onNodeWithTag("settings_tps").assertIsEnabled().performClick()
|
||||
assertEquals(0, masterChanges)
|
||||
assertEquals(1, userChanges)
|
||||
assertEquals(1, assistantChanges)
|
||||
assertEquals(1, tpsChanges)
|
||||
|
||||
composeTestRule.onNodeWithTag("settings_message_stats").performClick()
|
||||
assertEquals(1, masterChanges)
|
||||
assertEquals(1, userChanges)
|
||||
assertEquals(1, assistantChanges)
|
||||
assertEquals(1, tpsChanges)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user