diff --git a/app/src/main/java/dev/kaizen/app/chat/ChatScreen.kt b/app/src/main/java/dev/kaizen/app/chat/ChatScreen.kt index ebe9ae1..7397423 100644 --- a/app/src/main/java/dev/kaizen/app/chat/ChatScreen.kt +++ b/app/src/main/java/dev/kaizen/app/chat/ChatScreen.kt @@ -118,6 +118,7 @@ fun ChatScreen( // Conversation persistence: sidebar list from Room (instant), active conversation id var conversationId by remember { mutableStateOf(null) } + var viewingLockedChat by remember { mutableStateOf(false) } val conversations by chat.conversationRepo.observeAll().collectAsState(initial = emptyList()) var loadError by remember { mutableStateOf(null) } @@ -195,7 +196,12 @@ fun ChatScreen( session.config?.let { cfg -> scope.launch { KaizenApi.prewarm(cfg.baseUrl) } } - onPauseOrDispose { } + onPauseOrDispose { + if (viewingLockedChat) { + messages.clear() + viewingLockedChat = false + } + } } fun refreshConversations() { @@ -207,6 +213,7 @@ fun ChatScreen( fun newChat() { messages.clear() conversationId = null + viewingLockedChat = false } fun openConversation(summary: ConversationSummary) { @@ -244,6 +251,7 @@ fun ChatScreen( ) } conversationId = summary.id + viewingLockedChat = true } } } @@ -255,6 +263,7 @@ fun ChatScreen( } return } + viewingLockedChat = false scope.launch { messages.clear() val cachedList = chat.messageRepo.getCached(summary.id) diff --git a/app/src/test/java/dev/kaizen/app/ConvertersTest.kt b/app/src/test/java/dev/kaizen/app/ConvertersTest.kt index b7511c2..68332ed 100644 --- a/app/src/test/java/dev/kaizen/app/ConvertersTest.kt +++ b/app/src/test/java/dev/kaizen/app/ConvertersTest.kt @@ -2,6 +2,7 @@ package dev.kaizen.app import dev.kaizen.app.db.Converters import dev.kaizen.app.net.Attachment +import dev.kaizen.app.net.SearchSource import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue import org.junit.Test @@ -61,4 +62,56 @@ class ConvertersTest { assertEquals(1, result.size) assertEquals("https://x.com/f", result[0].url) } + + // --- SearchSource converter tests --- + + @Test + fun emptySourcesRoundTrip() { + val json = converters.sourcesToJson(emptyList()) + val result = converters.jsonToSources(json) + assertTrue(result.isEmpty()) + } + + @Test + fun singleSourceRoundTrip() { + val original = listOf( + SearchSource(title = "Wikipedia", url = "https://en.wikipedia.org/wiki/Test", snippet = "A test article") + ) + val json = converters.sourcesToJson(original) + val result = converters.jsonToSources(json) + + assertEquals(1, result.size) + assertEquals("Wikipedia", result[0].title) + assertEquals("https://en.wikipedia.org/wiki/Test", result[0].url) + assertEquals("A test article", result[0].snippet) + } + + @Test + fun multipleSourcesRoundTrip() { + val original = listOf( + SearchSource(title = "Source 1", url = "https://a.com"), + SearchSource(title = "Source 2", url = "https://b.com", snippet = "Details"), + ) + val json = converters.sourcesToJson(original) + val result = converters.jsonToSources(json) + + assertEquals(2, result.size) + assertEquals("Source 1", result[0].title) + assertEquals("Source 2", result[1].title) + assertEquals("Details", result[1].snippet) + } + + @Test + fun malformedSourcesJsonReturnsEmptyList() { + val result = converters.jsonToSources("{broken") + assertTrue(result.isEmpty()) + } + + @Test + fun sourcesWithUnknownFieldsIgnored() { + val json = """[{"title":"T","url":"https://x.com","snippet":"","futureField":"v"}]""" + val result = converters.jsonToSources(json) + assertEquals(1, result.size) + assertEquals("T", result[0].title) + } } diff --git a/app/src/test/java/dev/kaizen/app/MappersTest.kt b/app/src/test/java/dev/kaizen/app/MappersTest.kt index 41cea08..7ae2f77 100644 --- a/app/src/test/java/dev/kaizen/app/MappersTest.kt +++ b/app/src/test/java/dev/kaizen/app/MappersTest.kt @@ -7,8 +7,10 @@ import dev.kaizen.app.db.toStoredMessage import dev.kaizen.app.db.toSummary import dev.kaizen.app.net.Attachment import dev.kaizen.app.net.ConversationSummary +import dev.kaizen.app.net.SearchSource import dev.kaizen.app.net.StoredMessage import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull import org.junit.Assert.assertTrue import org.junit.Test @@ -119,4 +121,64 @@ class MappersTest { assertEquals(original.attachments[0].mimeType, roundTripped.attachments[0].mimeType) assertEquals(original.attachments[0].size, roundTripped.attachments[0].size) } + + @Test + fun reasoningPreservedInRoundTrip() { + val original = StoredMessage( + id = "msg-r", role = "assistant", content = "Answer", + reasoning = "Let me think about this step by step...", + ) + val roundTripped = original.toEntity("c1", 0).toStoredMessage() + + assertEquals("Let me think about this step by step...", roundTripped.reasoning) + } + + @Test + fun nullReasoningPreserved() { + val original = StoredMessage(id = "msg-nr", role = "assistant", content = "Simple answer") + val entity = original.toEntity("c1", 0) + assertNull(entity.reasoning) + val roundTripped = entity.toStoredMessage() + assertNull(roundTripped.reasoning) + } + + @Test + fun sourcesPreservedInRoundTrip() { + val sources = listOf( + SearchSource(title = "Wikipedia", url = "https://en.wikipedia.org/wiki/Test", snippet = "A test"), + SearchSource(title = "MDN", url = "https://developer.mozilla.org", snippet = "Web docs"), + ) + val original = StoredMessage( + id = "msg-s", role = "assistant", content = "Search result", + sources = sources, + ) + val roundTripped = original.toEntity("c1", 0).toStoredMessage() + + assertEquals(2, roundTripped.sources?.size) + assertEquals("Wikipedia", roundTripped.sources!![0].title) + assertEquals("https://developer.mozilla.org", roundTripped.sources!![1].url) + } + + @Test + fun emptySourcesBecomeNull() { + val original = StoredMessage(id = "msg-es", role = "assistant", content = "No sources") + val entity = original.toEntity("c1", 0) + assertTrue(entity.sources.isEmpty()) + val roundTripped = entity.toStoredMessage() + assertNull(roundTripped.sources) + } + + @Test + fun reasoningAndSourcesTogether() { + val original = StoredMessage( + id = "msg-rs", role = "assistant", content = "Full answer", + reasoning = "Thinking...", + sources = listOf(SearchSource(title = "Src", url = "https://src.com")), + ) + val roundTripped = original.toEntity("c1", 0).toStoredMessage() + + assertEquals("Thinking...", roundTripped.reasoning) + assertEquals(1, roundTripped.sources?.size) + assertEquals("Src", roundTripped.sources!![0].title) + } }