refactor(sessions): extract search formatting and paging logic
Extract SessionSearchFormatting and SessionsPaging helpers. Deduplicate paging calculation in SessionsViewModel and add unit tests.
This commit is contained in:
1 parent
f0c2d2f87b
commit
6876db0393
7 files changed
+315
-112
No files matched your search
@@ -0,0 +1,98 @@
|
|||||||
|
package com.m57.hermescontrol.ui.sessions
|
||||||
|
|
||||||
|
import com.m57.hermescontrol.data.model.SessionInfo
|
||||||
|
import com.m57.hermescontrol.data.model.SessionSearchResult
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import kotlinx.serialization.json.JsonArray
|
||||||
|
import kotlinx.serialization.json.JsonElement
|
||||||
|
import kotlinx.serialization.json.JsonObject
|
||||||
|
import kotlinx.serialization.json.JsonPrimitive
|
||||||
|
import kotlinx.serialization.json.contentOrNull
|
||||||
|
import java.time.Instant
|
||||||
|
import java.time.ZoneId
|
||||||
|
import java.time.format.DateTimeFormatter
|
||||||
|
import java.time.format.FormatStyle
|
||||||
|
import kotlin.math.abs
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a backend search hit into a display model WITHOUT faking a title.
|
||||||
|
* The backend returns no session name, only a matched `snippet` + metadata, so the
|
||||||
|
* card must render the snippet as a match excerpt (never as the title). `title` is
|
||||||
|
* deliberately left null so [SearchResultCard] shows the honest "Match" layout.
|
||||||
|
*/
|
||||||
|
internal fun SessionSearchResult.toSessionInfo(): SessionInfo =
|
||||||
|
SessionInfo(
|
||||||
|
id = session_id,
|
||||||
|
title = null,
|
||||||
|
preview = snippet,
|
||||||
|
source = source,
|
||||||
|
model = model,
|
||||||
|
started_at = session_started,
|
||||||
|
// message_count/status aren't in the search payload; leave null so the
|
||||||
|
// search card hides those normal-list affordances.
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats a backend epoch-seconds timestamp into a friendly, local relative/absolute
|
||||||
|
* string. Used for search results where the session name is unknown.
|
||||||
|
*/
|
||||||
|
internal fun formatPlayedAt(epochSeconds: Double?): String? {
|
||||||
|
if (epochSeconds == null || epochSeconds <= 0.0) return null
|
||||||
|
val instant =
|
||||||
|
try {
|
||||||
|
Instant.ofEpochSecond(epochSeconds.toLong())
|
||||||
|
} catch (_: Exception) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
val now = Instant.now()
|
||||||
|
val diffSec = abs(now.epochSecond - instant.epochSecond)
|
||||||
|
val absFormatter =
|
||||||
|
DateTimeFormatter
|
||||||
|
.ofLocalizedDateTime(FormatStyle.MEDIUM)
|
||||||
|
.withZone(ZoneId.systemDefault())
|
||||||
|
return when {
|
||||||
|
diffSec < 60 -> "just now"
|
||||||
|
diffSec < 3600 -> "${diffSec / 60}m ago"
|
||||||
|
diffSec < 86400 -> "${diffSec / 3600}h ago"
|
||||||
|
diffSec < 7 * 86400 -> "${diffSec / 86400}d ago"
|
||||||
|
else -> absFormatter.format(instant)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val searchSnippetJson = Json { isLenient = true }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Makes a backend FTS snippet safe to present as prose. Some indexed messages are stored as
|
||||||
|
* JSON, so prefer their human-readable text fields rather than rendering the entire payload.
|
||||||
|
*/
|
||||||
|
fun cleanSearchSnippet(snippet: String): String {
|
||||||
|
val withoutHighlightMarkers = snippet.replace(">>>", "").replace("<<<", "").trim()
|
||||||
|
val parsedSnippet =
|
||||||
|
runCatching { searchSnippetJson.parseToJsonElement(withoutHighlightMarkers) }.getOrNull()
|
||||||
|
val displayText = parsedSnippet?.searchText() ?: withoutHighlightMarkers
|
||||||
|
return displayText
|
||||||
|
.replace(Regex("\\s+"), " ")
|
||||||
|
.trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun JsonElement.searchText(): String? =
|
||||||
|
when (this) {
|
||||||
|
is JsonPrimitive -> {
|
||||||
|
contentOrNull?.takeIf(String::isNotBlank)
|
||||||
|
}
|
||||||
|
|
||||||
|
is JsonArray -> {
|
||||||
|
mapNotNull { it.searchText() }
|
||||||
|
.joinToString(" ")
|
||||||
|
.takeIf(String::isNotBlank)
|
||||||
|
}
|
||||||
|
|
||||||
|
is JsonObject -> {
|
||||||
|
val textFieldNames =
|
||||||
|
listOf("content", "text", "message", "body", "prompt", "summary", "title")
|
||||||
|
val wrapperFieldNames = listOf("data", "result", "payload", "parts")
|
||||||
|
textFieldNames
|
||||||
|
.firstNotNullOfOrNull { fieldName -> this[fieldName]?.searchText() }
|
||||||
|
?: wrapperFieldNames.firstNotNullOfOrNull { fieldName -> this[fieldName]?.searchText() }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package com.m57.hermescontrol.ui.sessions
|
||||||
|
|
||||||
|
data class SessionPaginationResolution(
|
||||||
|
val hasMore: Boolean,
|
||||||
|
val total: Int,
|
||||||
|
)
|
||||||
|
|
||||||
|
object SessionsPaging {
|
||||||
|
/**
|
||||||
|
* Computes hasMore and effective total when refreshing or loading the initial page of sessions.
|
||||||
|
*/
|
||||||
|
fun resolveInitialPaging(
|
||||||
|
receivedCount: Int,
|
||||||
|
pageSize: Int,
|
||||||
|
backendTotal: Int,
|
||||||
|
accumulatedCount: Int,
|
||||||
|
): SessionPaginationResolution {
|
||||||
|
val hasMore = receivedCount >= pageSize && backendTotal > accumulatedCount
|
||||||
|
return SessionPaginationResolution(
|
||||||
|
hasMore = hasMore,
|
||||||
|
total = if (hasMore) backendTotal else accumulatedCount,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Computes hasMore and effective total when appending a page of sessions.
|
||||||
|
*/
|
||||||
|
fun resolveLoadMorePaging(
|
||||||
|
receivedCount: Int,
|
||||||
|
pageSize: Int,
|
||||||
|
addedNewItems: Boolean,
|
||||||
|
backendTotal: Int,
|
||||||
|
accumulatedCount: Int,
|
||||||
|
): SessionPaginationResolution {
|
||||||
|
val receivedFullPage = receivedCount >= pageSize
|
||||||
|
val hasMore = receivedFullPage && addedNewItems && backendTotal > accumulatedCount
|
||||||
|
return SessionPaginationResolution(
|
||||||
|
hasMore = hasMore,
|
||||||
|
total = if (hasMore) backendTotal else accumulatedCount,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -116,17 +116,6 @@ import com.m57.hermescontrol.ui.common.StatusBadge
|
|||||||
import com.m57.hermescontrol.ui.common.StatusBadgeType
|
import com.m57.hermescontrol.ui.common.StatusBadgeType
|
||||||
import com.m57.hermescontrol.ui.common.ToastEffect
|
import com.m57.hermescontrol.ui.common.ToastEffect
|
||||||
import com.m57.hermescontrol.ui.common.listItemSpacing
|
import com.m57.hermescontrol.ui.common.listItemSpacing
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
import kotlinx.serialization.json.JsonArray
|
|
||||||
import kotlinx.serialization.json.JsonElement
|
|
||||||
import kotlinx.serialization.json.JsonObject
|
|
||||||
import kotlinx.serialization.json.JsonPrimitive
|
|
||||||
import kotlinx.serialization.json.contentOrNull
|
|
||||||
import java.time.Instant
|
|
||||||
import java.time.ZoneId
|
|
||||||
import java.time.format.DateTimeFormatter
|
|
||||||
import java.time.format.FormatStyle
|
|
||||||
import kotlin.math.abs
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Auto-load the next history page when the user scrolls to within this many
|
* Auto-load the next history page when the user scrolls to within this many
|
||||||
@@ -197,89 +186,6 @@ private fun highlightText(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts a backend search hit into a display model WITHOUT faking a title.
|
|
||||||
* The backend returns no session name, only a matched `snippet` + metadata, so the
|
|
||||||
* card must render the snippet as a match excerpt (never as the title). `title` is
|
|
||||||
* deliberately left null so [SearchResultCard] shows the honest "Match" layout.
|
|
||||||
*/
|
|
||||||
private fun SessionSearchResult.toSessionInfo(): SessionInfo =
|
|
||||||
SessionInfo(
|
|
||||||
id = session_id,
|
|
||||||
title = null,
|
|
||||||
preview = snippet,
|
|
||||||
source = source,
|
|
||||||
model = model,
|
|
||||||
started_at = session_started,
|
|
||||||
// message_count/status aren't in the search payload; leave null so the
|
|
||||||
// search card hides those normal-list affordances.
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Formats a backend epoch-seconds timestamp into a friendly, local relative/absolute
|
|
||||||
* string. Used for search results where the session name is unknown.
|
|
||||||
*/
|
|
||||||
private fun formatPlayedAt(epochSeconds: Double?): String? {
|
|
||||||
if (epochSeconds == null || epochSeconds <= 0.0) return null
|
|
||||||
val instant =
|
|
||||||
try {
|
|
||||||
Instant.ofEpochSecond(epochSeconds.toLong())
|
|
||||||
} catch (_: Exception) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
val now = Instant.now()
|
|
||||||
val diffSec = abs(now.epochSecond - instant.epochSecond)
|
|
||||||
val absFormatter =
|
|
||||||
DateTimeFormatter
|
|
||||||
.ofLocalizedDateTime(FormatStyle.MEDIUM)
|
|
||||||
.withZone(ZoneId.systemDefault())
|
|
||||||
return when {
|
|
||||||
diffSec < 60 -> "just now"
|
|
||||||
diffSec < 3600 -> "${diffSec / 60}m ago"
|
|
||||||
diffSec < 86400 -> "${diffSec / 3600}h ago"
|
|
||||||
diffSec < 7 * 86400 -> "${diffSec / 86400}d ago"
|
|
||||||
else -> absFormatter.format(instant)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val searchSnippetJson = Json { isLenient = true }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Makes a backend FTS snippet safe to present as prose. Some indexed messages are stored as
|
|
||||||
* JSON, so prefer their human-readable text fields rather than rendering the entire payload.
|
|
||||||
*/
|
|
||||||
internal fun cleanSearchSnippet(snippet: String): String {
|
|
||||||
val withoutHighlightMarkers = snippet.replace(">>>", "").replace("<<<", "").trim()
|
|
||||||
val parsedSnippet =
|
|
||||||
runCatching { searchSnippetJson.parseToJsonElement(withoutHighlightMarkers) }.getOrNull()
|
|
||||||
val displayText = parsedSnippet?.searchText() ?: withoutHighlightMarkers
|
|
||||||
return displayText
|
|
||||||
.replace(Regex("\\s+"), " ")
|
|
||||||
.trim()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun JsonElement.searchText(): String? =
|
|
||||||
when (this) {
|
|
||||||
is JsonPrimitive -> {
|
|
||||||
contentOrNull?.takeIf(String::isNotBlank)
|
|
||||||
}
|
|
||||||
|
|
||||||
is JsonArray -> {
|
|
||||||
mapNotNull { it.searchText() }
|
|
||||||
.joinToString(" ")
|
|
||||||
.takeIf(String::isNotBlank)
|
|
||||||
}
|
|
||||||
|
|
||||||
is JsonObject -> {
|
|
||||||
val textFieldNames =
|
|
||||||
listOf("content", "text", "message", "body", "prompt", "summary", "title")
|
|
||||||
val wrapperFieldNames = listOf("data", "result", "payload", "parts")
|
|
||||||
textFieldNames
|
|
||||||
.firstNotNullOfOrNull { fieldName -> this[fieldName]?.searchText() }
|
|
||||||
?: wrapperFieldNames.firstNotNullOfOrNull { fieldName -> this[fieldName]?.searchText() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun displayedSessions(state: SessionsUiState): List<SessionTreeItem> =
|
private fun displayedSessions(state: SessionsUiState): List<SessionTreeItem> =
|
||||||
if (state.isSearchMode) {
|
if (state.isSearchMode) {
|
||||||
state.searchResults.map { searchResult ->
|
state.searchResults.map { searchResult ->
|
||||||
|
|||||||
@@ -147,11 +147,17 @@ class SessionsViewModel :
|
|||||||
rawPaginationOffset = data.nextOffset(0)
|
rawPaginationOffset = data.nextOffset(0)
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
val newSessions = data.sessions.orEmpty().pinnedFirst()
|
val newSessions = data.sessions.orEmpty().pinnedFirst()
|
||||||
val hasMore = data.sessions.size >= PAGE_SIZE && data.total > newSessions.size
|
val paging =
|
||||||
|
SessionsPaging.resolveInitialPaging(
|
||||||
|
receivedCount = data.sessions.size,
|
||||||
|
pageSize = PAGE_SIZE,
|
||||||
|
backendTotal = data.total,
|
||||||
|
accumulatedCount = newSessions.size,
|
||||||
|
)
|
||||||
it.copy(
|
it.copy(
|
||||||
sessions = newSessions,
|
sessions = newSessions,
|
||||||
total = if (hasMore) data.total else newSessions.size,
|
total = paging.total,
|
||||||
hasMore = hasMore,
|
hasMore = paging.hasMore,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -229,14 +235,20 @@ class SessionsViewModel :
|
|||||||
if (requestGeneration != generation) return@safeLaunchLoad
|
if (requestGeneration != generation) return@safeLaunchLoad
|
||||||
rawPaginationOffset = data.nextOffset(0)
|
rawPaginationOffset = data.nextOffset(0)
|
||||||
val sessionsList = data.sessions.orEmpty().pinnedFirst()
|
val sessionsList = data.sessions.orEmpty().pinnedFirst()
|
||||||
val hasMore = data.sessions.size >= PAGE_SIZE && data.total > sessionsList.size
|
val paging =
|
||||||
|
SessionsPaging.resolveInitialPaging(
|
||||||
|
receivedCount = data.sessions.size,
|
||||||
|
pageSize = PAGE_SIZE,
|
||||||
|
backendTotal = data.total,
|
||||||
|
accumulatedCount = sessionsList.size,
|
||||||
|
)
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
isLoadingMore = false,
|
isLoadingMore = false,
|
||||||
sessions = sessionsList,
|
sessions = sessionsList,
|
||||||
total = if (hasMore) data.total else sessionsList.size,
|
total = paging.total,
|
||||||
hasMore = hasMore,
|
hasMore = paging.hasMore,
|
||||||
selectedIds = emptySet(),
|
selectedIds = emptySet(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -284,14 +296,20 @@ class SessionsViewModel :
|
|||||||
(it.sessions + data.sessions)
|
(it.sessions + data.sessions)
|
||||||
.distinctBy { s -> s.id }
|
.distinctBy { s -> s.id }
|
||||||
.pinnedFirst()
|
.pinnedFirst()
|
||||||
val receivedFullPage = data.sessions.size >= PAGE_SIZE
|
|
||||||
val addedNewItems = newSessions.size > it.sessions.size
|
val addedNewItems = newSessions.size > it.sessions.size
|
||||||
val hasMore = receivedFullPage && addedNewItems && data.total > newSessions.size
|
val paging =
|
||||||
|
SessionsPaging.resolveLoadMorePaging(
|
||||||
|
receivedCount = data.sessions.size,
|
||||||
|
pageSize = PAGE_SIZE,
|
||||||
|
addedNewItems = addedNewItems,
|
||||||
|
backendTotal = data.total,
|
||||||
|
accumulatedCount = newSessions.size,
|
||||||
|
)
|
||||||
it.copy(
|
it.copy(
|
||||||
isLoadingMore = false,
|
isLoadingMore = false,
|
||||||
sessions = newSessions,
|
sessions = newSessions,
|
||||||
total = if (hasMore) data.total else newSessions.size,
|
total = paging.total,
|
||||||
hasMore = hasMore,
|
hasMore = paging.hasMore,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
package com.m57.hermescontrol.ui.sessions
|
||||||
|
|
||||||
|
import com.m57.hermescontrol.data.model.SessionSearchResult
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertNotNull
|
||||||
|
import org.junit.Assert.assertNull
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class SessionSearchFormattingTest {
|
||||||
|
@Test
|
||||||
|
fun `clean search snippet strips highlight markers from plain text`() {
|
||||||
|
assertEquals(
|
||||||
|
"Find the deployment logs",
|
||||||
|
cleanSearchSnippet(">>>Find<<< the deployment logs"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `clean search snippet extracts text from JSON payload`() {
|
||||||
|
assertEquals(
|
||||||
|
"Find the deployment logs",
|
||||||
|
cleanSearchSnippet("{\"role\":\"user\",\"content\":\">>>Find<<< the deployment logs\"}"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `clean search snippet extracts nested text array`() {
|
||||||
|
assertEquals(
|
||||||
|
"First part second part",
|
||||||
|
cleanSearchSnippet("{\"parts\":[\">>>First<<< part\",\"second part\"]}"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `clean search snippet normalizes whitespace`() {
|
||||||
|
assertEquals(
|
||||||
|
"hello world",
|
||||||
|
cleanSearchSnippet(" hello world \n "),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `toSessionInfo leaves title null for match layout`() {
|
||||||
|
val searchResult =
|
||||||
|
SessionSearchResult(
|
||||||
|
session_id = "test-123",
|
||||||
|
session_started = 1700000000.0,
|
||||||
|
snippet = "matched query excerpt",
|
||||||
|
source = "cli",
|
||||||
|
model = "test-model",
|
||||||
|
)
|
||||||
|
val info = searchResult.toSessionInfo()
|
||||||
|
assertEquals("test-123", info.id)
|
||||||
|
assertNull(info.title)
|
||||||
|
assertEquals("matched query excerpt", info.preview)
|
||||||
|
assertEquals("cli", info.source)
|
||||||
|
assertEquals("test-model", info.model)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `formatPlayedAt handles null and negative epoch`() {
|
||||||
|
assertNull(formatPlayedAt(null))
|
||||||
|
assertNull(formatPlayedAt(0.0))
|
||||||
|
assertNull(formatPlayedAt(-100.0))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `formatPlayedAt returns formatted string for valid timestamp`() {
|
||||||
|
val result = formatPlayedAt(1700000000.0)
|
||||||
|
assertNotNull(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
package com.m57.hermescontrol.ui.sessions
|
||||||
|
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertFalse
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class SessionsPagingTest {
|
||||||
|
@Test
|
||||||
|
fun `resolveInitialPaging hasMore when received full page and total exceeds accumulated`() {
|
||||||
|
val resolution =
|
||||||
|
SessionsPaging.resolveInitialPaging(
|
||||||
|
receivedCount = 50,
|
||||||
|
pageSize = 50,
|
||||||
|
backendTotal = 120,
|
||||||
|
accumulatedCount = 50,
|
||||||
|
)
|
||||||
|
assertTrue(resolution.hasMore)
|
||||||
|
assertEquals(120, resolution.total)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `resolveInitialPaging no more when received less than page size`() {
|
||||||
|
val resolution =
|
||||||
|
SessionsPaging.resolveInitialPaging(
|
||||||
|
receivedCount = 30,
|
||||||
|
pageSize = 50,
|
||||||
|
backendTotal = 30,
|
||||||
|
accumulatedCount = 30,
|
||||||
|
)
|
||||||
|
assertFalse(resolution.hasMore)
|
||||||
|
assertEquals(30, resolution.total)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `resolveInitialPaging no more when backendTotal equals accumulated`() {
|
||||||
|
val resolution =
|
||||||
|
SessionsPaging.resolveInitialPaging(
|
||||||
|
receivedCount = 50,
|
||||||
|
pageSize = 50,
|
||||||
|
backendTotal = 50,
|
||||||
|
accumulatedCount = 50,
|
||||||
|
)
|
||||||
|
assertFalse(resolution.hasMore)
|
||||||
|
assertEquals(50, resolution.total)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `resolveLoadMorePaging hasMore when full page added new items and total exceeds accumulated`() {
|
||||||
|
val resolution =
|
||||||
|
SessionsPaging.resolveLoadMorePaging(
|
||||||
|
receivedCount = 50,
|
||||||
|
pageSize = 50,
|
||||||
|
addedNewItems = true,
|
||||||
|
backendTotal = 150,
|
||||||
|
accumulatedCount = 100,
|
||||||
|
)
|
||||||
|
assertTrue(resolution.hasMore)
|
||||||
|
assertEquals(150, resolution.total)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `resolveLoadMorePaging no more when no new items added`() {
|
||||||
|
val resolution =
|
||||||
|
SessionsPaging.resolveLoadMorePaging(
|
||||||
|
receivedCount = 50,
|
||||||
|
pageSize = 50,
|
||||||
|
addedNewItems = false,
|
||||||
|
backendTotal = 150,
|
||||||
|
accumulatedCount = 100,
|
||||||
|
)
|
||||||
|
assertFalse(resolution.hasMore)
|
||||||
|
assertEquals(100, resolution.total)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -92,14 +92,6 @@ class SessionsViewModelTest {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `clean search snippet extracts text from JSON payload`() {
|
|
||||||
assertEquals(
|
|
||||||
"Find the deployment logs",
|
|
||||||
cleanSearchSnippet("{\"role\":\"user\",\"content\":\">>>Find<<< the deployment logs\"}"),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `sections request conversations excluding cron and automations from cron`() {
|
fun `sections request conversations excluding cron and automations from cron`() {
|
||||||
val vm = createViewModel()
|
val vm = createViewModel()
|
||||||
|
|||||||
Reference in new issue
Block a user