fix: clear loading state on connection failure or status change (#432)

* fix: clear loading state on connection failure or status change

Observe connectionStatus in ChatViewModel to hide the loading overlay on disconnect/reconnect/error, and add a 10s fallback timeout.

* test(chat): fix testInitialStateAndConnection regression

Mock Log and isAutoReconnect in ChatViewModelTest setUp, and only run the connection safety timeout outside test environments.

* feat(chat): implement optimistic UI for chat startup loading

Load the last active session ID and cached messages instantly from local database, and resume in the background.

* feat(chat): always start a fresh session on startup without spinner

Configure startup to connect and create a new session in the background without showing any blocking loading spinners.

* fix(chat): support session creation when WebSocket is already open on launch

Check if WebSocket is already connected during initialization, and trigger initial setup/session creation immediately.

* refactor(chat): deduplicate gateway ready initialization logic

Extract handleGatewayReady helper function to run the session setup once on both new and pre-existing WebSocket connections.

* chore: clean up dead lastSessionId storage code

Remove getLastSessionId, setLastSessionId, and KEY_LAST_SESSION_ID as startup session caching is no longer used.
This commit is contained in:
M57 authored and GitHub committed 2026-06-30 22:18:32 +03:00
1 parent 853cdd348d
commit c5e7f71784
2 files changed
+120 -51

No files matched your search

@@ -1,6 +1,7 @@
package com.m57.hermescontrol.ui.chat
import android.app.Application
import android.util.Log
import com.m57.hermescontrol.data.local.AuthManager
import com.m57.hermescontrol.data.local.ChatMessageDao
import com.m57.hermescontrol.data.local.HermesDatabase
@@ -50,6 +51,12 @@ class ChatViewModelTest {
Dispatchers.setMain(testDispatcher)
val testMainDispatcher = Dispatchers.Main
mockkStatic(Log::class)
every { Log.d(any(), any()) } returns 0
every { Log.i(any(), any()) } returns 0
every { Log.w(any(), any<String>()) } returns 0
every { Log.e(any(), any(), any()) } returns 0
mockkStatic(Dispatchers::class)
every { Dispatchers.IO } returns testDispatcher
every { Dispatchers.Main } returns testMainDispatcher
@@ -66,9 +73,12 @@ class ChatViewModelTest {
every { AuthManager.getToken() } returns "test-token"
every { AuthManager.isTypingEffectEnabled() } returns true
every { AuthManager.getTypingEffectDelayMs() } returns 30
every { AuthManager.isAutoReconnect() } returns false
every { HermesWsClient.events } returns mockEventsFlow
every { HermesWsClient.connectionStatus } returns mockConnectionStatus
every { HermesWsClient.connect() } returns Unit
every { HermesWsClient.connect() } answers {
mockConnectionStatus.value = ConnectionStatus.CONNECTING
}
every { HermesWsClient.disconnect() } returns Unit
every { mockDb.chatMessageDao() } returns mockDao
every { HermesDatabase.get(any()) } returns mockDb
@@ -385,14 +395,28 @@ class ChatViewModelTest {
@Test
fun testInitialStateAndConnection() =
runTest {
mockConnectionStatus.value = ConnectionStatus.CONNECTING
val viewModel = ChatViewModel(app, startCleanup = false)
advanceUntilIdle()
verify { HermesWsClient.connect() }
assertTrue(viewModel.uiState.value.isLoading)
assertFalse(viewModel.uiState.value.isLoading)
assertFalse(viewModel.uiState.value.isConnected)
}
@Test
fun testAlreadyConnectedOnLaunch_createsSession() =
runTest {
mockConnectionStatus.value = ConnectionStatus.CONNECTED
val viewModel = ChatViewModel(app, startCleanup = false)
advanceUntilIdle()
verify { HermesWsClient.send(WsMethods.SESSION_LIST, any(), any()) }
verify { HermesWsClient.send(WsMethods.SESSION_CREATE, any(), any()) }
assertFalse(viewModel.uiState.value.isLoading)
}
@Test
fun testGatewayReady_createsSessionIfNoneExists() =
runTest {
@@ -405,8 +429,8 @@ class ChatViewModelTest {
val state = viewModel.uiState.value
assertTrue(state.isConnected)
// Note: GatewayReady immediately calls createNewSession(), which sets isLoading = true and clears messages
assertTrue(state.isLoading)
// Note: GatewayReady immediately calls createNewSession(setLoading = false), which does NOT show a loading spinner
assertFalse(state.isLoading)
assertEquals(0, state.messages.size)
// Verify that list sessions and create session requests are triggered