Skip to content

Commit 5ccbaa1

Browse files
committed
Surface rename failure as a Boolean instead of an exception
ChatHistoryRepository.renameChat now returns a Boolean: true when the store applied the rename, false when it could not (chat missing, JSON malformed). The ViewModel branches on that flag and emits the same Error outcome for both the false path and any thrown exception — the fragment shows one generic snackbar either way. Drops the check{} that turned a soft failure into IllegalStateException and removes the unused throwable field on RenameResult.Error.
1 parent ebe1873 commit 5ccbaa1

6 files changed

Lines changed: 36 additions & 25 deletions

File tree

duckchat/duckchat-impl/src/main/java/com/duckduckgo/duckchat/impl/history/ChatHistoryRepository.kt

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ interface ChatHistoryRepository {
3535

3636
suspend fun deleteChat(chatId: String)
3737
suspend fun deleteAllChats()
38-
suspend fun renameChat(chatId: String, newTitle: String)
38+
suspend fun renameChat(chatId: String, newTitle: String): Boolean
3939
}
4040

4141
@ContributesBinding(AppScope::class)
@@ -60,11 +60,8 @@ class RealChatHistoryRepository @Inject constructor(
6060
withContext(dispatchers.io()) { chatStore.deleteAllChats() }
6161
}
6262

63-
override suspend fun renameChat(chatId: String, newTitle: String) {
64-
withContext(dispatchers.io()) {
65-
check(chatStore.renameChat(chatId, newTitle)) { "Chat $chatId not found or unreadable" }
66-
}
67-
}
63+
override suspend fun renameChat(chatId: String, newTitle: String): Boolean =
64+
withContext(dispatchers.io()) { chatStore.renameChat(chatId, newTitle) }
6865

6966
private fun toChatHistoryItem(chat: DuckAiChat): ChatHistoryItem = ChatHistoryItem(
7067
chatId = chat.chatId,

duckchat/duckchat-impl/src/main/java/com/duckduckgo/duckchat/impl/history/RenameChatFragment.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ class RenameChatFragment : DuckDuckGoFragment(R.layout.fragment_rename_chat) {
8585
private fun onRenameResult(result: RenameChatViewModel.RenameResult) {
8686
when (result) {
8787
RenameChatViewModel.RenameResult.Success -> dismiss()
88-
is RenameChatViewModel.RenameResult.Error ->
88+
RenameChatViewModel.RenameResult.Error ->
8989
Snackbar.make(binding.root, R.string.duck_ai_chat_history_rename_error, Snackbar.LENGTH_SHORT).show()
9090
}
9191
}

duckchat/duckchat-impl/src/main/java/com/duckduckgo/duckchat/impl/history/RenameChatViewModel.kt

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,19 +40,19 @@ class RenameChatViewModel @Inject constructor(
4040

4141
fun onSaveClicked(chatId: String, newTitle: String) {
4242
appScope.launch {
43-
try {
44-
chatHistoryRepository.renameChat(chatId, newTitle.trim())
45-
resultChannel.trySend(RenameResult.Success)
43+
val outcome = try {
44+
if (chatHistoryRepository.renameChat(chatId, newTitle.trim())) RenameResult.Success else RenameResult.Error
4645
} catch (e: CancellationException) {
4746
throw e
4847
} catch (e: Exception) {
49-
resultChannel.trySend(RenameResult.Error(e))
48+
RenameResult.Error
5049
}
50+
resultChannel.trySend(outcome)
5151
}
5252
}
5353

5454
sealed interface RenameResult {
5555
data object Success : RenameResult
56-
data class Error(val throwable: Throwable) : RenameResult
56+
data object Error : RenameResult
5757
}
5858
}

duckchat/duckchat-impl/src/test/kotlin/com/duckduckgo/duckchat/impl/history/ChatHistoryRepositoryTest.kt

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import com.duckduckgo.duckchat.store.impl.DuckAiChatStore
2525
import kotlinx.coroutines.flow.MutableStateFlow
2626
import kotlinx.coroutines.test.runTest
2727
import org.junit.Assert.assertEquals
28+
import org.junit.Assert.assertFalse
2829
import org.junit.Assert.assertTrue
2930
import org.junit.Before
3031
import org.junit.Rule
@@ -166,19 +167,22 @@ class ChatHistoryRepositoryTest {
166167
}
167168

168169
@Test
169-
fun `renameChat delegates to store on success`() = runTest {
170+
fun `renameChat delegates to store and returns true on success`() = runTest {
170171
whenever(chatStore.renameChat("abc", "New")).thenReturn(true)
171172

172-
repository.renameChat("abc", "New")
173+
val result = repository.renameChat("abc", "New")
173174

175+
assertTrue(result)
174176
verify(chatStore).renameChat("abc", "New")
175177
}
176178

177-
@Test(expected = IllegalStateException::class)
178-
fun `renameChat throws when store reports the chat could not be updated`() = runTest {
179+
@Test
180+
fun `renameChat returns false when store reports the chat could not be updated`() = runTest {
179181
whenever(chatStore.renameChat("missing", "New")).thenReturn(false)
180182

181-
repository.renameChat("missing", "New")
183+
val result = repository.renameChat("missing", "New")
184+
185+
assertFalse(result)
182186
}
183187

184188
private fun chat(

duckchat/duckchat-impl/src/test/kotlin/com/duckduckgo/duckchat/impl/history/ChatHistoryViewModelTest.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -866,8 +866,9 @@ private class FakeChatHistoryRepository(
866866
source.value = emptyList()
867867
}
868868

869-
override suspend fun renameChat(chatId: String, newTitle: String) {
869+
override suspend fun renameChat(chatId: String, newTitle: String): Boolean {
870870
renamedChats += chatId to newTitle
871+
return true
871872
}
872873
}
873874

duckchat/duckchat-impl/src/test/kotlin/com/duckduckgo/duckchat/impl/history/RenameChatViewModelTest.kt

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ import kotlinx.coroutines.flow.Flow
2222
import kotlinx.coroutines.flow.MutableStateFlow
2323
import kotlinx.coroutines.test.runTest
2424
import org.junit.Assert.assertEquals
25-
import org.junit.Assert.assertTrue
2625
import org.junit.Rule
2726
import org.junit.Test
2827

@@ -44,32 +43,42 @@ class RenameChatViewModelTest {
4443
}
4544
}
4645

46+
@Test
47+
fun `onSaveClicked emits Error when repository reports failure`() = coroutineRule.testScope.runTest {
48+
repository.nextResult = false
49+
50+
viewModel.results.test {
51+
viewModel.onSaveClicked("chat-1", "New Title")
52+
53+
assertEquals(RenameChatViewModel.RenameResult.Error, awaitItem())
54+
}
55+
}
56+
4757
@Test
4858
fun `onSaveClicked emits Error when repository throws`() = coroutineRule.testScope.runTest {
49-
val failure = IllegalStateException("boom")
50-
repository.errorToThrow = failure
59+
repository.errorToThrow = IllegalStateException("boom")
5160

5261
viewModel.results.test {
5362
viewModel.onSaveClicked("chat-1", "New Title")
5463

55-
val result = awaitItem()
56-
assertTrue(result is RenameChatViewModel.RenameResult.Error)
57-
assertEquals(failure, (result as RenameChatViewModel.RenameResult.Error).throwable)
64+
assertEquals(RenameChatViewModel.RenameResult.Error, awaitItem())
5865
}
5966
}
6067
}
6168

6269
private class RecordingRenameRepository : ChatHistoryRepository {
6370
val renames: MutableList<Pair<String, String>> = mutableListOf()
6471
var errorToThrow: Throwable? = null
72+
var nextResult: Boolean = true
6573

6674
override fun observeChats(): Flow<List<ChatHistoryItem>> = MutableStateFlow(emptyList())
6775

6876
override suspend fun deleteChat(chatId: String) = Unit
6977
override suspend fun deleteAllChats() = Unit
7078

71-
override suspend fun renameChat(chatId: String, newTitle: String) {
79+
override suspend fun renameChat(chatId: String, newTitle: String): Boolean {
7280
errorToThrow?.let { throw it }
7381
renames += chatId to newTitle
82+
return nextResult
7483
}
7584
}

0 commit comments

Comments
 (0)