refactor(chat): isolate transcript and browser return state (#1320, #1321) (#1357)

This commit is contained in:
m57 authored and GitHub committed 2026-09-28 14:30:05 +03:00
1 parent cc010b451a
commit 76726dd086
12 files changed
+521 -201

No files matched your search

@@ -11,6 +11,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import com.m57.hermescontrol.data.model.Attachment
import com.m57.hermescontrol.data.ws.ConnectionStatus
import com.m57.hermescontrol.ui.chat.fullbleed.TranscriptUiState
import com.m57.hermescontrol.ui.common.ActionProgressController
import io.mockk.every
import io.mockk.mockk
@@ -61,6 +62,10 @@ class ChatScreenTest {
every { mockViewModel.uiState } returns MutableStateFlow(uiState).asStateFlow()
every { mockViewModel.streamingState } returns MutableStateFlow(StreamingState()).asStateFlow()
every { mockViewModel.timelineState } returns MutableStateFlow(ChatTimelineState()).asStateFlow()
every { mockViewModel.transcriptState } returns
MutableStateFlow(
TranscriptUiState.resolve(uiState, ChatTimelineState(), StreamingState(), null, null),
).asStateFlow()
every { mockViewModel.connectionOperationState } returns
MutableStateFlow(ConnectionOperationUiState()).asStateFlow()
// ActionProgressDialog collects the controller's StateFlow — a relaxed
@@ -110,6 +115,10 @@ class ChatScreenTest {
every { mockViewModel.uiState } returns MutableStateFlow(uiState).asStateFlow()
every { mockViewModel.streamingState } returns MutableStateFlow(StreamingState()).asStateFlow()
every { mockViewModel.timelineState } returns MutableStateFlow(ChatTimelineState()).asStateFlow()
every { mockViewModel.transcriptState } returns
MutableStateFlow(
TranscriptUiState.resolve(uiState, ChatTimelineState(), StreamingState(), null, null),
).asStateFlow()
every { mockViewModel.connectionOperationState } returns
MutableStateFlow(ConnectionOperationUiState()).asStateFlow()
every { mockViewModel.actionProgress } returns
@@ -12,7 +12,8 @@ import androidx.compose.ui.unit.dp
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.m57.hermescontrol.ui.chat.ChatMessage
import com.m57.hermescontrol.ui.chat.ChatSearchState
import com.m57.hermescontrol.ui.chat.ChatViewModel
import com.m57.hermescontrol.ui.chat.ChatTimelineState
import com.m57.hermescontrol.ui.chat.ChatUiState
import com.m57.hermescontrol.ui.chat.ClarifyUi
import com.m57.hermescontrol.ui.chat.MessageRole
import com.m57.hermescontrol.ui.chat.StreamingState
@@ -21,7 +22,6 @@ import com.m57.hermescontrol.ui.chat.VaultCodePromptUi
import com.m57.hermescontrol.ui.chat.VaultSaveLoginPromptUi
import com.m57.hermescontrol.ui.chat.VaultUnlockPromptUi
import com.m57.hermescontrol.ui.chat.components.rememberChatScrollController
import io.mockk.mockk
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.launch
@@ -116,29 +116,58 @@ class FullBleedChatAnchorTest {
val tailKeys =
listOf("agent_status", "clarify_bubble", "vault_unlock_card", "vault_save_login_card", "vault_code_card")
val listState = LazyListState(index, offset)
val viewModel = mockk<ChatViewModel>(relaxed = true)
lateinit var scope: CoroutineScope
compose.setContent {
scope = rememberCoroutineScope()
val controller = rememberChatScrollController(listState, scope)
Box(Modifier.size(width = 320.dp, height = if (tailKey == null) 400.dp else 24.dp)) {
FullBleedChatList(
messages = messages.value,
streamingState = streaming.value,
isAgentTyping = tailKey != null,
transcript =
TranscriptUiState.resolve(
chat =
ChatUiState(
messages = messages.value,
isAgentTyping = tailKey != null,
typingEffectEnabled = false,
typingEffectDelayMs = 30,
isLoadingOlder = loading.value,
currentSessionId = session.value,
clarifyRequest =
if (tailKey !=
null
) {
ClarifyUi("Choose an option", listOf("Yes"))
} else {
null
},
vaultUnlockPrompt =
if (tailKey != null) {
VaultUnlockPromptUi(
null,
null,
)
} else {
null
},
vaultSaveLoginPrompt =
if (tailKey !=
null
) {
VaultSaveLoginPromptUi(null, null)
} else {
null
},
vaultCodePrompt = if (tailKey != null) VaultCodePromptUi(null, null) else null,
),
timeline = ChatTimelineState(),
streaming = streaming.value,
savingAttachmentPath = null,
speakingMessageId = null,
),
actions = testTranscriptActions(),
searchState = ChatSearchState(),
typingEffectEnabled = false,
typingEffectDelayMs = 30,
isLoading = false,
isLoadingOlder = loading.value,
listState = listState,
scrollController = controller,
viewModel = viewModel,
pagingSessionId = session.value,
clarifyRequest = if (tailKey != null) ClarifyUi("Choose an option", listOf("Yes")) else null,
vaultUnlockPrompt = if (tailKey != null) VaultUnlockPromptUi(null, null) else null,
vaultSaveLoginPrompt = if (tailKey != null) VaultSaveLoginPromptUi(null, null) else null,
vaultCodePrompt = if (tailKey != null) VaultCodePromptUi(null, null) else null,
)
}
}
@@ -17,12 +17,12 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import com.m57.hermescontrol.ui.chat.ChatMessage
import com.m57.hermescontrol.ui.chat.ChatSearchState
import com.m57.hermescontrol.ui.chat.ChatViewModel
import com.m57.hermescontrol.ui.chat.ChatTimelineState
import com.m57.hermescontrol.ui.chat.ChatUiState
import com.m57.hermescontrol.ui.chat.MessageRole
import com.m57.hermescontrol.ui.chat.StreamingState
import com.m57.hermescontrol.ui.chat.ToolStatus
import com.m57.hermescontrol.ui.chat.components.ChatScrollController
import io.mockk.mockk
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import org.junit.Rule
@@ -76,34 +76,41 @@ class FullBleedChatListTest {
composeTestRule.setContent {
val listState = LazyListState()
FullBleedChatList(
messages = messages,
streamingState = streamingState,
isAgentTyping = isAgentTyping,
transcript =
TranscriptUiState.resolve(
chat =
ChatUiState(
messages = messages,
isAgentTyping = isAgentTyping,
typingEffectEnabled = false,
typingEffectDelayMs = 30,
messageStatsEnabled = messageStatsEnabled,
showUserMessageTokens = showUserMessageTokens,
showAssistantMessageTokens = showAssistantMessageTokens,
showTokensPerSecond = showTokensPerSecond,
clarifyRequest =
if (clarify) {
com.m57.hermescontrol.ui.chat.ClarifyUi(
text = "pick",
options = listOf("a"),
)
} else {
null
},
),
timeline = ChatTimelineState(),
streaming = streamingState,
savingAttachmentPath = null,
speakingMessageId = null,
),
actions = testTranscriptActions(),
searchState = ChatSearchState(),
typingEffectEnabled = false,
typingEffectDelayMs = 30,
messageStatsEnabled = messageStatsEnabled,
showUserMessageTokens = showUserMessageTokens,
showAssistantMessageTokens = showAssistantMessageTokens,
showTokensPerSecond = showTokensPerSecond,
isLoading = false,
isLoadingOlder = false,
listState = listState,
scrollController =
ChatScrollController(
listState = listState,
scope = CoroutineScope(Dispatchers.Main.immediate),
),
viewModel = mockk<ChatViewModel>(relaxed = true),
clarifyRequest =
if (clarify) {
com.m57.hermescontrol.ui.chat.ClarifyUi(
text = "pick",
options = listOf("a"),
)
} else {
null
},
)
}
}
@@ -0,0 +1,21 @@
package com.m57.hermescontrol.ui.chat.fullbleed
/** No-op actions for transcript layout tests; callbacks are irrelevant to their assertions. */
internal fun testTranscriptActions() =
TranscriptActions(
onLoadOlder = {},
onOpenAttachment = {},
onSaveAttachment = {},
onImageClick = {},
onRespondApproval = {},
onRespondClarify = {},
onRespondClarifyBatch = {},
onDismissClarify = {},
onRespondVaultUnlock = {},
onDismissVaultUnlock = {},
onRespondVaultSaveLogin = { _, _ -> },
onDismissVaultSaveLogin = {},
onRespondVaultCode = {},
onDismissVaultCode = {},
onToggleSpeak = {},
)