fix(notification): disconnect idle background socket (#858)

* fix(notification): disconnect idle background socket

* fix(notification): clear rejected prompt state
This commit is contained in:
AlexDev_ authored and GitHub committed 2026-08-09 12:06:59 +03:00
1 parent 0db741edfe
commit 3b033a025b
8 files changed
+221 -40

No files matched your search

@@ -8,6 +8,7 @@ import com.m57.hermescontrol.data.remote.DashboardSessionTokenRefresher
import com.m57.hermescontrol.data.remote.NetworkMonitor
import com.m57.hermescontrol.data.remote.ServerEndpoint
import com.m57.hermescontrol.data.remote.buildFakePersistentCookieJar
import com.m57.hermescontrol.data.session.ActiveSessionHolder
import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkObject
@@ -20,6 +21,7 @@ import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withTimeout
import okhttp3.Response
import okhttp3.WebSocket
import okhttp3.WebSocketListener
import okhttp3.mockwebserver.MockResponse
@@ -86,6 +88,7 @@ class HermesWsClientTest {
(queueField.get(HermesWsClient) as java.util.concurrent.ConcurrentLinkedQueue<String>).clear()
HermesWsClient.disconnect(clearPendingMessages = true) // Ensure it starts clean
HermesWsClient.setAppForeground(true)
val acceptQueuedMessagesField = HermesWsClient::class.java.getDeclaredField("acceptQueuedMessages")
acceptQueuedMessagesField.isAccessible = true
(acceptQueuedMessagesField.get(HermesWsClient) as java.util.concurrent.atomic.AtomicBoolean).set(true)
@@ -821,6 +824,139 @@ class HermesWsClientTest {
assertEquals("1", (receivedEvent as WsEvent.RpcResult).id)
}
@Test
fun testBackgroundWithoutPendingWorkDisconnects() {
mockWebServer.enqueue(MockResponse().withWebSocketUpgrade(object : WebSocketListener() {}))
HermesWsClient.connect()
runBlocking { withTimeout(5000) { HermesWsClient.connectionStatus.first { it == ConnectionStatus.CONNECTED } } }
HermesWsClient.setAppForeground(false)
assertFalse(HermesWsClient.isConnected)
assertEquals(ConnectionStatus.DISCONNECTED, HermesWsClient.connectionStatus.value)
}
@Test
fun testBackgroundWithPendingReplyStaysConnected() {
mockWebServer.enqueue(MockResponse().withWebSocketUpgrade(object : WebSocketListener() {}))
HermesWsClient.connect()
runBlocking { withTimeout(5000) { HermesWsClient.connectionStatus.first { it == ConnectionStatus.CONNECTED } } }
HermesWsClient.sendMessage("session-1", "hello")
HermesWsClient.setAppForeground(false)
assertTrue(HermesWsClient.isConnected)
}
@Test
fun testRejectedPromptDisconnectsIdleBackgroundSocket() {
lateinit var serverSocket: WebSocket
val connectedLatch = CountDownLatch(1)
mockWebServer.enqueue(
MockResponse().withWebSocketUpgrade(
object : WebSocketListener() {
override fun onOpen(
webSocket: WebSocket,
response: Response,
) {
serverSocket = webSocket
connectedLatch.countDown()
}
},
),
)
HermesWsClient.connect()
assertTrue(connectedLatch.await(5, TimeUnit.SECONDS))
val requestId = HermesWsClient.sendMessage("deleted-session", "hello")
HermesWsClient.setAppForeground(false)
assertTrue(HermesWsClient.pendingReply)
serverSocket.send(
"""{"jsonrpc":"2.0","id":"$requestId","error":{"code":4001,"message":"Session not found"}}""",
)
runBlocking {
withTimeout(5000) {
HermesWsClient.connectionStatus.first { it == ConnectionStatus.DISCONNECTED }
}
}
assertFalse(HermesWsClient.pendingReply)
assertFalse(HermesWsClient.isConnected)
}
@Test
fun testBackgroundQueuedSendDisconnectsAfterFlush() {
val messageLatch = CountDownLatch(1)
mockWebServer.enqueue(
MockResponse().withWebSocketUpgrade(
object : WebSocketListener() {
override fun onMessage(
webSocket: WebSocket,
text: String,
) {
messageLatch.countDown()
}
},
),
)
HermesWsClient.setAppForeground(false)
HermesWsClient.send(WsMethods.SESSION_REDIRECT)
assertTrue(messageLatch.await(5, TimeUnit.SECONDS))
runBlocking {
withTimeout(5000) {
HermesWsClient.connectionStatus.first { it == ConnectionStatus.DISCONNECTED }
}
}
assertFalse(HermesWsClient.isConnected)
}
@Test
fun testMessageCompleteDisconnectsIdleBackgroundSocket() {
lateinit var serverSocket: WebSocket
val connectedLatch = CountDownLatch(1)
mockWebServer.enqueue(
MockResponse().withWebSocketUpgrade(
object : WebSocketListener() {
override fun onOpen(
webSocket: WebSocket,
response: Response,
) {
serverSocket = webSocket
connectedLatch.countDown()
}
},
),
)
HermesWsClient.connect()
assertTrue(connectedLatch.await(5, TimeUnit.SECONDS))
ActiveSessionHolder.set("runtime-session", "stored-session")
HermesWsClient.sendMessage("session-1", "hello")
HermesWsClient.setAppForeground(false)
val completeEvent =
"""
{"jsonrpc":"2.0","method":"event","params":{"type":"message.complete",
"payload":{"text":"done","session_id":"runtime-session"}}}
""".trimIndent()
val receivedEvent =
runBlocking {
withTimeout(5000) {
launch { serverSocket.send(completeEvent) }
HermesWsClient.events.first { it is WsEvent.MessageComplete } as WsEvent.MessageComplete
}
}
runBlocking {
withTimeout(5000) {
HermesWsClient.connectionStatus.first { it == ConnectionStatus.DISCONNECTED }
}
}
assertFalse(HermesWsClient.isConnected)
assertEquals("stored-session", receivedEvent.storedSessionId)
}
@Test
fun testDisconnect() {
val serverLatch = CountDownLatch(1)