perf: optimize startup, database, and ui rendering (#405)

* perf: optimize startup, database, and ui rendering

* fix(#405): address code review feedback

* test: wait for async prefs initialization to prevent coroutine leaks

* test: stub mockViewModel.streamingState in ChatScreenTest to prevent ClassCastException on Android
This commit is contained in:
M57 authored and GitHub committed 2026-06-27 01:38:44 +03:00
1 parent c0ebc8ec64
commit 8f14aca647
14 files changed
+435 -169

No files matched your search

@@ -43,12 +43,19 @@ class AuthManagerTest {
every { MasterKeys.getOrCreate(any()) } returns "mockMasterKey"
// Reset prefs singleton instance using reflection
val field = AuthManager::class.java.getDeclaredField("prefs")
val field = AuthManager::class.java.getDeclaredField("prefsDeferred")
field.isAccessible = true
field.set(AuthManager, null)
// Initialise AuthManager
AuthManager.init(mockContext)
// Wait for async initialization to complete to prevent coroutine leaks
kotlinx.coroutines.runBlocking {
val deferred = field.get(AuthManager) as? kotlinx.coroutines.Deferred<*>
deferred?.await()
}
AuthManager.resetTokenCacheForTest()
}
@@ -263,8 +270,10 @@ class AuthManagerTest {
fun testConnectionProfiles_roundTrip() {
val profiles =
listOf(
com.m57.hermescontrol.data.model.ConnectionProfile("a", "A", "10.0.0.1", 9119),
com.m57.hermescontrol.data.model.ConnectionProfile("b", "B", "10.0.0.2", 9220),
com.m57.hermescontrol.data.model
.ConnectionProfile("a", "A", "10.0.0.1", 9119),
com.m57.hermescontrol.data.model
.ConnectionProfile("b", "B", "10.0.0.2", 9220),
)
AuthManager.saveConnectionProfiles(profiles)
@@ -42,6 +42,7 @@ class HermesWsClientTest {
mockkObject(AuthManager)
every { AuthManager.wsUrl() } returns mockWebServer.url("/").toString().replace("http://", "ws://")
every { AuthManager.isAutoReconnect() } returns false
every { AuthManager.getSessionCookie() } returns null
// Reset state
val connectedField = HermesWsClient::class.java.getDeclaredField("connected")
@@ -542,32 +542,40 @@ class ChatViewModelTest {
var state = viewModel.uiState.value
assertTrue(state.isAgentTyping)
assertNotNull(state.streamingMessage)
assertEquals("", state.streamingMessage?.content)
assertFalse(state.isThinking)
assertEquals("", state.thinkingText)
assertNotNull(viewModel.streamingState.value.streamingMessage)
assertEquals(
"",
viewModel.streamingState.value.streamingMessage
?.content,
)
assertFalse(viewModel.streamingState.value.isThinking)
assertEquals("", viewModel.streamingState.value.thinkingText)
// Thinking Delta 1
mockEventsFlow.emit(WsEvent.ThinkingDelta("Thinking...", "session-123"))
advanceUntilIdle()
state = viewModel.uiState.value
assertTrue(state.isThinking)
assertEquals("Thinking...", state.thinkingText)
assertTrue(viewModel.streamingState.value.isThinking)
assertEquals("Thinking...", viewModel.streamingState.value.thinkingText)
// Thinking Delta 2
mockEventsFlow.emit(WsEvent.ThinkingDelta(" deeper", "session-123"))
advanceUntilIdle()
state = viewModel.uiState.value
assertTrue(state.isThinking)
assertEquals("Thinking... deeper", state.thinkingText)
assertTrue(viewModel.streamingState.value.isThinking)
assertEquals("Thinking... deeper", viewModel.streamingState.value.thinkingText)
// Token 1
mockEventsFlow.emit(WsEvent.MessageToken("Hello", "session-123"))
advanceUntilIdle()
state = viewModel.uiState.value
assertFalse(state.isThinking)
assertNotNull(state.streamingMessage)
assertEquals("Hello", state.streamingMessage?.content)
assertFalse(viewModel.streamingState.value.isThinking)
assertNotNull(viewModel.streamingState.value.streamingMessage)
assertEquals(
"Hello",
viewModel.streamingState.value.streamingMessage
?.content,
)
// Streaming message is not in the main messages list yet, only "Session created" exists
assertEquals(1, state.messages.size)
@@ -575,8 +583,12 @@ class ChatViewModelTest {
mockEventsFlow.emit(WsEvent.MessageToken(" world", "session-123"))
advanceUntilIdle()
state = viewModel.uiState.value
assertNotNull(state.streamingMessage)
assertEquals("Hello world", state.streamingMessage?.content)
assertNotNull(viewModel.streamingState.value.streamingMessage)
assertEquals(
"Hello world",
viewModel.streamingState.value.streamingMessage
?.content,
)
assertEquals(1, state.messages.size)
// Complete
@@ -584,9 +596,9 @@ class ChatViewModelTest {
advanceUntilIdle()
state = viewModel.uiState.value
assertFalse(state.isAgentTyping)
assertNull(state.streamingMessage)
assertFalse(state.isThinking)
assertEquals("", state.thinkingText)
assertNull(viewModel.streamingState.value.streamingMessage)
assertFalse(viewModel.streamingState.value.isThinking)
assertEquals("", viewModel.streamingState.value.thinkingText)
assertEquals(2, state.messages.size)
assertEquals("Session created", state.messages[0].content)
assertEquals("Hello world!", state.messages[1].content)
@@ -838,7 +850,7 @@ class ChatViewModelTest {
assertEquals(MessageRole.ASSISTANT, state.messages[1].role)
assertFalse(state.messages[1].isStreaming)
assertEquals(MessageRole.TOOL, state.messages[2].role)
assertNull(state.streamingMessage)
assertNull(viewModel.streamingState.value.streamingMessage)
}
@Test
@@ -878,9 +890,16 @@ class ChatViewModelTest {
assertFalse(state.messages[1].isStreaming)
// Active streaming message should be the second segment
assertNotNull(state.streamingMessage)
assertEquals("Second response segment", state.streamingMessage?.content)
assertTrue(state.streamingMessage?.isStreaming == true)
assertNotNull(viewModel.streamingState.value.streamingMessage)
assertEquals(
"Second response segment",
viewModel.streamingState.value.streamingMessage
?.content,
)
assertTrue(
viewModel.streamingState.value.streamingMessage
?.isStreaming == true,
)
}
@Test
@@ -974,7 +993,7 @@ class ChatViewModelTest {
// Only the "Session created" message should be present (mismatch events ignored)
assertEquals(1, state.messages.size)
assertEquals("Session created", state.messages[0].content)
assertNull(state.streamingMessage)
assertNull(viewModel.streamingState.value.streamingMessage)
assertNull(state.clarifyRequest)
}
@@ -1008,7 +1027,11 @@ class ChatViewModelTest {
val state = viewModel.uiState.value
// Content should be "Hello", not "HelloHello" (no duplicate collectors running)
assertEquals("Hello", state.streamingMessage?.content)
assertEquals(
"Hello",
viewModel.streamingState.value.streamingMessage
?.content,
)
}
@Test