9 files changed
+491
-28
No files matched your search
@@ -204,4 +204,28 @@ class ChatSendStoreTest {
|
||||
assertEquals(PendingSendState.UNKNOWN, restored.state)
|
||||
assertEquals(0, restored.attempts)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun uncertainReceiptGhostsAreHiddenWithoutConfirmingChangedServerIdentity() {
|
||||
val local = ChatMessage("send-1", MessageRole.USER, "repeat", serverRowId = 10)
|
||||
val server = ChatMessage("rest-session-20", MessageRole.USER, "repeat", serverRowId = 20)
|
||||
val reply = ChatMessage("rest-session-21", MessageRole.ASSISTANT, "done", serverRowId = 21)
|
||||
val receipt = pending("send-1", "repeat").copy(userRowId = 10)
|
||||
val merged = mergeTranscriptWithLive(listOf(server, reply), listOf(local), preserveLiveIds = true)
|
||||
|
||||
assertEquals(
|
||||
listOf(server.id, reply.id),
|
||||
messagesWithoutUnconfirmedReceipts(merged, listOf(receipt)).map { it.id },
|
||||
)
|
||||
assertTrue(pendingSendIdsConfirmedByDurableAliases(merged, listOf(receipt)).isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun queuedBubblesAndConfirmedAliasesStayInTheTranscript() {
|
||||
val queued = ChatMessage("queued", MessageRole.USER, "later")
|
||||
val confirmed = ChatMessage("confirmed", MessageRole.USER, "delivered", restId = "rest-session-10")
|
||||
val pending = listOf(pending("queued", "later", PendingSendState.QUEUED), pending("confirmed", "delivered"))
|
||||
|
||||
assertEquals(listOf(queued, confirmed), messagesWithoutUnconfirmedReceipts(listOf(queued, confirmed), pending))
|
||||
}
|
||||
}
|
||||
@@ -3844,7 +3844,7 @@ class ChatViewModelTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun interruptWithUnknownDeliveryQueuesWithoutInterrupting() =
|
||||
fun interruptWithUnknownDeliverySubmitsWithoutReplayingEarlierPrompt() =
|
||||
runTest {
|
||||
val (viewModel, sessionId) = createViewModelWithSession()
|
||||
every { HermesWsClient.sendMessage(sessionId, "Uncertain", any(), false) } answers {
|
||||
@@ -3863,8 +3863,8 @@ class ChatViewModelTest {
|
||||
val replacement =
|
||||
viewModel.uiState.value.pendingSends
|
||||
.single { it.text == "Replacement" }
|
||||
assertEquals(BusySendMode.QUEUE, replacement.mode)
|
||||
assertEquals(PendingSendState.QUEUED, replacement.state)
|
||||
assertEquals(BusySendMode.CORRECT, replacement.mode)
|
||||
assertEquals(PendingSendState.SENDING, replacement.state)
|
||||
assertEquals(
|
||||
PendingSendState.UNKNOWN,
|
||||
viewModel.uiState.value.pendingSends
|
||||
@@ -3872,11 +3872,12 @@ class ChatViewModelTest {
|
||||
.state,
|
||||
)
|
||||
verify(exactly = 0) { HermesWsClient.send(WsMethods.SESSION_INTERRUPT, any(), any()) }
|
||||
verify(exactly = 0) { HermesWsClient.sendMessage(any(), "Replacement", any(), any()) }
|
||||
verify(exactly = 1) { HermesWsClient.sendMessage(any(), "Replacement", any(), any()) }
|
||||
verify(exactly = 1) { HermesWsClient.sendMessage(sessionId, "Uncertain", any(), any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun interruptWithAcceptedDeliveryQueuesWithoutInterrupting() =
|
||||
fun interruptWithAcceptedDeliverySubmitsWithoutReplayingEarlierPrompt() =
|
||||
runTest {
|
||||
val (viewModel, sessionId) = createViewModelWithSession()
|
||||
every { HermesWsClient.sendMessage(sessionId, "Accepted", any(), false) } answers {
|
||||
@@ -3895,8 +3896,8 @@ class ChatViewModelTest {
|
||||
val replacement =
|
||||
viewModel.uiState.value.pendingSends
|
||||
.single { it.text == "Replacement" }
|
||||
assertEquals(BusySendMode.QUEUE, replacement.mode)
|
||||
assertEquals(PendingSendState.QUEUED, replacement.state)
|
||||
assertEquals(BusySendMode.CORRECT, replacement.mode)
|
||||
assertEquals(PendingSendState.SENDING, replacement.state)
|
||||
assertEquals(
|
||||
PendingSendState.ACCEPTED,
|
||||
viewModel.uiState.value.pendingSends
|
||||
@@ -3904,7 +3905,8 @@ class ChatViewModelTest {
|
||||
.state,
|
||||
)
|
||||
verify(exactly = 0) { HermesWsClient.send(WsMethods.SESSION_INTERRUPT, any(), any()) }
|
||||
verify(exactly = 0) { HermesWsClient.sendMessage(any(), "Replacement", any(), any()) }
|
||||
verify(exactly = 1) { HermesWsClient.sendMessage(any(), "Replacement", any(), any()) }
|
||||
verify(exactly = 1) { HermesWsClient.sendMessage(sessionId, "Accepted", any(), any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -4149,7 +4151,7 @@ class ChatViewModelTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun sendNowWaitsForEarlierAcceptedTurnToReachRestHistory() =
|
||||
fun acceptedReceiptReleasesNextQueuedSubmissionWithoutWaitingForHistory() =
|
||||
runTest {
|
||||
val (viewModel, sessionId) = createViewModelWithSession()
|
||||
every { HermesWsClient.sendMessage(sessionId, "First", any(), true) } answers {
|
||||
@@ -4174,12 +4176,13 @@ class ChatViewModelTest {
|
||||
advanceUntilIdle()
|
||||
|
||||
assertEquals(
|
||||
PendingSendState.QUEUED,
|
||||
PendingSendState.SENDING,
|
||||
viewModel.uiState.value.pendingSends
|
||||
.single { it.id == second.id }
|
||||
.state,
|
||||
)
|
||||
verify(exactly = 0) { HermesWsClient.send(WsMethods.SESSION_INTERRUPT, any(), any()) }
|
||||
verify(exactly = 1) { HermesWsClient.sendMessage(sessionId, "Second", any(), true) }
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -10536,4 +10539,107 @@ class ChatViewModelTest {
|
||||
advanceUntilIdle()
|
||||
assertTrue(vm.uiState.value.canInterrupt)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun changedReceiptIdentityDoesNotBlockNewPromptsOrFalselyConfirmDelivery() =
|
||||
runTest {
|
||||
val store = ChatSendStore()
|
||||
val (vm, session) = createViewModelWithSession(sendStore = store)
|
||||
every { HermesWsClient.sendMessage(session, "probe-first", any(), any()) } answers {
|
||||
arg<((String) -> Unit)?>(2)?.invoke("probe-submit")
|
||||
"probe-submit"
|
||||
}
|
||||
assertTrue(vm.sendMessage("probe-first"))
|
||||
advanceUntilIdle()
|
||||
mockEventsFlow.emit(WsEvent.RpcResult("probe-submit", mapOf("status" to "streaming", "user_row_id" to 10)))
|
||||
advanceUntilIdle()
|
||||
coEvery { ApiClient.hermesApi.getSessionMessages(session, any(), any(), any(), any()) } returns
|
||||
Response.success(
|
||||
SessionMessagesResponse(
|
||||
messages =
|
||||
listOf(
|
||||
SessionMessage(id = 20, role = "user", content = JsonPrimitive("probe-first")),
|
||||
SessionMessage(id = 21, role = "assistant", content = JsonPrimitive("probe-reply")),
|
||||
),
|
||||
pagination = PaginationInfo(order = "latest"),
|
||||
),
|
||||
)
|
||||
mockEventsFlow.emit(WsEvent.MessageStart(session))
|
||||
mockEventsFlow.emit(WsEvent.MessageComplete("probe-reply", session))
|
||||
advanceUntilIdle()
|
||||
vm.syncCurrentSession()
|
||||
advanceUntilIdle()
|
||||
assertEquals(
|
||||
2,
|
||||
vm.uiState.value.messages
|
||||
.count { it.role == MessageRole.USER && it.content == "probe-first" },
|
||||
)
|
||||
assertEquals(
|
||||
"probe-first",
|
||||
vm.uiState.value.messages
|
||||
.last()
|
||||
.content,
|
||||
)
|
||||
assertEquals(PendingSendState.ACCEPTED, store.all().single().state)
|
||||
assertTrue(vm.uiState.value.isSessionReady)
|
||||
assertTrue(vm.sendMessage("probe-second"))
|
||||
advanceUntilIdle()
|
||||
assertTrue(
|
||||
vm.uiState.value.messages
|
||||
.any { it.content == "probe-second" },
|
||||
)
|
||||
assertEquals(PendingSendState.SENDING, store.all().single { it.text == "probe-second" }.state)
|
||||
assertNull(vm.uiState.value.errorMessage)
|
||||
verify(exactly = 1) { HermesWsClient.sendMessage(session, "probe-second", any(), any()) }
|
||||
verify(exactly = 1) { HermesWsClient.sendMessage(session, "probe-first", any(), any()) }
|
||||
assertEquals(PendingSendState.ACCEPTED, store.all().single { it.text == "probe-first" }.state)
|
||||
assertEquals(
|
||||
1,
|
||||
vm.transcriptState.value.messages
|
||||
.count { it.content == "probe-first" },
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun timeoutReleasesQueuedPromptWithoutReplayingUncertainSend() =
|
||||
runTest {
|
||||
val store = ChatSendStore()
|
||||
val (vm, session) = createViewModelWithSession(sendStore = store)
|
||||
every { HermesWsClient.sendMessage(session, "uncertain-first", any(), any()) } answers {
|
||||
arg<((String) -> Unit)?>(2)?.invoke("uncertain-submit")
|
||||
"uncertain-submit"
|
||||
}
|
||||
assertTrue(vm.sendMessage("uncertain-first"))
|
||||
advanceUntilIdle()
|
||||
assertTrue(vm.sendMessage("queued-second"))
|
||||
advanceUntilIdle()
|
||||
assertEquals(PendingSendState.QUEUED, store.all().single { it.text == "queued-second" }.state)
|
||||
vm.expireOutgoingRequest("uncertain-submit")
|
||||
advanceUntilIdle()
|
||||
|
||||
assertEquals(PendingSendState.UNKNOWN, store.all().single { it.text == "uncertain-first" }.state)
|
||||
verify(exactly = 1) { HermesWsClient.sendMessage(session, "queued-second", any(), true) }
|
||||
verify(exactly = 1) { HermesWsClient.sendMessage(session, "uncertain-first", any(), any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun dismissUnknownReceiptDoesNotResendAndCannotDismissInFlight() =
|
||||
runTest {
|
||||
val store = ChatSendStore()
|
||||
val (vm, session) = createViewModelWithSession(sendStore = store)
|
||||
assertTrue(vm.sendMessage("dismiss-first"))
|
||||
advanceUntilIdle()
|
||||
val receipt = store.all().single()
|
||||
vm.discardPendingSend(receipt.id)
|
||||
assertEquals(PendingSendState.SENDING, store.all().single().state)
|
||||
store.update(receipt.id) { it.copy(state = PendingSendState.UNKNOWN) }
|
||||
vm.discardPendingSend(receipt.id)
|
||||
advanceUntilIdle()
|
||||
assertTrue(store.all().isEmpty())
|
||||
assertTrue(
|
||||
vm.uiState.value.messages
|
||||
.none { it.id == receipt.id },
|
||||
)
|
||||
verify(exactly = 1) { HermesWsClient.sendMessage(session, "dismiss-first", any(), any()) }
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user