auto-lock on background + reasoning/sources tests

Clear locked chat messages when app goes to background
(onPauseOrDispose). Track viewingLockedChat state — reset on
new chat or opening a non-locked conversation.

Add tests: reasoning round-trip, null reasoning preserved,
sources round-trip, empty sources → null, reasoning+sources
together, SearchSource converter (empty/single/multi/malformed/
unknown fields).
This commit is contained in:
Bruno Deanoz 2026-06-22 00:03:59 +02:00
parent edbdbd8697
commit a16dfffd5c
3 changed files with 125 additions and 1 deletions

View file

@ -118,6 +118,7 @@ fun ChatScreen(
// Conversation persistence: sidebar list from Room (instant), active conversation id
var conversationId by remember { mutableStateOf<String?>(null) }
var viewingLockedChat by remember { mutableStateOf(false) }
val conversations by chat.conversationRepo.observeAll().collectAsState(initial = emptyList())
var loadError by remember { mutableStateOf<String?>(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)

View file

@ -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)
}
}

View file

@ -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)
}
}