refactor(chat): pure server message mapper (#1363)
This commit is contained in:
5 files changed
+179
-32
No files matched your search
@@ -0,0 +1,62 @@
|
||||
package com.m57.hermescontrol.ui.chat
|
||||
|
||||
import com.m57.hermescontrol.data.model.SessionMessage
|
||||
import com.m57.hermescontrol.notification.ReplyNotificationTarget
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Test
|
||||
|
||||
/** Issue #1337: the REST mapper runs on plain inputs, with no Android, AuthManager, or tracker state. */
|
||||
class ChatServerMessageMapperPurityTest {
|
||||
private fun assistant(
|
||||
id: Int,
|
||||
text: String,
|
||||
) = SessionMessage(id = id, role = "assistant", content = JsonPrimitive(text))
|
||||
|
||||
@Test
|
||||
fun mediaAttachmentsUseTheInjectedUrlBuilder() {
|
||||
val mapped =
|
||||
mapServerMessages(
|
||||
"s",
|
||||
listOf(assistant(1, "Chart MEDIA:/opt/hermes/chart.png")),
|
||||
0,
|
||||
true,
|
||||
emptyList(),
|
||||
mediaUrl = { path -> "gw://$path" },
|
||||
).single()
|
||||
|
||||
assertEquals("Chart", mapped.content.trim())
|
||||
assertEquals(listOf("gw:///opt/hermes/chart.png"), mapped.attachments?.map { it.uri })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun mediaWithoutAGatewayStripsTheTokenAndAddsNoAttachment() {
|
||||
val mapped =
|
||||
mapServerMessages("s", listOf(assistant(1, "Chart MEDIA:/opt/hermes/chart.png")), 0, true, emptyList())
|
||||
.single()
|
||||
|
||||
assertEquals("Chart", mapped.content.trim())
|
||||
assertNull(mapped.attachments)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun activeReplyTargetIsAPlainInputScopedToItsSession() {
|
||||
val rows = listOf(assistant(100, "Done"), assistant(200, "Done"))
|
||||
val target = ReplyNotificationTarget("default", "s", "comp", 1L, serverMessageId = 100)
|
||||
|
||||
val bound = mapServerMessages("s", rows, 0, true, emptyList(), activeReplyTarget = target)
|
||||
val otherSession =
|
||||
mapServerMessages("s", rows, 0, true, emptyList(), activeReplyTarget = target.copy(sessionId = "x"))
|
||||
|
||||
assertEquals(listOf("comp", null), bound.map { it.completionId })
|
||||
assertEquals(listOf(null, null), otherSession.map { it.completionId })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun rowsWithoutTimestampsUseTheInjectedClock() {
|
||||
val mapped = mapServerMessages("s", listOf(assistant(1, "Hi")), 0, true, emptyList(), nowMs = 42L)
|
||||
|
||||
assertEquals(42L, mapped.single().timestamp)
|
||||
}
|
||||
}
|
||||
+76
-9
@@ -169,11 +169,29 @@ class ReadNotificationReviewRegressionTest {
|
||||
SessionMessage(id = 200, role = "assistant", content = JsonPrimitive("Done")),
|
||||
)
|
||||
for (latest in listOf(true, false)) {
|
||||
val mapped = mapServerMessages("session", rows, 10, latest, listOf(live), context = context)
|
||||
val mapped =
|
||||
mapServerMessages(
|
||||
"session",
|
||||
rows,
|
||||
10,
|
||||
latest,
|
||||
listOf(live),
|
||||
activeReplyTarget = ReplyNotificationTracker.getActiveTarget(context),
|
||||
)
|
||||
assertEquals(listOf("comp", null), mapped.map { it.completionId })
|
||||
val merged = mergeTranscriptWithLive(mapped, listOf(live), preserveLiveIds = true)
|
||||
assertEquals(if (latest) "rest-session-100" else "rest-session-10", merged.single { it.id == "ws" }.restId)
|
||||
assertNull(mapServerMessages("session", rows.takeLast(1), 11, latest, listOf(live)).single().completionId)
|
||||
// Target row absent from this page: the reserved completion must fail closed, not rebind by text.
|
||||
val targetRowAbsent =
|
||||
mapServerMessages(
|
||||
"session",
|
||||
rows.takeLast(1),
|
||||
11,
|
||||
latest,
|
||||
listOf(live),
|
||||
activeReplyTarget = ReplyNotificationTracker.getActiveTarget(context),
|
||||
)
|
||||
assertNull(targetRowAbsent.single().completionId)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -214,6 +232,7 @@ class ReadNotificationReviewRegressionTest {
|
||||
0,
|
||||
true,
|
||||
current,
|
||||
activeReplyTarget = ReplyNotificationTracker.getActiveTarget(),
|
||||
)
|
||||
val merged = mergeTranscriptWithLive(mapped, current, preserveLiveIds = true).single()
|
||||
assertEquals("ws", merged.id)
|
||||
@@ -256,7 +275,7 @@ class ReadNotificationReviewRegressionTest {
|
||||
latestPaging = true,
|
||||
liveMessages = current,
|
||||
isPagingOlder = false,
|
||||
context = context,
|
||||
activeReplyTarget = ReplyNotificationTracker.getActiveTarget(context),
|
||||
)
|
||||
val merged = mergeTranscriptWithLive(mapped, current, preserveLiveIds = true)
|
||||
|
||||
@@ -685,7 +704,15 @@ class ReadNotificationReviewRegressionTest {
|
||||
)
|
||||
|
||||
val mapped =
|
||||
mapServerMessages("session", history, 0, true, emptyList(), isPagingOlder = false, context = context)
|
||||
mapServerMessages(
|
||||
"session",
|
||||
history,
|
||||
0,
|
||||
true,
|
||||
emptyList(),
|
||||
isPagingOlder = false,
|
||||
activeReplyTarget = ReplyNotificationTracker.getActiveTarget(context),
|
||||
)
|
||||
assertEquals("Older message must have null completionId", null, mapped[0].completionId)
|
||||
assertEquals("Newest message must receive target completionId", "comp-newest-done", mapped[1].completionId)
|
||||
|
||||
@@ -747,7 +774,15 @@ class ReadNotificationReviewRegressionTest {
|
||||
)
|
||||
|
||||
val mapped =
|
||||
mapServerMessages("session", history, 0, true, emptyList(), isPagingOlder = false, context = context)
|
||||
mapServerMessages(
|
||||
"session",
|
||||
history,
|
||||
0,
|
||||
true,
|
||||
emptyList(),
|
||||
isPagingOlder = false,
|
||||
activeReplyTarget = ReplyNotificationTracker.getActiveTarget(context),
|
||||
)
|
||||
assertEquals("comp-long-2", mapped[0].completionId)
|
||||
assertEquals(null, mapped[1].completionId)
|
||||
|
||||
@@ -796,7 +831,15 @@ class ReadNotificationReviewRegressionTest {
|
||||
)
|
||||
|
||||
val mapped =
|
||||
mapServerMessages("session", history, 0, true, emptyList(), isPagingOlder = false, context = context)
|
||||
mapServerMessages(
|
||||
"session",
|
||||
history,
|
||||
0,
|
||||
true,
|
||||
emptyList(),
|
||||
isPagingOlder = false,
|
||||
activeReplyTarget = ReplyNotificationTracker.getActiveTarget(context),
|
||||
)
|
||||
val row = mapped.single()
|
||||
assertEquals("comp-media-cold", row.completionId)
|
||||
assertEquals("Here is the image", row.content)
|
||||
@@ -839,7 +882,15 @@ class ReadNotificationReviewRegressionTest {
|
||||
)
|
||||
|
||||
val mapped =
|
||||
mapServerMessages("session", history, 0, true, emptyList(), isPagingOlder = false, context = context)
|
||||
mapServerMessages(
|
||||
"session",
|
||||
history,
|
||||
0,
|
||||
true,
|
||||
emptyList(),
|
||||
isPagingOlder = false,
|
||||
activeReplyTarget = ReplyNotificationTracker.getActiveTarget(context),
|
||||
)
|
||||
val row = mapped.single()
|
||||
assertEquals("comp-media-only-cold", row.completionId)
|
||||
assertEquals("", row.content)
|
||||
@@ -887,7 +938,15 @@ class ReadNotificationReviewRegressionTest {
|
||||
)
|
||||
|
||||
val mapped =
|
||||
mapServerMessages("session", history, 0, true, emptyList(), isPagingOlder = false, context = context)
|
||||
mapServerMessages(
|
||||
"session",
|
||||
history,
|
||||
0,
|
||||
true,
|
||||
emptyList(),
|
||||
isPagingOlder = false,
|
||||
activeReplyTarget = ReplyNotificationTracker.getActiveTarget(context),
|
||||
)
|
||||
assertEquals("comp-media-dup-2", mapped[0].completionId)
|
||||
assertEquals(null, mapped[1].completionId)
|
||||
|
||||
@@ -955,7 +1014,15 @@ class ReadNotificationReviewRegressionTest {
|
||||
}
|
||||
|
||||
val mapped =
|
||||
mapServerMessages("session", history, 0, true, emptyList(), isPagingOlder = false, context = context)
|
||||
mapServerMessages(
|
||||
"session",
|
||||
history,
|
||||
0,
|
||||
true,
|
||||
emptyList(),
|
||||
isPagingOlder = false,
|
||||
activeReplyTarget = ReplyNotificationTracker.getActiveTarget(context),
|
||||
)
|
||||
assertEquals(listOf(null, null, "comp-turn"), mapped.map { it.completionId })
|
||||
|
||||
assertFalse(
|
||||
|
||||
Reference in new issue
Block a user