@@ -4,6 +4,7 @@ package soy.iko.opencode.ui.chat
44
55import kotlinx.coroutines.Dispatchers
66import kotlinx.coroutines.ExperimentalCoroutinesApi
7+import kotlinx.coroutines.launch
78import kotlinx.coroutines.test.StandardTestDispatcher
89import kotlinx.coroutines.test.TestScope
910import kotlinx.coroutines.test.resetMain
@@ -18,6 +19,7 @@ import org.junit.Assert.assertTrue
1819import org.junit.Before
1920import org.junit.Test
2021import soy.iko.opencode.data.model.Command
22+import soy.iko.opencode.data.model.MessageWithParts
2123import soy.iko.opencode.data.model.ModelInfo
2224import soy.iko.opencode.data.model.Permission
2325import soy.iko.opencode.data.model.PermissionResponse
@@ -30,6 +32,7 @@ import soy.iko.opencode.data.model.SessionDeleted
3032import soy.iko.opencode.data.model.SessionError
3133import soy.iko.opencode.data.model.SessionIdle
3234import soy.iko.opencode.data.model.SessionUpdated
35+import soy.iko.opencode.data.model.UserMessage
3336import soy.iko.opencode.data.network.EventStreamClient
3437import soy.iko.opencode.ui.testing.FakeAppContainer
3538import soy.iko.opencode.ui.testing.FakeEventStreamClient
@@ -478,4 +481,55 @@ class ChatViewModelTest {
478481 vm.clearError()
479482 assertNull(vm.error.value)
480483 }
484+
485+ @Test
486+ fun messagesFlowRetry_clearsStaleErrorOnSuccess() = testScope.runTest {
487+ val api = FakeOpencodeApiClient()
488+ val events = FakeEventStreamClient()
489+ // Use a controllable flow so we can simulate a re-emission after an error.
490+ val messagesFlow = kotlinx.coroutines.flow.MutableStateFlow(
491+ listOf(MessageWithParts(info = UserMessage(id = "m1", sessionID = "session1")))
492+ )
493+ val repo = FakeSessionRepository(api, events)
494+ repo.observeMessagesOverride = messagesFlow
495+ val container = makeContainer(api = api, events = events, repo = repo)
496+ val vm = makeVm(container)
497+ // Subscribe to messages so the flow's onEach is active and can clear errors.
498+ val collector = launch { vm.messages.collect { /* keep subscribed */ } }
499+ testScope.testScheduler.advanceUntilIdle()
500+ // Set an error via SSE
501+ events.fakeEvents.tryEmit(SessionError(SessionError.Props(sessionID = "session1")))
502+ testScope.testScheduler.advanceUntilIdle()
503+ assertNotNull(vm.error.value)
504+ // Trigger a new messages emission — onEach should clear the stale error
505+ messagesFlow.value = messagesFlow.value + MessageWithParts(
506+ info = UserMessage(id = "m2", sessionID = "session1"),
507+ )
508+ testScope.testScheduler.advanceUntilIdle()
509+ assertNull(vm.error.value)
510+ collector.cancel()
511+ }
512+
513+ // --- Connection switch clears session title ---
514+
515+ @Test
516+ fun connectionChange_clearsSessionTitle() = testScope.runTest {
517+ val events = FakeEventStreamClient()
518+ val container = makeContainer(events = events)
519+ val vm = makeVm(container)
520+ // Set the title via SSE event
521+ events.fakeEvents.tryEmit(
522+ SessionUpdated(SessionUpdated.Props(info = Session(id = "session1", title = "Old Title"))),
523+ )
524+ testScope.testScheduler.advanceUntilIdle()
525+ assertEquals("Old Title", vm.sessionTitle.value)
526+ // Switch to a new connection
527+ val api2 = FakeOpencodeApiClient()
528+ val events2 = FakeEventStreamClient()
529+ val repo2 = FakeSessionRepository(api2, events2)
530+ val conn2 = FakeOpencodeConnection(api2, events2, repo2, ServerProfile(id = "s2", label = "Server 2", baseUrl = "http://other"))
531+ container.setActiveConnection(conn2)
532+ testScope.testScheduler.advanceUntilIdle()
533+ assertNull(vm.sessionTitle.value)
534+ }
481535}