feat(settings): restore last session on startup (#1102) (#1105)

This commit is contained in:
M57 authored and GitHub committed 2026-09-13 16:45:04 +03:00
1 parent 7e8af8db01
commit 4246ae196d
16 files changed
+234 -4

No files matched your search

@@ -12,6 +12,8 @@ data class ServerStoreState(
val port: Int = 9119, val port: Int = 9119,
val baseUrl: String? = null, val baseUrl: String? = null,
val autoReconnect: Boolean = true, val autoReconnect: Boolean = true,
val restoreLastSession: Boolean = false,
val lastOpenedSessionIds: Map<String, String> = emptyMap(),
val themePreference: ThemePreference = ThemePreference.SYSTEM, val themePreference: ThemePreference = ThemePreference.SYSTEM,
val useDynamicColors: Boolean = false, val useDynamicColors: Boolean = false,
val themePreset: ThemePreset = ThemePreset.DEFAULT, val themePreset: ThemePreset = ThemePreset.DEFAULT,
@@ -616,6 +616,47 @@ object AuthManager {
serverStore.update { it.copy(autoReconnect = enabled) } serverStore.update { it.copy(autoReconnect = enabled) }
} }
// ── Restore last session on startup (issue #1102) ─────────────────────
private fun currentSessionScopeKey(): String {
val connId = getSelectedProfileId()?.takeIf { it.isNotBlank() } ?: DEFAULT_PROFILE_ID
val serverProfile = activeProfileId.value?.takeIf { it.isNotBlank() } ?: DEFAULT_PROFILE_ID
return "$connId:$serverProfile"
}
fun isRestoreLastSession(): Boolean = serverStore.getLatestState().restoreLastSession
fun setRestoreLastSession(enabled: Boolean) {
serverStore.update { it.copy(restoreLastSession = enabled) }
}
fun getLastOpenedSessionId(): String? = serverStore.getLatestState().lastOpenedSessionIds[currentSessionScopeKey()]
fun setLastOpenedSessionId(sessionId: String?) {
val key = currentSessionScopeKey()
serverStore.update { state ->
val updated =
if (sessionId != null) {
state.lastOpenedSessionIds + (key to sessionId)
} else {
state.lastOpenedSessionIds - key
}
state.copy(lastOpenedSessionIds = updated)
}
}
fun clearLastOpenedSessionId() {
setLastOpenedSessionId(null)
}
fun clearLastOpenedSessionIdsForConnection(connectionProfileId: String) {
serverStore.update { state ->
val prefix = "$connectionProfileId:"
val updated = state.lastOpenedSessionIds.filterKeys { !it.startsWith(prefix) }
state.copy(lastOpenedSessionIds = updated)
}
}
// ── Theme preference ────────────────────────────────────────────────── // ── Theme preference ──────────────────────────────────────────────────
fun getThemePreference(): ThemePreference = serverStore.getLatestState().themePreference fun getThemePreference(): ThemePreference = serverStore.getLatestState().themePreference
@@ -677,6 +677,13 @@ class ChatViewModel(
if (!initial.isNullOrBlank()) { if (!initial.isNullOrBlank()) {
initialSessionId = null initialSessionId = null
switchSession(initial) switchSession(initial)
} else if (AuthManager.isRestoreLastSession()) {
val restoredId = AuthManager.getLastOpenedSessionId()
if (!restoredId.isNullOrBlank()) {
switchSession(restoredId)
} else {
createNewSession(setLoading = false)
}
} else { } else {
createNewSession(setLoading = false) createNewSession(setLoading = false)
} }
@@ -1406,6 +1413,7 @@ class ChatViewModel(
userMessage: ChatMessage? = null, userMessage: ChatMessage? = null,
) { ) {
val dispatchGeneration = sessionGeneration val dispatchGeneration = sessionGeneration
AuthManager.setLastOpenedSessionId(storageSessionId)
val msgToPersist = val msgToPersist =
userMessage ?: ChatMessage( userMessage ?: ChatMessage(
role = MessageRole.USER, role = MessageRole.USER,
@@ -2356,6 +2364,7 @@ class ChatViewModel(
.find { it.id == sessionId } .find { it.id == sessionId }
?.title ?: "Hermes" ?.title ?: "Hermes"
val generation = resetSessionState(sessionId, title, isLoading = true) val generation = resetSessionState(sessionId, title, isLoading = true)
AuthManager.setLastOpenedSessionId(sessionId)
viewModelScope.launch { viewModelScope.launch {
// Warm-cache fast-path (desktop parity): paint the cached Room // Warm-cache fast-path (desktop parity): paint the cached Room
// transcript immediately so the screen never sits blank, then load // transcript immediately so the screen never sits blank, then load
@@ -2708,6 +2717,9 @@ class ChatViewModel(
} }
sessionGoneRecoveryInFlight = true sessionGoneRecoveryInFlight = true
cancelResumeRetry() cancelResumeRetry()
if (AuthManager.getLastOpenedSessionId() == sessionId) {
AuthManager.clearLastOpenedSessionId()
}
_uiState.update { _uiState.update {
it.copy( it.copy(
isLoading = false, isLoading = false,
@@ -2,6 +2,7 @@ package com.m57.hermescontrol.ui.sessions
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.m57.hermescontrol.data.local.AuthManager
import com.m57.hermescontrol.data.model.BulkDeleteRequest import com.m57.hermescontrol.data.model.BulkDeleteRequest
import com.m57.hermescontrol.data.model.PruneRequest import com.m57.hermescontrol.data.model.PruneRequest
import com.m57.hermescontrol.data.model.SessionInfo import com.m57.hermescontrol.data.model.SessionInfo
@@ -689,6 +690,9 @@ class SessionsViewModel(
} }
when (result) { when (result) {
is NetworkResult.Success -> { is NetworkResult.Success -> {
if (AuthManager.getLastOpenedSessionId() == sessionId) {
AuthManager.clearLastOpenedSessionId()
}
_uiState.update { _uiState.update {
val updatedSessions = it.sessions.filter { s -> s.id != sessionId } val updatedSessions = it.sessions.filter { s -> s.id != sessionId }
val newTotal = (it.total - 1).coerceAtLeast(0) val newTotal = (it.total - 1).coerceAtLeast(0)
@@ -739,6 +743,10 @@ class SessionsViewModel(
} }
when (result) { when (result) {
is NetworkResult.Success -> { is NetworkResult.Success -> {
val lastOpened = AuthManager.getLastOpenedSessionId()
if (lastOpened != null && ids.contains(lastOpened)) {
AuthManager.clearLastOpenedSessionId()
}
val deletedCount = result.data.deleted val deletedCount = result.data.deleted
val toastMsg = val toastMsg =
if (deletedCount > 0) { if (deletedCount > 0) {
@@ -113,7 +113,7 @@ fun SettingsScreen(
icon = Icons.Filled.Tune, icon = Icons.Filled.Tune,
label = stringResource(R.string.settings_sec_behavior), label = stringResource(R.string.settings_sec_behavior),
summary = summary =
if (state.autoReconnect) { if (state.autoReconnect || state.restoreLastSession) {
stringResource(R.string.settings_summary_on) stringResource(R.string.settings_summary_on)
} else { } else {
stringResource(R.string.settings_summary_off) stringResource(R.string.settings_summary_off)
@@ -227,6 +227,8 @@ internal fun SettingsBehaviorPage(
BehaviorSection( BehaviorSection(
autoReconnect = state.autoReconnect, autoReconnect = state.autoReconnect,
onAutoReconnectChange = viewModel::onAutoReconnectChange, onAutoReconnectChange = viewModel::onAutoReconnectChange,
restoreLastSession = state.restoreLastSession,
onRestoreLastSessionChange = viewModel::onRestoreLastSessionChange,
) )
} }
} }
@@ -28,6 +28,7 @@ data class SettingsUiState(
val transportWarning: String? = null, val transportWarning: String? = null,
val token: String = "", val token: String = "",
val autoReconnect: Boolean = true, val autoReconnect: Boolean = true,
val restoreLastSession: Boolean = false,
val themePreference: ThemePreference = ThemePreference.SYSTEM, val themePreference: ThemePreference = ThemePreference.SYSTEM,
val useDynamicColors: Boolean = false, val useDynamicColors: Boolean = false,
val themePreset: ThemePreset = ThemePreset.DEFAULT, val themePreset: ThemePreset = ThemePreset.DEFAULT,
@@ -72,6 +73,7 @@ class SettingsViewModel(
val baseUrl = AuthManager.getBaseUrl() val baseUrl = AuthManager.getBaseUrl()
val token = AuthManager.getToken() ?: "" val token = AuthManager.getToken() ?: ""
val autoReconnect = AuthManager.isAutoReconnect() val autoReconnect = AuthManager.isAutoReconnect()
val restoreLastSession = AuthManager.isRestoreLastSession()
val themePreference = AuthManager.getThemePreference() val themePreference = AuthManager.getThemePreference()
val useDynamicColors = AuthManager.isUseDynamicColors() val useDynamicColors = AuthManager.isUseDynamicColors()
val themePreset = AuthManager.getThemePreset() val themePreset = AuthManager.getThemePreset()
@@ -92,6 +94,7 @@ class SettingsViewModel(
transportWarning = transportWarning, transportWarning = transportWarning,
token = token, token = token,
autoReconnect = autoReconnect, autoReconnect = autoReconnect,
restoreLastSession = restoreLastSession,
themePreference = themePreference, themePreference = themePreference,
useDynamicColors = useDynamicColors, useDynamicColors = useDynamicColors,
themePreset = themePreset, themePreset = themePreset,
@@ -120,6 +123,7 @@ class SettingsViewModel(
val updatedProfiles = AuthManager.getConnectionProfiles().filter { it.id != profileId } val updatedProfiles = AuthManager.getConnectionProfiles().filter { it.id != profileId }
AuthManager.saveConnectionProfiles(updatedProfiles) AuthManager.saveConnectionProfiles(updatedProfiles)
AuthManager.setProfileToken(profileId, null) AuthManager.setProfileToken(profileId, null)
AuthManager.clearLastOpenedSessionIdsForConnection(profileId)
// Never leave selection null (issue #478): if the deleted profile was selected, // Never leave selection null (issue #478): if the deleted profile was selected,
// fall back to the default profile instead of clearing selection. // fall back to the default profile instead of clearing selection.
val reselectDefault = val reselectDefault =
@@ -283,6 +287,11 @@ class SettingsViewModel(
AuthManager.setAutoReconnect(enabled) AuthManager.setAutoReconnect(enabled)
} }
fun onRestoreLastSessionChange(enabled: Boolean) {
_uiState.update { it.copy(restoreLastSession = enabled, isSaved = false) }
AuthManager.setRestoreLastSession(enabled)
}
fun onThemeChange(theme: ThemePreference) { fun onThemeChange(theme: ThemePreference) {
_uiState.update { it.copy(themePreference = theme, isSaved = false) } _uiState.update { it.copy(themePreference = theme, isSaved = false) }
AuthManager.setThemePreference(theme) AuthManager.setThemePreference(theme)
@@ -323,6 +332,7 @@ class SettingsViewModel(
AuthManager.setToken(null) AuthManager.setToken(null)
AuthManager.setSessionCookie(null) AuthManager.setSessionCookie(null)
AuthManager.setWsAuthParam("token") AuthManager.setWsAuthParam("token")
AuthManager.clearLastOpenedSessionId()
HermesWsClient.disconnect(clearPendingMessages = true) HermesWsClient.disconnect(clearPendingMessages = true)
// Don't rebuild ApiClient here — let the navigation complete first // Don't rebuild ApiClient here — let the navigation complete first
} }
@@ -337,6 +347,7 @@ class SettingsViewModel(
AuthManager.setBaseUrl(normalized) AuthManager.setBaseUrl(normalized)
AuthManager.setToken(state.token) AuthManager.setToken(state.token)
AuthManager.setAutoReconnect(state.autoReconnect) AuthManager.setAutoReconnect(state.autoReconnect)
AuthManager.setRestoreLastSession(state.restoreLastSession)
// B6 (Jun 18 2026, kanban t_86e9be9b): persist theme choice so it // B6 (Jun 18 2026, kanban t_86e9be9b): persist theme choice so it
// survives a cold start (was previously dropped on save()). // survives a cold start (was previously dropped on save()).
AuthManager.setThemePreference(state.themePreference) AuthManager.setThemePreference(state.themePreference)
@@ -3,17 +3,19 @@ package com.m57.hermescontrol.ui.settings.components
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Switch import androidx.compose.material3.Switch
import androidx.compose.material3.SwitchDefaults import androidx.compose.material3.SwitchDefaults
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.m57.hermescontrol.R import com.m57.hermescontrol.R
import com.m57.hermescontrol.ui.settings.SectionCard import com.m57.hermescontrol.ui.settings.SectionCard
@@ -21,6 +23,8 @@ import com.m57.hermescontrol.ui.settings.SectionCard
internal fun BehaviorSection( internal fun BehaviorSection(
autoReconnect: Boolean, autoReconnect: Boolean,
onAutoReconnectChange: (Boolean) -> Unit, onAutoReconnectChange: (Boolean) -> Unit,
restoreLastSession: Boolean,
onRestoreLastSessionChange: (Boolean) -> Unit,
) { ) {
SectionCard { SectionCard {
Row( Row(
@@ -28,7 +32,7 @@ internal fun BehaviorSection(
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
Column { Column(modifier = Modifier.weight(1f)) {
Text( Text(
text = stringResource(R.string.settings_item_auto_reconnect), text = stringResource(R.string.settings_item_auto_reconnect),
style = MaterialTheme.typography.bodyLarge, style = MaterialTheme.typography.bodyLarge,
@@ -51,5 +55,38 @@ internal fun BehaviorSection(
), ),
) )
} }
Spacer(modifier = Modifier.height(12.dp))
HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant)
Spacer(modifier = Modifier.height(12.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
Column(modifier = Modifier.weight(1f)) {
Text(
text = stringResource(R.string.settings_item_restore_session),
style = MaterialTheme.typography.bodyLarge,
)
Text(
text = stringResource(R.string.settings_desc_restore_session),
style =
MaterialTheme.typography.bodySmall.copy(
color = MaterialTheme.colorScheme.onSurfaceVariant,
),
)
}
Switch(
checked = restoreLastSession,
onCheckedChange = onRestoreLastSessionChange,
colors =
SwitchDefaults.colors(
checkedThumbColor = MaterialTheme.colorScheme.primary,
checkedTrackColor = MaterialTheme.colorScheme.primaryContainer,
),
)
}
} }
} }
+2
View File
@@ -177,6 +177,8 @@
<string name="settings_sec_behavior">السلوك</string> <string name="settings_sec_behavior">السلوك</string>
<string name="settings_item_auto_reconnect">إعادة الاتصال التلقائي</string> <string name="settings_item_auto_reconnect">إعادة الاتصال التلقائي</string>
<string name="settings_desc_auto_reconnect">إعادة الاتصال تلقائيًا عند انقطاعه</string> <string name="settings_desc_auto_reconnect">إعادة الاتصال تلقائيًا عند انقطاعه</string>
<string name="settings_item_restore_session">استعادة الجلسة الأخيرة</string>
<string name="settings_desc_restore_session">إعادة فتح أحدث جلسة محادثة عند بدء التشغيل</string>
<string name="settings_sec_appearance">المظهر</string> <string name="settings_sec_appearance">المظهر</string>
<string name="settings_item_theme">السمة</string> <string name="settings_item_theme">السمة</string>
<string name="settings_sec_chat">المحادثة</string> <string name="settings_sec_chat">المحادثة</string>
+2
View File
@@ -180,6 +180,8 @@
<string name="settings_sec_behavior">동작</string> <string name="settings_sec_behavior">동작</string>
<string name="settings_item_auto_reconnect">자동 재연결</string> <string name="settings_item_auto_reconnect">자동 재연결</string>
<string name="settings_desc_auto_reconnect">연결 끊김 시 자동 재연결</string> <string name="settings_desc_auto_reconnect">연결 끊김 시 자동 재연결</string>
<string name="settings_item_restore_session">마지막 세션 복원</string>
<string name="settings_desc_restore_session">시작 시 가장 최근 채팅 세션을 다시 엽니다</string>
<string name="settings_sec_appearance">테마</string> <string name="settings_sec_appearance">테마</string>
<string name="settings_item_theme">테마</string> <string name="settings_item_theme">테마</string>
<string name="settings_sec_chat">채팅</string> <string name="settings_sec_chat">채팅</string>
+2
View File
@@ -177,6 +177,8 @@
<string name="settings_sec_behavior">行为</string> <string name="settings_sec_behavior">行为</string>
<string name="settings_item_auto_reconnect">自动重连</string> <string name="settings_item_auto_reconnect">自动重连</string>
<string name="settings_desc_auto_reconnect">断开后自动重新连接</string> <string name="settings_desc_auto_reconnect">断开后自动重新连接</string>
<string name="settings_item_restore_session">恢复上次会话</string>
<string name="settings_desc_restore_session">启动时重新打开最近的聊天会话</string>
<string name="settings_sec_appearance">外观</string> <string name="settings_sec_appearance">外观</string>
<string name="settings_item_theme">主题</string> <string name="settings_item_theme">主题</string>
<string name="settings_sec_chat">对话</string> <string name="settings_sec_chat">对话</string>
+2
View File
@@ -201,6 +201,8 @@
<string name="settings_sec_behavior">Behavior</string> <string name="settings_sec_behavior">Behavior</string>
<string name="settings_item_auto_reconnect">Auto-Reconnect</string> <string name="settings_item_auto_reconnect">Auto-Reconnect</string>
<string name="settings_desc_auto_reconnect">Automatically reconnect on disconnect</string> <string name="settings_desc_auto_reconnect">Automatically reconnect on disconnect</string>
<string name="settings_item_restore_session">Restore Last Session</string>
<string name="settings_desc_restore_session">Reopen the most recent chat session on startup</string>
<string name="settings_sec_appearance">Appearance</string> <string name="settings_sec_appearance">Appearance</string>
<string name="settings_item_theme">Theme</string> <string name="settings_item_theme">Theme</string>
<string name="settings_sec_chat">Chat</string> <string name="settings_sec_chat">Chat</string>
@@ -90,6 +90,10 @@ class ChatUpdateCommandTest {
every { AuthManager.isTypingEffectEnabled() } returns true every { AuthManager.isTypingEffectEnabled() } returns true
every { AuthManager.getTypingEffectDelayMs() } returns 30 every { AuthManager.getTypingEffectDelayMs() } returns 30
every { AuthManager.isAutoReconnect() } returns false every { AuthManager.isAutoReconnect() } returns false
every { AuthManager.isRestoreLastSession() } returns false
every { AuthManager.getLastOpenedSessionId() } returns null
every { AuthManager.setLastOpenedSessionId(any()) } returns Unit
every { AuthManager.clearLastOpenedSessionId() } returns Unit
every { HermesWsClient.events } returns mockEventsFlow every { HermesWsClient.events } returns mockEventsFlow
every { HermesWsClient.connectionStatus } returns mockConnectionStatus every { HermesWsClient.connectionStatus } returns mockConnectionStatus
every { HermesWsClient.connect() } answers { every { HermesWsClient.connect() } answers {
@@ -164,6 +164,10 @@ class ChatViewModelTest {
every { AuthManager.isTypingEffectEnabled() } returns true every { AuthManager.isTypingEffectEnabled() } returns true
every { AuthManager.getTypingEffectDelayMs() } returns 30 every { AuthManager.getTypingEffectDelayMs() } returns 30
every { AuthManager.isAutoReconnect() } returns false every { AuthManager.isAutoReconnect() } returns false
every { AuthManager.isRestoreLastSession() } returns false
every { AuthManager.getLastOpenedSessionId() } returns null
every { AuthManager.setLastOpenedSessionId(any()) } returns Unit
every { AuthManager.clearLastOpenedSessionId() } returns Unit
every { HermesWsClient.events } returns mockEventsFlow every { HermesWsClient.events } returns mockEventsFlow
every { HermesWsClient.connectionStatus } returns mockConnectionStatus every { HermesWsClient.connectionStatus } returns mockConnectionStatus
every { HermesWsClient.connect() } answers { every { HermesWsClient.connect() } answers {
@@ -5083,4 +5087,75 @@ class ChatViewModelTest {
assertTrue(updated.logs.any { it.text.contains("Stopped subagent") }) assertTrue(updated.logs.any { it.text.contains("Stopped subagent") })
io.mockk.verify { HermesWsClient.sendRedirect(sessionId, "/stop sub-123", any()) } io.mockk.verify { HermesWsClient.sendRedirect(sessionId, "/stop sub-123", any()) }
} }
@Test
fun testHandleGatewayReady_withRestoreLastSessionEnabled_resumesStoredSession() =
runTest {
every { AuthManager.isRestoreLastSession() } returns true
every { AuthManager.getLastOpenedSessionId() } returns "session-stored-999"
val resumeParamsSlot = slot<Map<String, Any>>()
every {
HermesWsClient.send(WsMethods.SESSION_RESUME, capture(resumeParamsSlot), any())
} answers {
"req-resume-test"
}
val viewModel = createViewModel()
advanceUntilIdle()
mockConnectionStatus.value = ConnectionStatus.CONNECTED
mockEventsFlow.emit(WsEvent.GatewayReady(null))
advanceUntilIdle()
io.mockk.verify {
HermesWsClient.send(
WsMethods.SESSION_RESUME,
any(),
any(),
)
}
assertEquals("session-stored-999", resumeParamsSlot.captured["session_id"])
}
@Test
fun testHandleGatewayReady_withRestoreLastSessionDisabled_createsNewSession() =
runTest {
every { AuthManager.isRestoreLastSession() } returns false
every { AuthManager.getLastOpenedSessionId() } returns "session-stored-999"
val viewModel = createViewModel()
advanceUntilIdle()
mockConnectionStatus.value = ConnectionStatus.CONNECTED
mockEventsFlow.emit(WsEvent.GatewayReady(null))
advanceUntilIdle()
io.mockk.verify {
HermesWsClient.send(
WsMethods.SESSION_CREATE,
any(),
any(),
)
}
io.mockk.verify(exactly = 0) {
HermesWsClient.send(
WsMethods.SESSION_RESUME,
any(),
any(),
)
}
}
@Test
fun testSwitchSession_updatesLastOpenedSessionId() =
runTest {
val (viewModel, _) = createViewModelWithSession()
advanceUntilIdle()
viewModel.switchSession("session-other-456")
advanceUntilIdle()
io.mockk.verify { AuthManager.setLastOpenedSessionId("session-other-456") }
}
} }
@@ -95,6 +95,10 @@ class SlashCommandDispatchRpcTest {
every { AuthManager.isTypingEffectEnabled() } returns true every { AuthManager.isTypingEffectEnabled() } returns true
every { AuthManager.getTypingEffectDelayMs() } returns 30 every { AuthManager.getTypingEffectDelayMs() } returns 30
every { AuthManager.isAutoReconnect() } returns false every { AuthManager.isAutoReconnect() } returns false
every { AuthManager.isRestoreLastSession() } returns false
every { AuthManager.getLastOpenedSessionId() } returns null
every { AuthManager.setLastOpenedSessionId(any()) } returns Unit
every { AuthManager.clearLastOpenedSessionId() } returns Unit
every { HermesWsClient.events } returns mockEventsFlow every { HermesWsClient.events } returns mockEventsFlow
every { HermesWsClient.connectionStatus } returns mockConnectionStatus every { HermesWsClient.connectionStatus } returns mockConnectionStatus
every { HermesWsClient.connect() } answers { every { HermesWsClient.connect() } answers {
@@ -63,6 +63,10 @@ class SettingsViewModelTest {
every { AuthManager.getAppLanguage() } returns "system" every { AuthManager.getAppLanguage() } returns "system"
every { AuthManager.getToken() } returns "" every { AuthManager.getToken() } returns ""
every { AuthManager.isAutoReconnect() } returns true every { AuthManager.isAutoReconnect() } returns true
every { AuthManager.isRestoreLastSession() } returns false
every { AuthManager.setRestoreLastSession(any()) } returns Unit
every { AuthManager.clearLastOpenedSessionIdsForConnection(any()) } returns Unit
every { AuthManager.clearLastOpenedSessionId() } returns Unit
every { AuthManager.getThemePreference() } returns ThemePreference.SYSTEM every { AuthManager.getThemePreference() } returns ThemePreference.SYSTEM
every { AuthManager.isUseDynamicColors() } returns true every { AuthManager.isUseDynamicColors() } returns true
every { AuthManager.getThemePreset() } returns ThemePreset.DEFAULT every { AuthManager.getThemePreset() } returns ThemePreset.DEFAULT
@@ -253,4 +257,26 @@ class SettingsViewModelTest {
verify { AuthManager.setSelectedProfileId(any()) } verify { AuthManager.setSelectedProfileId(any()) }
assertEquals(true, viewModel.uiState.value.navigateToLogin) assertEquals(true, viewModel.uiState.value.navigateToLogin)
} }
@Test
fun testRestoreLastSession_loadsAndUpdatesState() {
every { AuthManager.isRestoreLastSession() } returns true
val viewModel = createViewModel()
assertEquals(true, viewModel.uiState.value.restoreLastSession)
viewModel.onRestoreLastSessionChange(false)
assertEquals(false, viewModel.uiState.value.restoreLastSession)
verify { AuthManager.setRestoreLastSession(false) }
}
@Test
fun testDeleteProfile_clearsStoredSessionIdsForConnection() {
val viewModel = createViewModel()
viewModel.deleteProfile("prof-1")
testDispatcher.scheduler.advanceUntilIdle()
verify { AuthManager.clearLastOpenedSessionIdsForConnection("prof-1") }
}
} }