* feat(#538): wire tool.progress/generating + subagent.* events into chat * fix(#538): remove subagent indicator on subagent.complete so it doesn't linger * style(#538): drop dead Elvis on non-nullable event.text in onMessageComplete --------- Co-authored-by: M57 <m57@hermes-control.app>
This commit is contained in:
1 parent
9e10c813d4
commit
ee28ee5ce2
9 files changed
+592
-2
No files matched your search
@@ -522,4 +522,86 @@ class EventParserTest {
|
||||
val event = EventParser.parse(response, """{"null":"type"}""")
|
||||
assertTrue(event is WsEvent.Unknown)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testParseToolProgress_returnsToolProgressEvent() {
|
||||
val response =
|
||||
createJsonRpcResponse(
|
||||
jsonrpc = "2.0",
|
||||
id = null,
|
||||
result = null,
|
||||
error = null,
|
||||
method = "event",
|
||||
params =
|
||||
mapOf(
|
||||
"type" to "tool.progress",
|
||||
"session_id" to "sess-123",
|
||||
"payload" to mapOf("name" to "web_search", "preview" to "downloading content..."),
|
||||
),
|
||||
)
|
||||
val event = EventParser.parse(response)
|
||||
assertTrue(event is WsEvent.ToolProgress)
|
||||
val toolProgress = event as WsEvent.ToolProgress
|
||||
assertEquals("web_search", toolProgress.name)
|
||||
assertEquals("downloading content...", toolProgress.preview)
|
||||
assertEquals("sess-123", toolProgress.sessionId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testParseToolGenerating_returnsToolGeneratingEvent() {
|
||||
val response =
|
||||
createJsonRpcResponse(
|
||||
jsonrpc = "2.0",
|
||||
id = null,
|
||||
result = null,
|
||||
error = null,
|
||||
method = "event",
|
||||
params =
|
||||
mapOf(
|
||||
"type" to "tool.generating",
|
||||
"session_id" to "sess-123",
|
||||
"payload" to mapOf("name" to "code_writer"),
|
||||
),
|
||||
)
|
||||
val event = EventParser.parse(response)
|
||||
assertTrue(event is WsEvent.ToolGenerating)
|
||||
val toolGenerating = event as WsEvent.ToolGenerating
|
||||
assertEquals("code_writer", toolGenerating.name)
|
||||
assertEquals("sess-123", toolGenerating.sessionId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testParseSubagentStart_returnsSubagentEvent() {
|
||||
val response =
|
||||
createJsonRpcResponse(
|
||||
jsonrpc = "2.0",
|
||||
id = null,
|
||||
result = null,
|
||||
error = null,
|
||||
method = "event",
|
||||
params =
|
||||
mapOf(
|
||||
"type" to "subagent.start",
|
||||
"session_id" to "sess-123",
|
||||
"payload" to
|
||||
mapOf(
|
||||
"goal" to "research oauth providers",
|
||||
"task_index" to 1,
|
||||
"task_count" to 5,
|
||||
"subagent_id" to "sub-456",
|
||||
"text" to "analyzing docs...",
|
||||
),
|
||||
),
|
||||
)
|
||||
val event = EventParser.parse(response)
|
||||
assertTrue(event is WsEvent.SubagentEvent)
|
||||
val subagentEvent = event as WsEvent.SubagentEvent
|
||||
assertEquals("subagent.start", subagentEvent.type)
|
||||
assertEquals("sess-123", subagentEvent.sessionId)
|
||||
assertEquals("research oauth providers", subagentEvent.payload?.get("goal"))
|
||||
assertEquals(1, (subagentEvent.payload?.get("task_index") as? Number)?.toInt())
|
||||
assertEquals(5, (subagentEvent.payload?.get("task_count") as? Number)?.toInt())
|
||||
assertEquals("sub-456", subagentEvent.payload?.get("subagent_id"))
|
||||
assertEquals("analyzing docs...", subagentEvent.payload?.get("text"))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,236 @@
|
||||
package com.m57.hermescontrol.ui.chat
|
||||
|
||||
import com.m57.hermescontrol.data.ws.WsEvent
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
|
||||
class ChatWsEventReducerTest {
|
||||
@Test
|
||||
fun testToolProgress_updatesProgressPreviewForMatchingRunningTool() {
|
||||
val initialMessage =
|
||||
ChatMessage(
|
||||
role = MessageRole.TOOL,
|
||||
content = "{}",
|
||||
toolName = "web_search",
|
||||
toolStatus = ToolStatus.RUNNING,
|
||||
)
|
||||
val state =
|
||||
ChatUiState(
|
||||
messages = listOf(initialMessage),
|
||||
currentSessionId = "session-1",
|
||||
)
|
||||
val event =
|
||||
WsEvent.ToolProgress(
|
||||
name = "web_search",
|
||||
preview = "fetching google...",
|
||||
sessionId = "session-1",
|
||||
)
|
||||
|
||||
val result =
|
||||
ChatWsEventReducer.reduce(
|
||||
state = state,
|
||||
streamingState = StreamingState(),
|
||||
event = event,
|
||||
currentSessionId = "session-1",
|
||||
)
|
||||
|
||||
assertEquals(1, result.state.messages.size)
|
||||
val updatedMessage = result.state.messages.first()
|
||||
assertEquals(ToolStatus.RUNNING, updatedMessage.toolStatus)
|
||||
assertEquals("fetching google...", updatedMessage.progressPreview)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testToolGenerating_clearsProgressPreviewForMatchingRunningTool() {
|
||||
val initialMessage =
|
||||
ChatMessage(
|
||||
role = MessageRole.TOOL,
|
||||
content = "{}",
|
||||
toolName = "code_writer",
|
||||
toolStatus = ToolStatus.RUNNING,
|
||||
progressPreview = "writing...",
|
||||
)
|
||||
val state =
|
||||
ChatUiState(
|
||||
messages = listOf(initialMessage),
|
||||
currentSessionId = "session-1",
|
||||
)
|
||||
val event =
|
||||
WsEvent.ToolGenerating(
|
||||
name = "code_writer",
|
||||
sessionId = "session-1",
|
||||
)
|
||||
|
||||
val result =
|
||||
ChatWsEventReducer.reduce(
|
||||
state = state,
|
||||
streamingState = StreamingState(),
|
||||
event = event,
|
||||
currentSessionId = "session-1",
|
||||
)
|
||||
|
||||
assertEquals(1, result.state.messages.size)
|
||||
val updatedMessage = result.state.messages.first()
|
||||
assertEquals(ToolStatus.RUNNING, updatedMessage.toolStatus)
|
||||
assertEquals("", updatedMessage.progressPreview)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSubagentEvent_appendsToSubagentIndicators() {
|
||||
val state =
|
||||
ChatUiState(
|
||||
currentSessionId = "session-1",
|
||||
subagentIndicators = emptyList(),
|
||||
)
|
||||
val event =
|
||||
WsEvent.SubagentEvent(
|
||||
type = "subagent.start",
|
||||
sessionId = "session-1",
|
||||
payload =
|
||||
mapOf(
|
||||
"goal" to "analyze repository",
|
||||
"task_index" to 2,
|
||||
"task_count" to 4,
|
||||
"subagent_id" to "sub-1",
|
||||
"text" to "analyzing files",
|
||||
),
|
||||
)
|
||||
|
||||
val result =
|
||||
ChatWsEventReducer.reduce(
|
||||
state = state,
|
||||
streamingState = StreamingState(),
|
||||
event = event,
|
||||
currentSessionId = "session-1",
|
||||
)
|
||||
|
||||
assertEquals(1, result.state.subagentIndicators.size)
|
||||
val indicator = result.state.subagentIndicators.first()
|
||||
assertEquals("subagent.start", indicator.type)
|
||||
assertEquals("analyze repository", indicator.goal)
|
||||
assertEquals(2, indicator.taskIndex)
|
||||
assertEquals(4, indicator.taskCount)
|
||||
assertEquals("sub-1", indicator.subagentId)
|
||||
assertEquals("analyzing files", indicator.text)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSubagentEvent_updatesExistingIndicatorBySubagentId() {
|
||||
val initialIndicator =
|
||||
SubagentIndicator(
|
||||
type = "subagent.start",
|
||||
goal = "analyze repository",
|
||||
taskIndex = 1,
|
||||
taskCount = 4,
|
||||
subagentId = "sub-1",
|
||||
text = "starting",
|
||||
)
|
||||
val state =
|
||||
ChatUiState(
|
||||
currentSessionId = "session-1",
|
||||
subagentIndicators = listOf(initialIndicator),
|
||||
)
|
||||
val event =
|
||||
WsEvent.SubagentEvent(
|
||||
type = "subagent.progress",
|
||||
sessionId = "session-1",
|
||||
payload =
|
||||
mapOf(
|
||||
"task_index" to 2,
|
||||
"subagent_id" to "sub-1",
|
||||
"text" to "in progress",
|
||||
),
|
||||
)
|
||||
|
||||
val result =
|
||||
ChatWsEventReducer.reduce(
|
||||
state = state,
|
||||
streamingState = StreamingState(),
|
||||
event = event,
|
||||
currentSessionId = "session-1",
|
||||
)
|
||||
|
||||
assertEquals(1, result.state.subagentIndicators.size)
|
||||
val indicator = result.state.subagentIndicators.first()
|
||||
assertEquals("subagent.progress", indicator.type)
|
||||
assertEquals("analyze repository", indicator.goal)
|
||||
assertEquals(2, indicator.taskIndex)
|
||||
assertEquals(4, indicator.taskCount)
|
||||
assertEquals("sub-1", indicator.subagentId)
|
||||
assertEquals("in progress", indicator.text)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSubagentComplete_removesIndicator() {
|
||||
val initialIndicator =
|
||||
SubagentIndicator(
|
||||
type = "subagent.start",
|
||||
goal = "analyze repository",
|
||||
taskIndex = 1,
|
||||
taskCount = 4,
|
||||
subagentId = "sub-1",
|
||||
text = "starting",
|
||||
)
|
||||
val state =
|
||||
ChatUiState(
|
||||
currentSessionId = "session-1",
|
||||
subagentIndicators = listOf(initialIndicator),
|
||||
)
|
||||
val event =
|
||||
WsEvent.SubagentEvent(
|
||||
type = "subagent.complete",
|
||||
sessionId = "session-1",
|
||||
payload =
|
||||
mapOf(
|
||||
"subagent_id" to "sub-1",
|
||||
"status" to "completed",
|
||||
"summary" to "done",
|
||||
),
|
||||
)
|
||||
|
||||
val result =
|
||||
ChatWsEventReducer.reduce(
|
||||
state = state,
|
||||
streamingState = StreamingState(),
|
||||
event = event,
|
||||
currentSessionId = "session-1",
|
||||
)
|
||||
|
||||
assertEquals(0, result.state.subagentIndicators.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSessionMismatch_isIgnored() {
|
||||
val initialMessage =
|
||||
ChatMessage(
|
||||
role = MessageRole.TOOL,
|
||||
content = "{}",
|
||||
toolName = "web_search",
|
||||
toolStatus = ToolStatus.RUNNING,
|
||||
)
|
||||
val state =
|
||||
ChatUiState(
|
||||
messages = listOf(initialMessage),
|
||||
currentSessionId = "session-1",
|
||||
)
|
||||
val event =
|
||||
WsEvent.ToolProgress(
|
||||
name = "web_search",
|
||||
preview = "fetching google...",
|
||||
sessionId = "session-different",
|
||||
)
|
||||
|
||||
val result =
|
||||
ChatWsEventReducer.reduce(
|
||||
state = state,
|
||||
streamingState = StreamingState(),
|
||||
event = event,
|
||||
currentSessionId = "session-1",
|
||||
)
|
||||
|
||||
assertEquals(1, result.state.messages.size)
|
||||
val updatedMessage = result.state.messages.first()
|
||||
assertEquals(ToolStatus.RUNNING, updatedMessage.toolStatus)
|
||||
assertEquals(null, updatedMessage.progressPreview)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user