Fix WebSocket events being silently dropped on buffer overflow (#313)

* Fix WebSocket events being silently dropped on buffer overflow

Increases the MutableSharedFlow `extraBufferCapacity` from 64 to 256
and changes the `onBufferOverflow` policy to `BufferOverflow.DROP_OLDEST`
in `HermesWsClient.kt`. This fixes an issue where the slow collection
(e.g., due to DB writes) caused the buffer to fill up and silently drop
incoming real-time WebSocket events.

By setting `DROP_OLDEST`, `tryEmit()` remains functional without
suspending, allowing real-time events to be retained and prioritized.

Fixes #275

* Fix ktlint formatting issues in HermesWsClient.kt

The formatting changes adhere to ktlint rules required by the CI pipeline:
- Removed a trailing comma in MutableSharedFlow initialization to avoid multiline expression wrapping errors.
- Sorted imports to be in lexicographical order.

These formatting changes fix the failing CI check.

Fixes #275

* Fix WebSocket events being silently dropped on buffer overflow

Increases the MutableSharedFlow `extraBufferCapacity` from 64 to 256
and changes the `onBufferOverflow` policy to `BufferOverflow.DROP_OLDEST`
in `HermesWsClient.kt`. This fixes an issue where the slow collection
(e.g., due to DB writes) caused the buffer to fill up and silently drop
incoming real-time WebSocket events.

By setting `DROP_OLDEST`, `tryEmit()` remains functional without
suspending, allowing real-time events to be retained and prioritized.

Fixes #275

* Fix WebSocket events being silently dropped on buffer overflow

Increases the MutableSharedFlow extraBufferCapacity from 64 to 256
and changes the onBufferOverflow policy to BufferOverflow.DROP_OLDEST
in HermesWsClient.kt. This fixes an issue where the slow collection
(e.g., due to DB writes) caused the buffer to fill up and silently drop
incoming real-time WebSocket events.

By setting DROP_OLDEST, tryEmit() remains functional without
suspending, allowing real-time events to be retained and prioritized.

Fixes #275

* Fix WebSocket events being silently dropped on buffer overflow

Increases the MutableSharedFlow extraBufferCapacity from 64 to 256
and changes the onBufferOverflow policy to BufferOverflow.DROP_OLDEST
in HermesWsClient.kt. This fixes an issue where the slow collection
(e.g., due to DB writes) caused the buffer to fill up and silently drop
incoming real-time WebSocket events.

By setting DROP_OLDEST, tryEmit() remains functional without
suspending, allowing real-time events to be retained and prioritized.

Fixes #275

* Fix WebSocket events being silently dropped on buffer overflow

Increases the MutableSharedFlow extraBufferCapacity from 64 to 256
and changes the onBufferOverflow policy to BufferOverflow.DROP_OLDEST
in HermesWsClient.kt. This fixes an issue where the slow collection
(e.g., due to DB writes) caused the buffer to fill up and silently drop
incoming real-time WebSocket events.

By setting DROP_OLDEST, tryEmit() remains functional without
suspending, allowing real-time events to be retained and prioritized.

Fixes #275

---------

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
This commit is contained in:
M57andM57 authored and GitHub committed 2026-06-23 22:13:55 +03:00
1 parent 2c6e437a5b
commit eb71ae3a46
41 files changed
+666 -246

No files matched your search

@@ -2,7 +2,6 @@ package com.m57.hermescontrol.notification
import org.junit.Assert.assertEquals
import org.junit.Test
import java.lang.reflect.Modifier
/**
* Unit tests for [ChatNotificationService] companion object — specifically
@@ -25,11 +24,13 @@ class ChatNotificationServiceTest {
.getDeclaredField("isAppInForeground")
field.isAccessible = true
val atomicBoolean = field.get(null) as java.util.concurrent.atomic.AtomicBoolean
// Reset to known state
field.setBoolean(null, false)
atomicBoolean.set(false)
ChatNotificationService.setAppForeground(true)
assertEquals("flag should be true after setAppForeground(true)", true, field.getBoolean(null))
assertEquals("flag should be true after setAppForeground(true)", true, atomicBoolean.get())
}
@Test
@@ -39,11 +40,13 @@ class ChatNotificationServiceTest {
.getDeclaredField("isAppInForeground")
field.isAccessible = true
val atomicBoolean = field.get(null) as java.util.concurrent.atomic.AtomicBoolean
// Set to known state
field.setBoolean(null, true)
atomicBoolean.set(true)
ChatNotificationService.setAppForeground(false)
assertEquals("flag should be false after setAppForeground(false)", false, field.getBoolean(null))
assertEquals("flag should be false after setAppForeground(false)", false, atomicBoolean.get())
}
@Test
@@ -53,26 +56,17 @@ class ChatNotificationServiceTest {
.getDeclaredField("isAppInForeground")
field.isAccessible = true
// Ensure it's in a clean state
field.setBoolean(null, false)
val atomicBoolean = field.get(null) as java.util.concurrent.atomic.AtomicBoolean
assertEquals("initial value should be false", false, field.getBoolean(null))
// Ensure it's in a clean state
atomicBoolean.set(false)
assertEquals("initial value should be false", false, atomicBoolean.get())
}
@Test
fun `isAppInForeground field is volatile for thread safety`() {
val field =
ChatNotificationService::class.java
.getDeclaredField("isAppInForeground")
val modifiers = field.modifiers
assertEquals(
"isAppInForeground must be @Volatile — it is read from a background " +
"coroutine (event collection) and written from the UI thread " +
"(setAppForeground called from ChatScreen lifecycle)",
true,
Modifier.isVolatile(modifiers),
)
// Obsolete test as the type is now AtomicBoolean which guarantees thread safety implicitly
}
@Test
@@ -82,10 +76,12 @@ class ChatNotificationServiceTest {
.getDeclaredField("isAppInForeground")
field.isAccessible = true
val atomicBoolean = field.get(null) as java.util.concurrent.atomic.AtomicBoolean
// Set true twice
ChatNotificationService.setAppForeground(true)
ChatNotificationService.setAppForeground(true)
assertEquals("flag should remain true after consecutive setAppForeground(true)", true, field.getBoolean(null))
assertEquals("flag should remain true after consecutive setAppForeground(true)", true, atomicBoolean.get())
// Set false twice
ChatNotificationService.setAppForeground(false)
@@ -93,7 +89,7 @@ class ChatNotificationServiceTest {
assertEquals(
"flag should remain false after consecutive setAppForeground(false)",
false,
field.getBoolean(null),
atomicBoolean.get(),
)
}
}
@@ -252,6 +252,27 @@ class NotificationReplyReceiverTest {
verify { mockPendingResult.finish() }
}
@Test
fun `Room hang triggers timeout and finishes pending result`() {
givenValidReply("session-abc", "Hello")
// Make upsert hang indefinitely
coEvery { mockDao.upsert(any<ChatMessageEntity>()) } coAnswers {
kotlinx.coroutines.delay(10000)
}
receiver.onReceive(mockContext, mockIntent)
// Wait longer than the 5-second timeout, but less than the 10-second delay
Thread.sleep(6000)
// Timeout should be caught in catch block and logged
verify { android.util.Log.e("NotificationReply", any<String>(), any()) }
// Pending result must still finish after timeout
verify { mockPendingResult.finish() }
}
// ── Helpers ────────────────────────────────────────────────────────────
private fun givenValidReply(