feat(bots): add multi-agent group chat collaboration room (#998)
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
This commit is contained in:
1 parent
6bc4e872ca
commit
f04d878358
16 files changed
+1589
-126
No files matched your search
@@ -1,8 +1,9 @@
|
||||
package com.m57.hermescontrol.data.model
|
||||
|
||||
import com.m57.hermescontrol.data.remote.OkHttpProvider
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
import kotlinx.serialization.json.encodeToJsonElement
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertNotNull
|
||||
@@ -11,120 +12,132 @@ import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
class ProfileSerializationTest {
|
||||
private val json = OkHttpProvider.json
|
||||
private val json = Json { ignoreUnknownKeys = true }
|
||||
|
||||
@Test
|
||||
fun testProfileInfoDeserializationWithBotMetadata() {
|
||||
val jsonStr =
|
||||
"""
|
||||
{
|
||||
"name": "researcher",
|
||||
"path": "/home/user/.hermes/profiles/researcher",
|
||||
"is_default": false,
|
||||
"model": "anthropic/claude-3-7-sonnet",
|
||||
"provider": "anthropic",
|
||||
"skill_count": 5,
|
||||
"description": "Base description",
|
||||
"display_name": "Researcher Agent",
|
||||
"has_avatar": true,
|
||||
"canonical_session": {
|
||||
"id": "sess-canon-123",
|
||||
"resolved_id": "sess-canon-tip",
|
||||
"root_title": "Bot Chat",
|
||||
"title": "Bot Chat",
|
||||
"preview": "Latest research report ready.",
|
||||
"started_at": 1700000000,
|
||||
"last_active": 1700005000,
|
||||
"message_count": 42
|
||||
},
|
||||
"worker_session": {
|
||||
"id": "worker-456",
|
||||
"source": "kanban",
|
||||
"title": "Background analysis",
|
||||
"last_active": 1700006000
|
||||
},
|
||||
"last_session": {
|
||||
"id": "last-789",
|
||||
"title": "Ad-hoc task",
|
||||
"preview": "Done!",
|
||||
"started_at": 1700001000,
|
||||
"last_active": 1700002000,
|
||||
"message_count": 10
|
||||
},
|
||||
"ui_meta": {
|
||||
"hermes-bots": {
|
||||
"title": "Dr. Researcher",
|
||||
"description": "Deep literature scout",
|
||||
"avatar": {
|
||||
"shape": "hexagon",
|
||||
"color": "#4A90E2",
|
||||
"icon": "science",
|
||||
"image_url": "https://example.com/avatar.png"
|
||||
},
|
||||
"hidden": false,
|
||||
"groups": ["science-team", "daily-sync"]
|
||||
}
|
||||
}
|
||||
}
|
||||
""".trimIndent()
|
||||
fun testBotRosterMetaSerialization() {
|
||||
val meta =
|
||||
BotRosterMeta(
|
||||
title = "Literature Scout",
|
||||
description = "Scrapes arXiv and summarizes papers",
|
||||
avatar =
|
||||
BotAvatarMeta(
|
||||
shape = "circle",
|
||||
color = "#4A90E2",
|
||||
icon = "science",
|
||||
),
|
||||
hidden = false,
|
||||
groups = listOf("research", "ai-lab"),
|
||||
group = "research",
|
||||
created = 1718000000.0,
|
||||
)
|
||||
|
||||
val profile = json.decodeFromString<ProfileInfo>(jsonStr)
|
||||
assertEquals("researcher", profile.name)
|
||||
assertEquals("Researcher Agent", profile.display_name)
|
||||
assertTrue(profile.has_avatar == true)
|
||||
val encoded = json.encodeToString(meta)
|
||||
val decoded = json.decodeFromString<BotRosterMeta>(encoded)
|
||||
|
||||
// Canonical session
|
||||
val canon = profile.canonical_session
|
||||
assertNotNull(canon)
|
||||
assertEquals("sess-canon-123", canon?.id)
|
||||
assertEquals("sess-canon-tip", canon?.resolved_id)
|
||||
assertEquals("Latest research report ready.", canon?.preview)
|
||||
assertEquals(42, canon?.message_count)
|
||||
assertEquals("Literature Scout", decoded.title)
|
||||
assertEquals("Scrapes arXiv and summarizes papers", decoded.description)
|
||||
assertEquals("circle", decoded.avatar?.shape)
|
||||
assertEquals("#4A90E2", decoded.avatar?.color)
|
||||
assertEquals("science", decoded.avatar?.icon)
|
||||
assertEquals(false, decoded.hidden)
|
||||
assertEquals(listOf("research", "ai-lab"), decoded.groups)
|
||||
assertEquals("research", decoded.group)
|
||||
assertEquals(1718000000.0, decoded.created)
|
||||
}
|
||||
|
||||
// Worker session
|
||||
val worker = profile.worker_session
|
||||
assertNotNull(worker)
|
||||
assertEquals("worker-456", worker?.id)
|
||||
assertEquals("kanban", worker?.source)
|
||||
@Test
|
||||
fun testProfileInfoWithBotMetaExtraction() {
|
||||
val botMeta =
|
||||
BotRosterMeta(
|
||||
title = "Custom Bot Title",
|
||||
description = "Custom Bot Description",
|
||||
avatar = BotAvatarMeta(shape = "hexagon", color = "#E91E63", icon = "robot"),
|
||||
hidden = false,
|
||||
)
|
||||
|
||||
// Bot metadata decoding
|
||||
val botMeta = profile.botMeta(json)
|
||||
assertNotNull(botMeta)
|
||||
assertEquals("Dr. Researcher", botMeta?.title)
|
||||
assertEquals("Deep literature scout", botMeta?.description)
|
||||
assertEquals("hexagon", botMeta?.avatar?.shape)
|
||||
assertEquals("#4A90E2", botMeta?.avatar?.color)
|
||||
assertEquals(listOf("science-team", "daily-sync"), botMeta?.groups)
|
||||
val profile =
|
||||
ProfileInfo(
|
||||
name = "custom_bot",
|
||||
display_name = "Original Name",
|
||||
description = "Original Description",
|
||||
ui_meta = mapOf("hermes-bots" to json.encodeToJsonElement(botMeta)),
|
||||
canonical_session =
|
||||
CanonicalSessionInfo(
|
||||
id = "sess-123",
|
||||
last_active = 1720000000.0,
|
||||
),
|
||||
worker_session =
|
||||
ProfileWorkerSummary(
|
||||
id = "worker-456",
|
||||
source = "kanban",
|
||||
title = "Task #42",
|
||||
last_active = 1720000100.0,
|
||||
),
|
||||
)
|
||||
|
||||
// Helpers
|
||||
assertEquals("Dr. Researcher", profile.effectiveTitle)
|
||||
assertEquals("Deep literature scout", profile.effectiveDescription)
|
||||
val extractedMeta = profile.botMeta(json)
|
||||
assertNotNull(extractedMeta)
|
||||
assertEquals("Custom Bot Title", extractedMeta?.title)
|
||||
assertEquals("Custom Bot Title", profile.effectiveTitle)
|
||||
assertEquals("Custom Bot Description", profile.effectiveDescription)
|
||||
assertFalse(profile.isHidden)
|
||||
assertEquals("sess-123", profile.canonical_session?.id)
|
||||
assertEquals("worker-456", profile.worker_session?.id)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testProfileInfoWithoutBotMeta_fallsBackToProfileFields() {
|
||||
val profile =
|
||||
ProfileInfo(
|
||||
name = "regular_profile",
|
||||
display_name = "My Profile",
|
||||
description = "Just a profile",
|
||||
)
|
||||
|
||||
assertNull(profile.botMeta(json))
|
||||
assertEquals("My Profile", profile.effectiveTitle)
|
||||
assertEquals("Just a profile", profile.effectiveDescription)
|
||||
assertFalse(profile.isHidden)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testProfileInfoBackwardCompatibility() {
|
||||
val legacyJson =
|
||||
"""
|
||||
{
|
||||
"name": "default",
|
||||
"is_default": true,
|
||||
"model": "openai/gpt-4o",
|
||||
"provider": "openai",
|
||||
"description": "Default Assistant"
|
||||
}
|
||||
""".trimIndent()
|
||||
fun testProfileInfoHidden_derivedFromBotMeta() {
|
||||
val hiddenBot =
|
||||
ProfileInfo(
|
||||
name = "hidden_bot",
|
||||
ui_meta =
|
||||
mapOf(
|
||||
"hermes-bots" to
|
||||
json.encodeToJsonElement(
|
||||
BotRosterMeta(title = "Hidden", hidden = true),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
val visibleBot =
|
||||
ProfileInfo(
|
||||
name = "visible_bot",
|
||||
ui_meta =
|
||||
mapOf(
|
||||
"hermes-bots" to
|
||||
json.encodeToJsonElement(
|
||||
BotRosterMeta(title = "Visible", hidden = false),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
assertTrue(hiddenBot.isHidden)
|
||||
assertFalse(visibleBot.isHidden)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testProfileInfoDefault_fallbackNames() {
|
||||
val profile = ProfileInfo(name = "default")
|
||||
|
||||
val profile = json.decodeFromString<ProfileInfo>(legacyJson)
|
||||
assertEquals("default", profile.name)
|
||||
assertTrue(profile.is_default == true)
|
||||
assertNull(profile.canonical_session)
|
||||
assertNull(profile.worker_session)
|
||||
assertNull(profile.last_session)
|
||||
assertNull(profile.ui_meta)
|
||||
assertNull(profile.botMeta(json))
|
||||
assertEquals("default", profile.effectiveTitle)
|
||||
assertEquals("Default Assistant", profile.effectiveDescription)
|
||||
assertEquals("", profile.effectiveDescription)
|
||||
assertFalse(profile.isHidden)
|
||||
}
|
||||
|
||||
@@ -140,8 +153,17 @@ class ProfileSerializationTest {
|
||||
GroupChatRoomMeta(
|
||||
id = "room-1",
|
||||
name = "War Room",
|
||||
members = listOf("default", "researcher"),
|
||||
members = listOf(JsonPrimitive("default"), JsonPrimitive("researcher")),
|
||||
picture = null,
|
||||
log =
|
||||
listOf(
|
||||
GroupChatSyncLogEntry(
|
||||
id = "msg-1",
|
||||
from = GroupChatSyncFrom(kind = "user", name = "You"),
|
||||
text = "Let's review the code",
|
||||
at = 1724000000L,
|
||||
),
|
||||
),
|
||||
updatedAt = 1724000000L,
|
||||
createdAt = 1723000000L,
|
||||
),
|
||||
@@ -153,8 +175,11 @@ class ProfileSerializationTest {
|
||||
val decoded = json.decodeFromString<GroupChatSyncSnapshot>(encoded)
|
||||
assertEquals(3, decoded.version)
|
||||
assertEquals(1, decoded.rooms?.size)
|
||||
assertEquals("War Room", decoded.rooms?.get("room-1")?.name)
|
||||
assertEquals(listOf("default", "researcher"), decoded.rooms?.get("room-1")?.members)
|
||||
val room = decoded.rooms?.get("room-1")
|
||||
assertEquals("War Room", room?.name)
|
||||
assertEquals(listOf("default", "researcher"), room?.memberNames)
|
||||
assertEquals(1, room?.log?.size)
|
||||
assertEquals("Let's review the code", room?.log?.first()?.text)
|
||||
assertEquals(1723500000L, decoded.deleted?.get("old-room"))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package com.m57.hermescontrol.ui.bots.group
|
||||
|
||||
import com.m57.hermescontrol.data.model.BotAvatarMeta
|
||||
import com.m57.hermescontrol.data.model.BotRosterMeta
|
||||
import com.m57.hermescontrol.data.model.ProfileInfo
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.encodeToJsonElement
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
class GroupChatMentionsTest {
|
||||
private val json = Json { ignoreUnknownKeys = true }
|
||||
|
||||
private val botA =
|
||||
ProfileInfo(
|
||||
name = "scoutbot",
|
||||
ui_meta =
|
||||
mapOf(
|
||||
"hermes-bots" to
|
||||
json.encodeToJsonElement(
|
||||
BotRosterMeta(title = "Scout Bot", avatar = BotAvatarMeta()),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
private val botB =
|
||||
ProfileInfo(
|
||||
name = "coder",
|
||||
ui_meta =
|
||||
mapOf(
|
||||
"hermes-bots" to
|
||||
json.encodeToJsonElement(
|
||||
BotRosterMeta(title = "Dev Bot", avatar = BotAvatarMeta()),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
private val members = listOf(botA, botB)
|
||||
|
||||
@Test
|
||||
fun parseMentions_detectsDirectBotHandle() {
|
||||
val result = GroupChatMentions.parseMentions("Hey @scoutbot can you check this?", members)
|
||||
assertFalse(result.isEveryone)
|
||||
assertTrue(result.mentionedBots.contains("scoutbot"))
|
||||
assertFalse(result.mentionedBots.contains("coder"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun parseMentions_detectsEveryoneAndAll() {
|
||||
val resEveryone = GroupChatMentions.parseMentions("Hello @everyone!", members)
|
||||
assertTrue(resEveryone.isEveryone)
|
||||
|
||||
val resAll = GroupChatMentions.parseMentions("@all status update please", members)
|
||||
assertTrue(resAll.isEveryone)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun resolveResponders_defaultsToAllWhenNoMention() {
|
||||
val responders = GroupChatMentions.resolveResponders("General discussion message", members)
|
||||
assertEquals(2, responders.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun resolveResponders_filtersToMentionedOnly() {
|
||||
val responders = GroupChatMentions.resolveResponders("@coder build the apk", members)
|
||||
assertEquals(1, responders.size)
|
||||
assertEquals("coder", responders.first().name)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,266 @@
|
||||
package com.m57.hermescontrol.ui.bots.group
|
||||
|
||||
import android.util.Log
|
||||
import com.m57.hermescontrol.data.model.BotAvatarMeta
|
||||
import com.m57.hermescontrol.data.model.BotRosterMeta
|
||||
import com.m57.hermescontrol.data.model.GroupChatRoomMeta
|
||||
import com.m57.hermescontrol.data.model.GroupChatSyncFrom
|
||||
import com.m57.hermescontrol.data.model.GroupChatSyncLogEntry
|
||||
import com.m57.hermescontrol.data.model.GroupChatSyncSnapshot
|
||||
import com.m57.hermescontrol.data.model.ProfileInfo
|
||||
import com.m57.hermescontrol.data.model.ProfilesResponse
|
||||
import com.m57.hermescontrol.data.remote.ApiClient
|
||||
import com.m57.hermescontrol.data.remote.HermesApiService
|
||||
import com.m57.hermescontrol.data.ws.HermesWsClient
|
||||
import com.m57.hermescontrol.data.ws.WsEvent
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.mockkObject
|
||||
import io.mockk.mockkStatic
|
||||
import io.mockk.unmockkObject
|
||||
import io.mockk.unmockkStatic
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.test.StandardTestDispatcher
|
||||
import kotlinx.coroutines.test.advanceUntilIdle
|
||||
import kotlinx.coroutines.test.resetMain
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import kotlinx.coroutines.test.setMain
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
import kotlinx.serialization.json.encodeToJsonElement
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import retrofit2.Response
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class GroupChatViewModelTest {
|
||||
private val testDispatcher = StandardTestDispatcher()
|
||||
private lateinit var mockApi: HermesApiService
|
||||
private val eventsFlow = MutableSharedFlow<WsEvent>()
|
||||
private val json = Json { ignoreUnknownKeys = true }
|
||||
|
||||
private val botA =
|
||||
ProfileInfo(
|
||||
name = "scout",
|
||||
ui_meta =
|
||||
mapOf(
|
||||
"hermes-bots" to
|
||||
json.encodeToJsonElement(
|
||||
BotRosterMeta(
|
||||
title = "Scout Bot",
|
||||
groups = listOf("Dev Team"),
|
||||
avatar = BotAvatarMeta(icon = "science"),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
private val botB =
|
||||
ProfileInfo(
|
||||
name = "coder",
|
||||
ui_meta =
|
||||
mapOf(
|
||||
"hermes-bots" to
|
||||
json.encodeToJsonElement(
|
||||
BotRosterMeta(
|
||||
title = "Dev Bot",
|
||||
groups = listOf("Dev Team"),
|
||||
avatar = BotAvatarMeta(icon = "code"),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
Dispatchers.setMain(testDispatcher)
|
||||
mockkStatic(Log::class)
|
||||
every { Log.d(any(), any()) } returns 0
|
||||
every { Log.i(any(), any()) } returns 0
|
||||
every { Log.w(any(), any<String>()) } returns 0
|
||||
every { Log.e(any(), any()) } returns 0
|
||||
|
||||
mockkObject(ApiClient)
|
||||
mockApi = mockk(relaxed = true)
|
||||
every { ApiClient.hermesApi } returns mockApi
|
||||
|
||||
coEvery { mockApi.getProfiles() } returns
|
||||
Response.success(
|
||||
ProfilesResponse(
|
||||
profiles = listOf(botA, botB),
|
||||
),
|
||||
)
|
||||
|
||||
mockkObject(HermesWsClient)
|
||||
every { HermesWsClient.events } returns eventsFlow
|
||||
every { HermesWsClient.send(any(), any(), any()) } returns "req-1"
|
||||
every { HermesWsClient.sendMessage(any(), any(), any()) } returns "req-msg"
|
||||
|
||||
val createDeferred = CompletableDeferred<Any?>()
|
||||
createDeferred.complete(mapOf("session_id" to "session-scout-1"))
|
||||
every { HermesWsClient.request(any(), any(), any()) } returns createDeferred
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
unmockkObject(ApiClient)
|
||||
unmockkObject(HermesWsClient)
|
||||
unmockkStatic(Log::class)
|
||||
Dispatchers.resetMain()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testLoadGroup_resolvesMembers() =
|
||||
runTest(testDispatcher) {
|
||||
val viewModel = GroupChatViewModel("Dev Team", ioDispatcher = testDispatcher)
|
||||
advanceUntilIdle()
|
||||
|
||||
val state = viewModel.uiState.value
|
||||
assertFalse(state.isLoading)
|
||||
assertNull(state.errorMessage)
|
||||
assertEquals(2, state.members.size)
|
||||
assertEquals("scout", state.members[0].name)
|
||||
assertEquals("coder", state.members[1].name)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testLoadGroup_hydratesFromSyncSnapshot() =
|
||||
runTest(testDispatcher) {
|
||||
val syncSnapshot =
|
||||
GroupChatSyncSnapshot(
|
||||
version = 3,
|
||||
updatedAt = 1724000000L,
|
||||
rooms =
|
||||
mapOf(
|
||||
"name:Dev Team" to
|
||||
GroupChatRoomMeta(
|
||||
name = "Dev Team",
|
||||
members = listOf(JsonPrimitive("scout"), JsonPrimitive("coder")),
|
||||
log =
|
||||
listOf(
|
||||
GroupChatSyncLogEntry(
|
||||
id = "msg-prev-1",
|
||||
from = GroupChatSyncFrom(kind = "user", name = "You"),
|
||||
text = "Initial design spec",
|
||||
at = 1724000000L,
|
||||
),
|
||||
GroupChatSyncLogEntry(
|
||||
id = "msg-prev-2",
|
||||
from = GroupChatSyncFrom(kind = "member", name = "scout"),
|
||||
text = "Looks solid, ready to build.",
|
||||
at = 1724000010L,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
val defaultProfileWithSnapshot =
|
||||
ProfileInfo(
|
||||
name = "default",
|
||||
is_default = true,
|
||||
ui_meta = mapOf("hermes-bots-groups" to json.encodeToJsonElement(syncSnapshot)),
|
||||
)
|
||||
|
||||
coEvery { mockApi.getProfiles() } returns
|
||||
Response.success(
|
||||
ProfilesResponse(
|
||||
profiles = listOf(defaultProfileWithSnapshot, botA, botB),
|
||||
),
|
||||
)
|
||||
|
||||
val viewModel = GroupChatViewModel("Dev Team", ioDispatcher = testDispatcher)
|
||||
advanceUntilIdle()
|
||||
|
||||
val state = viewModel.uiState.value
|
||||
assertEquals(2, state.messages.size)
|
||||
assertEquals("msg-prev-1", state.messages[0].id)
|
||||
assertTrue(state.messages[0].isUser)
|
||||
assertEquals("Initial design spec", state.messages[0].text)
|
||||
|
||||
assertEquals("msg-prev-2", state.messages[1].id)
|
||||
assertFalse(state.messages[1].isUser)
|
||||
assertEquals("scout", state.messages[1].senderName)
|
||||
assertEquals("Looks solid, ready to build.", state.messages[1].text)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSendMessage_streamsTokensAndCompletes() =
|
||||
runTest(testDispatcher) {
|
||||
val viewModel = GroupChatViewModel("Dev Team", ioDispatcher = testDispatcher)
|
||||
testScheduler.runCurrent()
|
||||
|
||||
// Send message targeting only @scout
|
||||
viewModel.sendMessage("@scout what's the status?")
|
||||
testScheduler.runCurrent()
|
||||
|
||||
// State should contain user message
|
||||
val userMsg = viewModel.uiState.value.messages.first()
|
||||
assertTrue(userMsg.isUser)
|
||||
assertEquals("@scout what's the status?", userMsg.text)
|
||||
|
||||
// Stream tokens
|
||||
eventsFlow.emit(WsEvent.MessageToken(token = "Building ", sessionId = "session-scout-1"))
|
||||
testScheduler.runCurrent()
|
||||
|
||||
val streamingMsg = viewModel.uiState.value.messages.last()
|
||||
assertFalse(streamingMsg.isUser)
|
||||
assertEquals("scout", streamingMsg.senderName)
|
||||
assertEquals("Building ", streamingMsg.text)
|
||||
assertTrue(streamingMsg.isStreaming)
|
||||
|
||||
eventsFlow.emit(WsEvent.MessageToken(token = "the APK...", sessionId = "session-scout-1"))
|
||||
testScheduler.runCurrent()
|
||||
|
||||
val updatedStreaming = viewModel.uiState.value.messages.last()
|
||||
assertEquals("Building the APK...", updatedStreaming.text)
|
||||
|
||||
// Complete turn
|
||||
eventsFlow.emit(
|
||||
WsEvent.MessageComplete(
|
||||
text = "Building the APK now! Everything looks green.",
|
||||
sessionId = "session-scout-1",
|
||||
),
|
||||
)
|
||||
testScheduler.runCurrent()
|
||||
|
||||
val finalState = viewModel.uiState.value
|
||||
assertEquals(2, finalState.messages.size)
|
||||
val botMsg = finalState.messages[1]
|
||||
assertEquals("Building the APK now! Everything looks green.", botMsg.text)
|
||||
assertFalse(botMsg.isStreaming)
|
||||
assertNull(finalState.activeSpeaker)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSendMessage_filtersOutPass() =
|
||||
runTest(testDispatcher) {
|
||||
val viewModel = GroupChatViewModel("Dev Team", ioDispatcher = testDispatcher)
|
||||
testScheduler.runCurrent()
|
||||
|
||||
viewModel.sendMessage("@scout any updates?")
|
||||
testScheduler.runCurrent()
|
||||
|
||||
eventsFlow.emit(
|
||||
WsEvent.MessageComplete(
|
||||
text = "(pass)",
|
||||
sessionId = "session-scout-1",
|
||||
),
|
||||
)
|
||||
testScheduler.runCurrent()
|
||||
|
||||
val finalState = viewModel.uiState.value
|
||||
// Only the user message should remain; (pass) is filtered out
|
||||
assertEquals(1, finalState.messages.size)
|
||||
assertTrue(finalState.messages.first().isUser)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user