5 files changed
+177
-172
No files matched your search
@@ -9,11 +9,11 @@ import org.junit.Test
|
||||
|
||||
class ChatStreamingControllerTest {
|
||||
@Test
|
||||
fun flushPendingReasoning_flushesThrottledReasoningOnTransition() =
|
||||
fun flushPendingTransition_flushesThrottledReasoning() =
|
||||
runTest {
|
||||
// isTestEnvironment = false so the ~33ms throttle is live. The FIRST delta
|
||||
// always flushes (lastFlushMs == 0), but a SECOND delta within 33ms is held
|
||||
// in the buffer and only pushed out by flushPendingReasoning() (called at
|
||||
// in the buffer and only pushed out by flushPendingTransition() (called at
|
||||
// streaming transitions) — that is the exact drop we guard against.
|
||||
val uiState = MutableStateFlow(ChatUiState())
|
||||
val streamingState =
|
||||
@@ -38,13 +38,13 @@ class ChatStreamingControllerTest {
|
||||
assertEquals("A", streamingState.value.streamingMessage?.reasoningText)
|
||||
|
||||
// A streaming transition forces the buffered reasoning onto the message.
|
||||
controller.flushPendingReasoning()
|
||||
controller.flushPendingTransition()
|
||||
|
||||
assertEquals("AB", streamingState.value.streamingMessage?.reasoningText)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun flushPendingTokens_flushesThrottledTokensBeforeSeal() =
|
||||
fun flushPendingTransition_flushesThrottledTokensBeforeSeal() =
|
||||
runTest {
|
||||
// Issue #842: a delta landing <33ms before tool.start stays in the
|
||||
// throttled buffer; flushing the tokens before the reducer seals the
|
||||
@@ -73,7 +73,7 @@ class ChatStreamingControllerTest {
|
||||
|
||||
// The tool.start transition forces the buffered tokens out so the
|
||||
// sealed orphan carries the complete narration.
|
||||
controller.flushPendingTokens()
|
||||
controller.flushPendingTransition()
|
||||
|
||||
assertEquals(
|
||||
"tool's loaded! 🔍 now searchin' for the best hummus",
|
||||
@@ -95,7 +95,7 @@ class ChatStreamingControllerTest {
|
||||
// A fresh reasoning delta + flush must carry ONLY the new value, proving the
|
||||
// stale buffer was cleared and cannot be resurrected.
|
||||
controller.handleReasoningDelta(WsEvent.ReasoningDelta("Fresh reasoning", "session"))
|
||||
controller.flushPendingReasoning()
|
||||
controller.flushPendingTransition()
|
||||
|
||||
assertEquals("Fresh reasoning", streamingState.value.reasoningText)
|
||||
}
|
||||
@@ -168,6 +168,24 @@ class ChatStreamingControllerTest {
|
||||
assertEquals("First Second", streamingState.value.streamingMessage?.content)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun thinkingDelta_trailingFlushAndResetDoNotLeakIntoNextTurn() =
|
||||
runTest {
|
||||
val uiState = MutableStateFlow(ChatUiState())
|
||||
val streamingState = MutableStateFlow(StreamingState())
|
||||
val controller = controller(this, uiState, streamingState, isTestEnvironment = { false })
|
||||
|
||||
controller.handleThinkingDelta(WsEvent.ThinkingDelta("Think", "session"))
|
||||
controller.handleThinkingDelta(WsEvent.ThinkingDelta(" more", "session"))
|
||||
assertEquals("Think", streamingState.value.thinkingText)
|
||||
testScheduler.advanceTimeBy(35L)
|
||||
assertEquals("Think more", streamingState.value.thinkingText)
|
||||
|
||||
controller.resetStreaming()
|
||||
controller.handleThinkingDelta(WsEvent.ThinkingDelta("Fresh", "session"))
|
||||
assertEquals("Fresh", streamingState.value.thinkingText)
|
||||
}
|
||||
|
||||
private fun controller(
|
||||
scope: CoroutineScope,
|
||||
uiState: MutableStateFlow<ChatUiState>,
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
package com.m57.hermescontrol.ui.chat
|
||||
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
|
||||
class StreamingBufferTest {
|
||||
@Test
|
||||
fun channelsThrottleIndependentlyAndFlushCumulativeText() =
|
||||
runTest {
|
||||
var now = 100L
|
||||
val tokens = mutableListOf<String>()
|
||||
val reasoning = mutableListOf<String>()
|
||||
val tokenBuffer = StreamingBuffer(this, 33L, { now }, tokens::add)
|
||||
val reasoningBuffer = StreamingBuffer(this, 33L, { now }, reasoning::add)
|
||||
|
||||
tokenBuffer.append("A", flushImmediately = false)
|
||||
tokenBuffer.append("B", flushImmediately = false)
|
||||
reasoningBuffer.append("R", flushImmediately = false)
|
||||
assertEquals(listOf("A"), tokens)
|
||||
assertEquals(listOf("R"), reasoning)
|
||||
|
||||
now += 34
|
||||
testScheduler.advanceTimeBy(34L)
|
||||
assertEquals(listOf("A", "AB"), tokens)
|
||||
assertEquals(listOf("R"), reasoning)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun transitionFlushCancelsTrailingJobWithoutDroppingTheNextDelta() =
|
||||
runTest {
|
||||
val output = mutableListOf<String>()
|
||||
val buffer = StreamingBuffer(this, 33L, { 100L }, output::add)
|
||||
buffer.append("A", flushImmediately = false)
|
||||
buffer.append("B", flushImmediately = false)
|
||||
buffer.flushPending()
|
||||
testScheduler.advanceTimeBy(34L)
|
||||
assertEquals(listOf("A", "AB"), output)
|
||||
|
||||
buffer.append("C", flushImmediately = true)
|
||||
assertEquals(listOf("A", "AB", "ABC"), output)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun clearCancelsTrailingJobAndStartsFresh() =
|
||||
runTest {
|
||||
val output = mutableListOf<String>()
|
||||
val buffer = StreamingBuffer(this, 33L, { 100L }, output::add)
|
||||
buffer.append("Old", flushImmediately = false)
|
||||
buffer.append(" tail", flushImmediately = false)
|
||||
buffer.clear()
|
||||
testScheduler.advanceTimeBy(34L)
|
||||
buffer.append("Fresh", flushImmediately = false)
|
||||
assertEquals(listOf("Old", "Fresh"), output)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user