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:
parent
edbdbd8697
commit
a16dfffd5c
3 changed files with 125 additions and 1 deletions
|
|
@ -118,6 +118,7 @@ fun ChatScreen(
|
||||||
|
|
||||||
// Conversation persistence: sidebar list from Room (instant), active conversation id
|
// Conversation persistence: sidebar list from Room (instant), active conversation id
|
||||||
var conversationId by remember { mutableStateOf<String?>(null) }
|
var conversationId by remember { mutableStateOf<String?>(null) }
|
||||||
|
var viewingLockedChat by remember { mutableStateOf(false) }
|
||||||
val conversations by chat.conversationRepo.observeAll().collectAsState(initial = emptyList())
|
val conversations by chat.conversationRepo.observeAll().collectAsState(initial = emptyList())
|
||||||
|
|
||||||
var loadError by remember { mutableStateOf<String?>(null) }
|
var loadError by remember { mutableStateOf<String?>(null) }
|
||||||
|
|
@ -195,7 +196,12 @@ fun ChatScreen(
|
||||||
session.config?.let { cfg ->
|
session.config?.let { cfg ->
|
||||||
scope.launch { KaizenApi.prewarm(cfg.baseUrl) }
|
scope.launch { KaizenApi.prewarm(cfg.baseUrl) }
|
||||||
}
|
}
|
||||||
onPauseOrDispose { }
|
onPauseOrDispose {
|
||||||
|
if (viewingLockedChat) {
|
||||||
|
messages.clear()
|
||||||
|
viewingLockedChat = false
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun refreshConversations() {
|
fun refreshConversations() {
|
||||||
|
|
@ -207,6 +213,7 @@ fun ChatScreen(
|
||||||
fun newChat() {
|
fun newChat() {
|
||||||
messages.clear()
|
messages.clear()
|
||||||
conversationId = null
|
conversationId = null
|
||||||
|
viewingLockedChat = false
|
||||||
}
|
}
|
||||||
|
|
||||||
fun openConversation(summary: ConversationSummary) {
|
fun openConversation(summary: ConversationSummary) {
|
||||||
|
|
@ -244,6 +251,7 @@ fun ChatScreen(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
conversationId = summary.id
|
conversationId = summary.id
|
||||||
|
viewingLockedChat = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -255,6 +263,7 @@ fun ChatScreen(
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
viewingLockedChat = false
|
||||||
scope.launch {
|
scope.launch {
|
||||||
messages.clear()
|
messages.clear()
|
||||||
val cachedList = chat.messageRepo.getCached(summary.id)
|
val cachedList = chat.messageRepo.getCached(summary.id)
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package dev.kaizen.app
|
||||||
|
|
||||||
import dev.kaizen.app.db.Converters
|
import dev.kaizen.app.db.Converters
|
||||||
import dev.kaizen.app.net.Attachment
|
import dev.kaizen.app.net.Attachment
|
||||||
|
import dev.kaizen.app.net.SearchSource
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Assert.assertTrue
|
import org.junit.Assert.assertTrue
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
@ -61,4 +62,56 @@ class ConvertersTest {
|
||||||
assertEquals(1, result.size)
|
assertEquals(1, result.size)
|
||||||
assertEquals("https://x.com/f", result[0].url)
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,10 @@ import dev.kaizen.app.db.toStoredMessage
|
||||||
import dev.kaizen.app.db.toSummary
|
import dev.kaizen.app.db.toSummary
|
||||||
import dev.kaizen.app.net.Attachment
|
import dev.kaizen.app.net.Attachment
|
||||||
import dev.kaizen.app.net.ConversationSummary
|
import dev.kaizen.app.net.ConversationSummary
|
||||||
|
import dev.kaizen.app.net.SearchSource
|
||||||
import dev.kaizen.app.net.StoredMessage
|
import dev.kaizen.app.net.StoredMessage
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertNull
|
||||||
import org.junit.Assert.assertTrue
|
import org.junit.Assert.assertTrue
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
|
|
@ -119,4 +121,64 @@ class MappersTest {
|
||||||
assertEquals(original.attachments[0].mimeType, roundTripped.attachments[0].mimeType)
|
assertEquals(original.attachments[0].mimeType, roundTripped.attachments[0].mimeType)
|
||||||
assertEquals(original.attachments[0].size, roundTripped.attachments[0].size)
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue