* fix(#686): implement robust history pagination Honor server-returned offset, degrade gracefully on older servers, and parse JSON tool results. * fix(ci): repair unit-test compilation and pagination test mocks - Remove trailing blank line in SessionMessagesResponse.kt (ktlint) - Replace unresolved io.mockk.eq/coAnswers usage in ChatViewModelTest with an ordered-call counter inside coAnswers, which compiles and runs on MockK 1.14.11 (eq is a matcher-scope member, not a top-level import) - Align HermesApiServiceMockWebServerTest with the PR's JsonElement content model (contentText) and the new include_compacted=true default query param All unit tests pass locally with the ByteBuddy agent preload. --------- Co-authored-by: M57 <hy4ri@users.noreply.github.com>
This commit is contained in:
1 parent
34ba93901b
commit
def122ab58
5 files changed
+294
-16
No files matched your search
@@ -1,4 +1,5 @@
|
||||
package com.m57.hermescontrol.data.model
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
@@ -6,15 +7,25 @@ import kotlinx.serialization.json.JsonPrimitive
|
||||
@Serializable
|
||||
data class SessionMessagesResponse(
|
||||
val messages: List<SessionMessage>,
|
||||
val offset: Int? = null,
|
||||
val total: Int? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class SessionMessage(
|
||||
val role: String? = null,
|
||||
val content: String? = null,
|
||||
val content: JsonElement? = null,
|
||||
val timestamp: JsonElement? = null,
|
||||
val type: String? = null,
|
||||
) {
|
||||
val timestampText: String?
|
||||
get() = (timestamp as? JsonPrimitive)?.content
|
||||
|
||||
val contentText: String
|
||||
get() =
|
||||
when (content) {
|
||||
is JsonPrimitive -> content.content
|
||||
null -> ""
|
||||
else -> content.toString()
|
||||
}
|
||||
}
|
||||
@@ -143,6 +143,7 @@ interface HermesApiService {
|
||||
@Path("id", encoded = true) sessionId: String,
|
||||
@Query("limit") limit: Int? = null,
|
||||
@Query("offset") offset: Int = 0,
|
||||
@Query("include_compacted") includeCompacted: Boolean? = true,
|
||||
): Response<SessionMessagesResponse>
|
||||
|
||||
@GET("api/sessions/stats")
|
||||
|
||||
@@ -1408,8 +1408,9 @@ class ChatViewModel(
|
||||
val result = fetchMessagePage(sessionId, offset, MESSAGE_PAGE_SIZE)
|
||||
when (result) {
|
||||
is NetworkResult.Success -> {
|
||||
val chatMessages = mapServerMessages(sessionId, result.data.messages.orEmpty(), offset)
|
||||
loadedMessageOffset = offset
|
||||
val serverOffset = result.data.offset ?: offset
|
||||
val chatMessages = mapServerMessages(sessionId, result.data.messages.orEmpty(), serverOffset)
|
||||
loadedMessageOffset = serverOffset
|
||||
withContext(Dispatchers.IO) {
|
||||
repo.persistMessages(chatMessages, sessionId)
|
||||
}
|
||||
@@ -1418,7 +1419,7 @@ class ChatViewModel(
|
||||
state.copy(
|
||||
messages = chatMessages,
|
||||
isLoading = false,
|
||||
hasOlderMessages = offset > 0 && chatMessages.isNotEmpty(),
|
||||
hasOlderMessages = serverOffset > 0 && chatMessages.isNotEmpty(),
|
||||
isLoadingOlder = false,
|
||||
)
|
||||
}
|
||||
@@ -1449,15 +1450,16 @@ class ChatViewModel(
|
||||
viewModelScope.launch {
|
||||
when (val result = fetchMessagePage(sessionId, newOffset, limit)) {
|
||||
is NetworkResult.Success -> {
|
||||
val older = mapServerMessages(sessionId, result.data.messages.orEmpty(), newOffset)
|
||||
loadedMessageOffset = newOffset
|
||||
val returnedOffset = result.data.offset ?: newOffset
|
||||
val older = mapServerMessages(sessionId, result.data.messages.orEmpty(), returnedOffset)
|
||||
loadedMessageOffset = returnedOffset
|
||||
withContext(Dispatchers.IO) { repo.persistMessages(older, sessionId) }
|
||||
_uiState.update { current ->
|
||||
if (current.currentSessionId != sessionId) return@update current
|
||||
current.copy(
|
||||
messages = (older + current.messages).distinctBy { it.id },
|
||||
isLoadingOlder = false,
|
||||
hasOlderMessages = newOffset > 0,
|
||||
hasOlderMessages = returnedOffset < oldOffset && older.isNotEmpty() && returnedOffset > 0,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1559,7 +1561,14 @@ class ChatViewModel(
|
||||
offset: Int,
|
||||
limit: Int,
|
||||
) = withContext(Dispatchers.IO) {
|
||||
safeApiCall { ApiClient.hermesApi.getSessionMessages(sessionId, limit = limit, offset = offset) }
|
||||
safeApiCall {
|
||||
ApiClient.hermesApi.getSessionMessages(
|
||||
sessionId = sessionId,
|
||||
limit = limit,
|
||||
offset = offset,
|
||||
includeCompacted = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun mapServerMessages(
|
||||
@@ -1585,7 +1594,7 @@ class ChatViewModel(
|
||||
ChatMessage(
|
||||
id = "rest-$sessionId-$globalIndex",
|
||||
role = role,
|
||||
content = msg.content.orEmpty(),
|
||||
content = msg.contentText,
|
||||
timestamp = timestamp,
|
||||
isStreaming = false,
|
||||
)
|
||||
|
||||
Reference in new issue
Block a user