feat: add setting to keep connection alive in background (#1196)

This commit is contained in:
M57 authored and GitHub committed 2026-09-19 18:28:48 +03:00
1 parent a61a1b9974
commit 759d59b9f9
19 files changed
+1155 -24

No files matched your search

@@ -18,6 +18,7 @@ class ServerStoreTest {
assertEquals("token", state.wsAuthParam)
assertEquals(1.0f, state.chatFontScale)
assertFalse(state.showModelProvider)
assertFalse(state.keepConnectedInBackground)
assertTrue(state.connectionProfiles.isEmpty())
assertNull(state.selectedProfileId)
}
@@ -77,4 +78,19 @@ class ServerStoreTest {
val updated = state.copy(showModelProvider = true)
assertTrue(updated.showModelProvider)
}
@Test
fun testKeepConnectedInBackgroundUpdate() {
val state = ServerStoreState()
assertFalse(state.keepConnectedInBackground)
val updated = state.copy(keepConnectedInBackground = true)
assertTrue(updated.keepConnectedInBackground)
}
@Test
fun testKeepConnectedInBackground_deserializationDefault() {
val json = kotlinx.serialization.json.Json { ignoreUnknownKeys = true }
val decoded = json.decodeFromString<ServerStoreState>("{}")
assertFalse(decoded.keepConnectedInBackground)
}
}
@@ -106,6 +106,7 @@ class HermesWsClientTest {
HermesWsClient.disconnect(clearPendingMessages = true) // Ensure it starts clean
HermesWsClient.releaseExternalActivityConnectionLease()
HermesWsClient.releaseBackgroundConnectionLease()
HermesWsClient.setAppForeground(true)
val acceptQueuedMessagesField = HermesWsClient::class.java.getDeclaredField("acceptQueuedMessages")
acceptQueuedMessagesField.isAccessible = true
@@ -115,6 +116,7 @@ class HermesWsClientTest {
@After
fun tearDown() {
HermesWsClient.releaseExternalActivityConnectionLease()
HermesWsClient.releaseBackgroundConnectionLease()
HermesWsClient.disconnect(clearPendingMessages = true)
// Wait a bit to allow internal OkHttp coroutines to clean up before shutting down MockWebServer
// Increased from 100ms for OkHttp 5.x — needs more time for the WS close handshake
@@ -973,6 +975,23 @@ class HermesWsClientTest {
assertEquals(ConnectionStatus.DISCONNECTED, HermesWsClient.connectionStatus.value)
}
@Test
fun testBackgroundConnectionLeaseKeepsIdleBackgroundSocketUntilReleased() {
mockWebServer.enqueue(MockResponse().withWebSocketUpgrade(object : WebSocketListener() {}))
HermesWsClient.connect()
runBlocking { withTimeout(5000) { HermesWsClient.connectionStatus.first { it == ConnectionStatus.CONNECTED } } }
HermesWsClient.acquireBackgroundConnectionLease()
HermesWsClient.setAppForeground(false)
assertTrue(HermesWsClient.isConnected)
HermesWsClient.releaseBackgroundConnectionLease()
assertFalse(HermesWsClient.isConnected)
assertEquals(ConnectionStatus.DISCONNECTED, HermesWsClient.connectionStatus.value)
}
@Test
fun testBackgroundWithPendingReplyStaysConnected() {
mockWebServer.enqueue(MockResponse().withWebSocketUpgrade(object : WebSocketListener() {}))
@@ -0,0 +1,410 @@
package com.m57.hermescontrol.notification
import android.util.Log
import com.m57.hermescontrol.data.local.AuthManager
import com.m57.hermescontrol.data.remote.NetworkMonitor
import com.m57.hermescontrol.data.ws.ConnectionStatus
import com.m57.hermescontrol.data.ws.HermesWsClient
import io.mockk.every
import io.mockk.mockkObject
import io.mockk.mockkStatic
import io.mockk.unmockkObject
import io.mockk.unmockkStatic
import io.mockk.verify
import kotlinx.coroutines.flow.MutableStateFlow
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
class BackgroundConnectionControllerTest {
@Before
fun setUp() {
mockkStatic(Log::class)
every { Log.w(any<String>(), any<String>(), any<Throwable>()) } returns 0
every { Log.w(any<String>(), any<String>()) } returns 0
every { Log.d(any<String>(), any<String>()) } returns 0
}
@After
fun tearDown() {
unmockkStatic(Log::class)
}
@Test
fun testOnAppPause_whenOptIn_acquiresLeaseAndRequestsStart() {
var leaseAcquired = false
var serviceStartRequested = false
var startActionInvoked = false
val snapshot =
BackgroundConnectionSnapshot(
appInForeground = true,
isDeparting = false,
keepConnectedOptIn = true,
pendingReply = false,
isEligibleForConnection = true,
status = ConnectionStatus.CONNECTED,
isAutoReconnect = true,
hasActiveNetwork = true,
)
val controller =
BackgroundConnectionController(
snapshotProvider = { isDeparting -> snapshot.copy(isDeparting = isDeparting) },
acquireLease = { leaseAcquired = true },
releaseLease = { leaseAcquired = false },
requestServiceStart = { action ->
serviceStartRequested = true
action()
},
)
controller.onAppPause {
startActionInvoked = true
}
assertTrue(leaseAcquired)
assertTrue(serviceStartRequested)
assertTrue(startActionInvoked)
}
@Test
fun testOnAppPause_whenStartThrowsException_rollsBackLeaseAndDoesNotRethrow() {
var leaseAcquired = false
var leaseReleased = false
val snapshot =
BackgroundConnectionSnapshot(
appInForeground = true,
isDeparting = false,
keepConnectedOptIn = true,
pendingReply = false,
isEligibleForConnection = true,
status = ConnectionStatus.CONNECTED,
isAutoReconnect = true,
hasActiveNetwork = true,
)
val controller =
BackgroundConnectionController(
snapshotProvider = { isDeparting -> snapshot.copy(isDeparting = isDeparting) },
acquireLease = { leaseAcquired = true },
releaseLease = { leaseReleased = true },
requestServiceStart = { _ ->
throw IllegalStateException("Background start not allowed")
},
)
// Must not rethrow into Activity.onPause, but must roll back acquired lease
controller.onAppPause {}
assertTrue(leaseAcquired)
assertTrue(leaseReleased)
}
@Test
fun testOnAppPause_whenOptInOffAndNoPendingReply_doesNothing() {
var leaseAcquired = false
var serviceStartRequested = false
val snapshot =
BackgroundConnectionSnapshot(
appInForeground = true,
isDeparting = false,
keepConnectedOptIn = false,
pendingReply = false,
isEligibleForConnection = true,
status = ConnectionStatus.CONNECTED,
isAutoReconnect = true,
hasActiveNetwork = true,
)
val controller =
BackgroundConnectionController(
snapshotProvider = { isDeparting -> snapshot.copy(isDeparting = isDeparting) },
acquireLease = { leaseAcquired = true },
releaseLease = { leaseAcquired = false },
requestServiceStart = { serviceStartRequested = true },
)
controller.onAppPause {}
assertFalse(leaseAcquired)
assertFalse(serviceStartRequested)
}
@Test
fun testOnAppPause_whenPendingReplyOnly_requestsServiceWithoutPersistentLease() {
var leaseAcquired = false
var serviceStartRequested = false
val snapshot =
BackgroundConnectionSnapshot(
appInForeground = true,
isDeparting = false,
keepConnectedOptIn = false,
pendingReply = true,
isEligibleForConnection = true,
status = ConnectionStatus.CONNECTED,
isAutoReconnect = true,
hasActiveNetwork = true,
)
val controller =
BackgroundConnectionController(
snapshotProvider = { isDeparting -> snapshot.copy(isDeparting = isDeparting) },
acquireLease = { leaseAcquired = true },
releaseLease = { leaseAcquired = false },
requestServiceStart = { serviceStartRequested = true },
)
controller.onAppPause {}
assertFalse(leaseAcquired)
assertTrue(serviceStartRequested)
}
@Test
fun testOnAppResume_releasesLeaseAndStopsService() {
var leaseReleased = false
var serviceStopped = false
val controller =
BackgroundConnectionController(
releaseLease = { leaseReleased = true },
requestServiceStop = { serviceStopped = true },
)
controller.onAppResume()
assertTrue(leaseReleased)
assertTrue(serviceStopped)
}
@Test
fun testOnReplyCompleted_whenOptInOn_keepsServiceAndUpdatesNotification() {
var serviceCompleted = false
var notifiedState: BackgroundNotificationState? = null
val snapshot =
BackgroundConnectionSnapshot(
appInForeground = false,
isDeparting = false,
keepConnectedOptIn = true,
pendingReply = true,
isEligibleForConnection = true,
status = ConnectionStatus.CONNECTED,
isAutoReconnect = true,
hasActiveNetwork = true,
)
val controller =
BackgroundConnectionController(
snapshotProvider = { snapshot },
requestServiceComplete = { serviceCompleted = true },
onNotificationStateChanged = { state -> notifiedState = state },
)
controller.onReplyCompleted(42L)
assertFalse(serviceCompleted)
assertEquals(BackgroundNotificationState.ConnectedInBackground, notifiedState)
}
@Test
fun testOnReplyCompleted_whenOptInOff_completesService() {
var completedGeneration: Long? = null
val snapshot =
BackgroundConnectionSnapshot(
appInForeground = false,
isDeparting = false,
keepConnectedOptIn = false,
pendingReply = true,
isEligibleForConnection = true,
status = ConnectionStatus.CONNECTED,
isAutoReconnect = true,
hasActiveNetwork = true,
)
val controller =
BackgroundConnectionController(
snapshotProvider = { snapshot },
requestServiceComplete = { gen -> completedGeneration = gen },
)
controller.onReplyCompleted(42L)
assertEquals(42L, completedGeneration)
}
@Test
fun testDefaultSnapshot_whenNotReady_returnsIneligibleSnapshotWithoutReadingServerStore() {
mockkObject(AuthManager)
mockkObject(ChatNotificationService)
mockkObject(HermesWsClient)
mockkObject(NetworkMonitor)
try {
every { AuthManager.initializationState } returns MutableStateFlow(AuthManager.InitializationState.Loading)
every { ChatNotificationService.isAppInForeground() } returns true
val snapshot = BackgroundConnectionController.defaultSnapshot()
assertFalse(snapshot.isEligibleForConnection)
assertFalse(snapshot.keepConnectedOptIn)
assertFalse(snapshot.pendingReply)
assertEquals(ConnectionStatus.DISCONNECTED, snapshot.status)
// Must NOT call any serverStore getters before Ready
verify(exactly = 0) { AuthManager.isGatedMode() }
verify(exactly = 0) { AuthManager.getToken() }
verify(exactly = 0) { AuthManager.isKeepConnectedInBackground() }
verify(exactly = 0) { AuthManager.isAutoReconnect() }
val decision = BackgroundConnectionPolicy.evaluate(snapshot)
assertFalse(decision.shouldHoldService)
assertFalse(decision.shouldHoldPersistentLease)
assertEquals(BackgroundNotificationState.None, decision.notificationState)
} finally {
unmockkObject(AuthManager)
unmockkObject(ChatNotificationService)
unmockkObject(HermesWsClient)
unmockkObject(NetworkMonitor)
}
}
@Test
fun testDefaultSnapshot_whenReadyGatedModeWithNullToken_isEligibleForConnection() {
mockkObject(AuthManager)
mockkObject(ChatNotificationService)
mockkObject(HermesWsClient)
mockkObject(NetworkMonitor)
try {
every { AuthManager.initializationState } returns MutableStateFlow(AuthManager.InitializationState.Ready)
every { AuthManager.isGatedMode() } returns true
every { AuthManager.getToken() } returns null
every { AuthManager.isKeepConnectedInBackground() } returns true
every { AuthManager.isAutoReconnect() } returns true
every { ChatNotificationService.isAppInForeground() } returns false
every { HermesWsClient.pendingReply } returns false
every { HermesWsClient.connectionStatus } returns MutableStateFlow(ConnectionStatus.CONNECTED)
every { NetworkMonitor.isConnected } returns MutableStateFlow(true)
val snapshot = BackgroundConnectionController.defaultSnapshot()
assertTrue(snapshot.isEligibleForConnection)
assertEquals(ConnectionStatus.CONNECTED, snapshot.status)
val decision = BackgroundConnectionPolicy.evaluate(snapshot)
assertTrue(decision.shouldHoldService)
assertTrue(decision.shouldHoldPersistentLease)
assertEquals(BackgroundNotificationState.ConnectedInBackground, decision.notificationState)
} finally {
unmockkObject(AuthManager)
unmockkObject(ChatNotificationService)
unmockkObject(HermesWsClient)
unmockkObject(NetworkMonitor)
}
}
@Test
fun testDefaultSnapshot_whenReadyNotGatedModeAndNullToken_isNotEligible() {
mockkObject(AuthManager)
mockkObject(ChatNotificationService)
mockkObject(HermesWsClient)
mockkObject(NetworkMonitor)
try {
every { AuthManager.initializationState } returns MutableStateFlow(AuthManager.InitializationState.Ready)
every { AuthManager.isGatedMode() } returns false
every { AuthManager.getToken() } returns null
every { AuthManager.isKeepConnectedInBackground() } returns true
every { AuthManager.isAutoReconnect() } returns true
every { ChatNotificationService.isAppInForeground() } returns false
every { HermesWsClient.pendingReply } returns false
every { HermesWsClient.connectionStatus } returns MutableStateFlow(ConnectionStatus.DISCONNECTED)
every { NetworkMonitor.isConnected } returns MutableStateFlow(true)
val snapshot = BackgroundConnectionController.defaultSnapshot()
assertFalse(snapshot.isEligibleForConnection)
val decision = BackgroundConnectionPolicy.evaluate(snapshot)
assertFalse(decision.shouldHoldService)
assertFalse(decision.shouldHoldPersistentLease)
assertEquals(BackgroundNotificationState.None, decision.notificationState)
} finally {
unmockkObject(AuthManager)
unmockkObject(ChatNotificationService)
unmockkObject(HermesWsClient)
unmockkObject(NetworkMonitor)
}
}
@Test
fun testDefaultSnapshot_whenAuthExpired_policyReleasesService() {
mockkObject(AuthManager)
mockkObject(ChatNotificationService)
mockkObject(HermesWsClient)
mockkObject(NetworkMonitor)
try {
every { AuthManager.initializationState } returns MutableStateFlow(AuthManager.InitializationState.Ready)
every { AuthManager.isGatedMode() } returns false
every { AuthManager.getToken() } returns "some-token"
every { AuthManager.isKeepConnectedInBackground() } returns true
every { AuthManager.isAutoReconnect() } returns true
every { ChatNotificationService.isAppInForeground() } returns false
every { HermesWsClient.pendingReply } returns false
every { HermesWsClient.connectionStatus } returns MutableStateFlow(ConnectionStatus.AUTH_EXPIRED)
every { NetworkMonitor.isConnected } returns MutableStateFlow(true)
val snapshot = BackgroundConnectionController.defaultSnapshot()
assertTrue(snapshot.isEligibleForConnection)
assertEquals(ConnectionStatus.AUTH_EXPIRED, snapshot.status)
val decision = BackgroundConnectionPolicy.evaluate(snapshot)
assertFalse(decision.shouldHoldService)
assertFalse(decision.shouldHoldPersistentLease)
assertEquals(BackgroundNotificationState.None, decision.notificationState)
} finally {
unmockkObject(AuthManager)
unmockkObject(ChatNotificationService)
unmockkObject(HermesWsClient)
unmockkObject(NetworkMonitor)
}
}
@Test
fun testDefaultSnapshot_whenAutoReconnectDisabledAndDisconnected_policyReleasesService() {
mockkObject(AuthManager)
mockkObject(ChatNotificationService)
mockkObject(HermesWsClient)
mockkObject(NetworkMonitor)
try {
every { AuthManager.initializationState } returns MutableStateFlow(AuthManager.InitializationState.Ready)
every { AuthManager.isGatedMode() } returns false
every { AuthManager.getToken() } returns "some-token"
every { AuthManager.isKeepConnectedInBackground() } returns true
every { AuthManager.isAutoReconnect() } returns false
every { ChatNotificationService.isAppInForeground() } returns false
every { HermesWsClient.pendingReply } returns false
every { HermesWsClient.connectionStatus } returns MutableStateFlow(ConnectionStatus.DISCONNECTED)
every { NetworkMonitor.isConnected } returns MutableStateFlow(true)
val snapshot = BackgroundConnectionController.defaultSnapshot()
assertTrue(snapshot.isEligibleForConnection)
assertEquals(ConnectionStatus.DISCONNECTED, snapshot.status)
assertFalse(snapshot.isAutoReconnect)
val decision = BackgroundConnectionPolicy.evaluate(snapshot)
assertFalse(decision.shouldHoldService)
assertFalse(decision.shouldHoldPersistentLease)
assertEquals(BackgroundNotificationState.None, decision.notificationState)
} finally {
unmockkObject(AuthManager)
unmockkObject(ChatNotificationService)
unmockkObject(HermesWsClient)
unmockkObject(NetworkMonitor)
}
}
}
@@ -0,0 +1,253 @@
package com.m57.hermescontrol.notification
import com.m57.hermescontrol.data.ws.ConnectionStatus
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
class BackgroundConnectionPolicyTest {
@Test
fun testForeground_hasNoServiceDemand() {
val state =
BackgroundConnectionSnapshot(
appInForeground = true,
isDeparting = false,
keepConnectedOptIn = true,
pendingReply = true,
isEligibleForConnection = true,
status = ConnectionStatus.CONNECTED,
isAutoReconnect = true,
hasActiveNetwork = true,
)
val decision = BackgroundConnectionPolicy.evaluate(state)
assertFalse(decision.shouldHoldService)
assertFalse(decision.shouldHoldPersistentLease)
assertEquals(BackgroundNotificationState.None, decision.notificationState)
}
@Test
fun testIneligible_winsOverAllDemand() {
val state =
BackgroundConnectionSnapshot(
appInForeground = false,
isDeparting = false,
keepConnectedOptIn = true,
pendingReply = true,
isEligibleForConnection = false,
status = ConnectionStatus.CONNECTED,
isAutoReconnect = true,
hasActiveNetwork = true,
)
val decision = BackgroundConnectionPolicy.evaluate(state)
assertFalse(decision.shouldHoldService)
assertFalse(decision.shouldHoldPersistentLease)
assertEquals(BackgroundNotificationState.None, decision.notificationState)
}
@Test
fun testBackground_optInOff_noPendingReply_noService() {
val state =
BackgroundConnectionSnapshot(
appInForeground = false,
isDeparting = false,
keepConnectedOptIn = false,
pendingReply = false,
isEligibleForConnection = true,
status = ConnectionStatus.CONNECTED,
isAutoReconnect = true,
hasActiveNetwork = true,
)
val decision = BackgroundConnectionPolicy.evaluate(state)
assertFalse(decision.shouldHoldService)
assertFalse(decision.shouldHoldPersistentLease)
assertEquals(BackgroundNotificationState.None, decision.notificationState)
}
@Test
fun testBackground_optInOff_pendingReply_holdsServiceRepliesOnly() {
val state =
BackgroundConnectionSnapshot(
appInForeground = false,
isDeparting = false,
keepConnectedOptIn = false,
pendingReply = true,
isEligibleForConnection = true,
status = ConnectionStatus.CONNECTED,
isAutoReconnect = true,
hasActiveNetwork = true,
)
val decision = BackgroundConnectionPolicy.evaluate(state)
assertTrue(decision.shouldHoldService)
assertFalse(decision.shouldHoldPersistentLease)
assertEquals(BackgroundNotificationState.WaitingForReplies, decision.notificationState)
}
@Test
fun testBackground_optInOn_noPendingReply_holdsServiceAndPersistentLease() {
val state =
BackgroundConnectionSnapshot(
appInForeground = false,
isDeparting = false,
keepConnectedOptIn = true,
pendingReply = false,
isEligibleForConnection = true,
status = ConnectionStatus.CONNECTED,
isAutoReconnect = true,
hasActiveNetwork = true,
)
val decision = BackgroundConnectionPolicy.evaluate(state)
assertTrue(decision.shouldHoldService)
assertTrue(decision.shouldHoldPersistentLease)
assertEquals(BackgroundNotificationState.ConnectedInBackground, decision.notificationState)
}
@Test
fun testBackground_optInOn_pendingReply_holdsBoth_notificationPrefersWaiting() {
val state =
BackgroundConnectionSnapshot(
appInForeground = false,
isDeparting = false,
keepConnectedOptIn = true,
pendingReply = true,
isEligibleForConnection = true,
status = ConnectionStatus.CONNECTED,
isAutoReconnect = true,
hasActiveNetwork = true,
)
val decision = BackgroundConnectionPolicy.evaluate(state)
assertTrue(decision.shouldHoldService)
assertTrue(decision.shouldHoldPersistentLease)
assertEquals(BackgroundNotificationState.WaitingForReplies, decision.notificationState)
}
@Test
fun testDeparting_holdsServiceBeforeBackgroundCompletes() {
val state =
BackgroundConnectionSnapshot(
appInForeground = true,
isDeparting = true,
keepConnectedOptIn = true,
pendingReply = false,
isEligibleForConnection = true,
status = ConnectionStatus.CONNECTED,
isAutoReconnect = true,
hasActiveNetwork = true,
)
val decision = BackgroundConnectionPolicy.evaluate(state)
assertTrue(decision.shouldHoldService)
assertTrue(decision.shouldHoldPersistentLease)
}
@Test
fun testBackground_noNetwork_whenAutoReconnectEnabled_showsWaitingForNetwork() {
val state =
BackgroundConnectionSnapshot(
appInForeground = false,
isDeparting = false,
keepConnectedOptIn = true,
pendingReply = false,
isEligibleForConnection = true,
status = ConnectionStatus.NO_NETWORK,
isAutoReconnect = true,
hasActiveNetwork = false,
)
val decision = BackgroundConnectionPolicy.evaluate(state)
assertTrue(decision.shouldHoldService)
assertEquals(BackgroundNotificationState.WaitingForNetwork, decision.notificationState)
}
@Test
fun testBackground_reconnecting_showsReconnecting() {
val state =
BackgroundConnectionSnapshot(
appInForeground = false,
isDeparting = false,
keepConnectedOptIn = true,
pendingReply = false,
isEligibleForConnection = true,
status = ConnectionStatus.RECONNECTING,
isAutoReconnect = true,
hasActiveNetwork = true,
)
val decision = BackgroundConnectionPolicy.evaluate(state)
assertTrue(decision.shouldHoldService)
assertEquals(BackgroundNotificationState.Reconnecting, decision.notificationState)
}
@Test
fun testAuthExpired_releasesServiceAndLease_showsNone() {
val state =
BackgroundConnectionSnapshot(
appInForeground = false,
isDeparting = false,
keepConnectedOptIn = true,
pendingReply = true,
isEligibleForConnection = true,
status = ConnectionStatus.AUTH_EXPIRED,
isAutoReconnect = true,
hasActiveNetwork = true,
)
val decision = BackgroundConnectionPolicy.evaluate(state)
assertFalse(decision.shouldHoldService)
assertFalse(decision.shouldHoldPersistentLease)
assertEquals(BackgroundNotificationState.None, decision.notificationState)
}
@Test
fun testDisconnected_whenAutoReconnectDisabled_releasesServiceAndLease_evenWithPendingReply() {
val state =
BackgroundConnectionSnapshot(
appInForeground = false,
isDeparting = false,
keepConnectedOptIn = true,
pendingReply = true,
isEligibleForConnection = true,
status = ConnectionStatus.DISCONNECTED,
isAutoReconnect = false,
hasActiveNetwork = true,
)
val decision = BackgroundConnectionPolicy.evaluate(state)
assertFalse(decision.shouldHoldService)
assertFalse(decision.shouldHoldPersistentLease)
assertEquals(BackgroundNotificationState.None, decision.notificationState)
}
@Test
fun testNoNetwork_whenAutoReconnectDisabled_releasesServiceAndLease() {
val state =
BackgroundConnectionSnapshot(
appInForeground = false,
isDeparting = false,
keepConnectedOptIn = true,
pendingReply = false,
isEligibleForConnection = true,
status = ConnectionStatus.NO_NETWORK,
isAutoReconnect = false,
hasActiveNetwork = false,
)
val decision = BackgroundConnectionPolicy.evaluate(state)
assertFalse(decision.shouldHoldService)
assertFalse(decision.shouldHoldPersistentLease)
assertEquals(BackgroundNotificationState.None, decision.notificationState)
}
@Test
fun testConnecting_whenAutoReconnectDisabled_allowsServiceAndLease() {
val state =
BackgroundConnectionSnapshot(
appInForeground = false,
isDeparting = false,
keepConnectedOptIn = true,
pendingReply = false,
isEligibleForConnection = true,
status = ConnectionStatus.CONNECTING,
isAutoReconnect = false,
hasActiveNetwork = true,
)
val decision = BackgroundConnectionPolicy.evaluate(state)
assertTrue(decision.shouldHoldService)
assertTrue(decision.shouldHoldPersistentLease)
assertEquals(BackgroundNotificationState.Connecting, decision.notificationState)
}
}
@@ -78,6 +78,7 @@ class SettingsViewModelTest {
every { AuthManager.isAssistantMessageTokensEnabled() } returns true
every { AuthManager.isTokensPerSecondEnabled() } returns true
every { AuthManager.isModelProviderShown() } returns false
every { AuthManager.isKeepConnectedInBackground() } returns false
every { AuthManager.getConnectionProfiles() } returns emptyList()
every { AuthManager.getSelectedProfileId() } answers { storedSelectedProfileId }
every { AuthManager.baseUrl() } returns "http://127.0.0.1:9119/"
@@ -95,6 +96,7 @@ class SettingsViewModelTest {
every { AuthManager.setAssistantMessageTokensEnabled(any()) } returns Unit
every { AuthManager.setTokensPerSecondEnabled(any()) } returns Unit
every { AuthManager.setModelProviderShown(any()) } returns Unit
every { AuthManager.setKeepConnectedInBackground(any()) } returns Unit
every { AuthManager.setSelectedProfileId(any()) } answers {
storedSelectedProfileId = firstArg()
}
@@ -362,4 +364,36 @@ class SettingsViewModelTest {
verify { AuthManager.setModelProviderShown(true) }
assertEquals(true, viewModel.uiState.value.isSaved)
}
@Test
fun testKeepConnectedInBackground_loadsAndToggles() =
runTest {
every { AuthManager.isKeepConnectedInBackground() } returns false
val viewModel = SettingsViewModel(ioDispatcher = testDispatcher)
testDispatcher.scheduler.advanceUntilIdle()
assertEquals(false, viewModel.uiState.value.keepConnectedInBackground)
viewModel.onKeepConnectedInBackgroundChange(true)
assertEquals(true, viewModel.uiState.value.keepConnectedInBackground)
verify { AuthManager.setKeepConnectedInBackground(true) }
viewModel.onKeepConnectedInBackgroundChange(false)
assertEquals(false, viewModel.uiState.value.keepConnectedInBackground)
verify { AuthManager.setKeepConnectedInBackground(false) }
}
@Test
fun testSave_persistsKeepConnectedInBackground() =
runTest {
val viewModel = SettingsViewModel(ioDispatcher = testDispatcher)
testDispatcher.scheduler.advanceUntilIdle()
viewModel.onKeepConnectedInBackgroundChange(true)
viewModel.save()
testDispatcher.scheduler.advanceUntilIdle()
verify { AuthManager.setKeepConnectedInBackground(true) }
assertEquals(true, viewModel.uiState.value.isSaved)
}
}