feat: add setting to keep connection alive in background (#1196)
This commit is contained in:
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() {}))
|
||||
|
||||
+410
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+253
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user