feat(chat): show agent message reactions (#1422)

This commit is contained in:
m57 authored and GitHub committed 2026-09-30 23:15:11 +03:00
1 parent 6e14c49df1
commit 0c336ecc4d
14 files changed
+289

No files matched your search

@@ -1,5 +1,6 @@
package com.m57.hermescontrol.data.ws
import com.m57.hermescontrol.data.model.MessageReaction
import io.mockk.every
import io.mockk.mockkStatic
import io.mockk.unmockkAll
@@ -928,6 +929,52 @@ class EventParserTest {
)
}
@Test
fun testParseMessageReaction_returnsMessageReactionUpdated() {
val response =
createJsonRpcResponse(
jsonrpc = "2.0",
id = null,
result = null,
error = null,
method = "event",
params =
mapOf(
"type" to "message.reaction",
"session_id" to "s1",
"payload" to
mapOf(
"row_id" to 42,
"role" to "user",
"reactions" to
listOf(
mapOf("emoji" to "\uD83D\uDE02", "author" to "agent", "at" to 1.0),
mapOf("emoji" to "", "author" to "user"),
),
),
),
)
val event = EventParser.parse(response, "") as WsEvent.MessageReactionUpdated
assertEquals(42L, event.rowId)
assertEquals("user", event.role)
assertEquals("s1", event.sessionId)
assertEquals(listOf(MessageReaction("\uD83D\uDE02", "agent")), event.reactions)
}
@Test
fun testParseMessageReaction_withoutRowId_isUnknown() {
val response =
createJsonRpcResponse(
jsonrpc = "2.0",
id = null,
result = null,
error = null,
method = "event",
params = mapOf("type" to "message.reaction", "payload" to mapOf("reactions" to emptyList<Any>())),
)
assertTrue(EventParser.parse(response, "") is WsEvent.Unknown)
}
@Test
fun testParseUnknownType_returnsUnknownEvent() {
val response =
@@ -1,8 +1,12 @@
package com.m57.hermescontrol.ui.chat
import com.m57.hermescontrol.data.model.MessageReaction
import com.m57.hermescontrol.data.model.SessionMessage
import com.m57.hermescontrol.notification.ReplyNotificationTarget
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.buildJsonArray
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.put
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Test
@@ -59,4 +63,35 @@ class ChatServerMessageMapperPurityTest {
assertEquals(42L, mapped.single().timestamp)
}
@Test
fun persistedReactionsHydrateFromDisplayMetadata() {
val row =
SessionMessage(
id = 5,
role = "user",
content = JsonPrimitive("hello"),
display_metadata =
buildJsonObject {
put(
"reactions",
buildJsonArray {
add(
buildJsonObject {
put("emoji", "\uD83D\uDE02")
put("author", "agent")
},
)
add(buildJsonObject { put("author", "user") })
},
)
},
)
val mapped = mapServerMessages("s", listOf(row), 0, true, emptyList()).single()
assertEquals(listOf(MessageReaction("\uD83D\uDE02", "agent")), mapped.reactions)
assertEquals(
emptyList<MessageReaction>(),
mapServerMessages("s", listOf(assistant(6, "x")), 0, true, emptyList()).single().reactions,
)
}
}
@@ -1,5 +1,6 @@
package com.m57.hermescontrol.ui.chat
import com.m57.hermescontrol.data.model.MessageReaction
import com.m57.hermescontrol.data.model.UsageSnapshotResponse
import com.m57.hermescontrol.data.ws.WsEvent
import org.junit.Assert.assertEquals
@@ -1642,4 +1643,60 @@ class ChatWsEventReducerTest {
assertEquals("idle", result.state.compressionStatus)
assertEquals(initialMessages, result.state.messages)
}
@Test
fun messageReactionPaintsOnlyTheRowWithTheMatchingServerId() {
val target = ChatMessage(id = "a", role = MessageRole.USER, content = "hi", serverRowId = 7L)
val other = ChatMessage(id = "b", role = MessageRole.ASSISTANT, content = "yo", serverRowId = 8L)
val state = ChatUiState(currentSessionId = "s", messages = listOf(target, other))
val reactions = listOf(MessageReaction("\u2764\uFE0F", "agent"))
val result =
ChatWsEventReducer.reduce(
state,
StreamingState(),
WsEvent.MessageReactionUpdated(7L, reactions, "user", "s"),
"s",
)
assertEquals(reactions, result.state.messages[0].reactions)
assertTrue(
result.state.messages[1]
.reactions
.isEmpty(),
)
val retracted =
ChatWsEventReducer.reduce(
result.state,
result.streamingState,
WsEvent.MessageReactionUpdated(7L, emptyList(), "user", "s"),
"s",
)
assertTrue(
retracted.state.messages[0]
.reactions
.isEmpty(),
)
}
@Test
fun messageReactionForUnknownRowOrOtherSessionIsIgnored() {
val msg = ChatMessage(id = "a", role = MessageRole.USER, content = "hi", serverRowId = 7L)
val state = ChatUiState(currentSessionId = "s", messages = listOf(msg))
val reactions = listOf(MessageReaction("\uD83D\uDC4D", "agent"))
val unknownRow =
ChatWsEventReducer.reduce(
state,
StreamingState(),
WsEvent.MessageReactionUpdated(99L, reactions, "user", "s"),
"s",
)
assertEquals(state, unknownRow.state)
val otherSession =
ChatWsEventReducer.reduce(
state,
StreamingState(),
WsEvent.MessageReactionUpdated(7L, reactions, "user", "zzz"),
"s",
)
assertEquals(state, otherSession.state)
}
}