fix(#1174): preserve pending process state and match tool events by ID (#1180)

This commit is contained in:
M57 authored and GitHub committed 2026-09-17 19:51:04 +03:00
1 parent 880adbef4f
commit 1ab30f1e90
9 files changed
+179 -42

No files matched your search

@@ -980,7 +980,12 @@ class EventParserTest {
mapOf(
"type" to "tool.progress",
"session_id" to "sess-123",
"payload" to mapOf("name" to "web_search", "preview" to "downloading content..."),
"payload" to
mapOf(
"tool_id" to "call-progress",
"name" to "web_search",
"preview" to "downloading content...",
),
),
)
val event = EventParser.parse(response)
@@ -989,6 +994,7 @@ class EventParserTest {
assertEquals("web_search", toolProgress.name)
assertEquals("downloading content...", toolProgress.preview)
assertEquals("sess-123", toolProgress.sessionId)
assertEquals("call-progress", toolProgress.toolId)
}
@Test
@@ -1004,7 +1010,7 @@ class EventParserTest {
mapOf(
"type" to "tool.generating",
"session_id" to "sess-123",
"payload" to mapOf("name" to "code_writer"),
"payload" to mapOf("tool_id" to "call-generating", "name" to "code_writer"),
),
)
val event = EventParser.parse(response)
@@ -1012,6 +1018,7 @@ class EventParserTest {
val toolGenerating = event as WsEvent.ToolGenerating
assertEquals("code_writer", toolGenerating.name)
assertEquals("sess-123", toolGenerating.sessionId)
assertEquals("call-generating", toolGenerating.toolId)
}
@Test
@@ -509,6 +509,103 @@ class ChatWsEventReducerTest {
assertTrue(result.state.todos[1].isInProgress)
}
@Test
fun testToolLifecycle_sameNameUsesToolIdForProgressAndCompletion() {
val state = ChatUiState(currentSessionId = "session-1")
val first =
ChatWsEventReducer.reduce(
state,
StreamingState(),
WsEvent.ToolStart(
name = "process_manage",
data = mapOf("tool_id" to "call-1", "action" to "wait"),
sessionId = "session-1",
),
"session-1",
)
val second =
ChatWsEventReducer.reduce(
first.state,
first.streamingState,
WsEvent.ToolStart(
name = "process_manage",
data = mapOf("tool_id" to "call-2", "action" to "wait"),
sessionId = "session-1",
),
"session-1",
)
val progress =
ChatWsEventReducer.reduce(
second.state,
second.streamingState,
WsEvent.ToolProgress(
name = "process_manage",
preview = "call-1 is still waiting",
sessionId = "session-1",
toolId = "call-1",
),
"session-1",
)
assertEquals("call-1 is still waiting", progress.state.messages[0].progressPreview)
assertNull(progress.state.messages[1].progressPreview)
val completed =
ChatWsEventReducer.reduce(
progress.state,
progress.streamingState,
WsEvent.ToolComplete(
name = "process_manage",
data = mapOf("tool_id" to "call-1", "output" to "finished"),
sessionId = "session-1",
),
"session-1",
)
assertEquals(ToolStatus.COMPLETED, completed.state.messages[0].toolStatus)
assertEquals(ToolStatus.RUNNING, completed.state.messages[1].toolStatus)
}
@Test
fun testToolOutputRisk_sameNameUsesToolId() {
val state = ChatUiState(currentSessionId = "session-1")
val first =
ChatWsEventReducer.reduce(
state,
StreamingState(),
WsEvent.ToolStart("terminal", mapOf("tool_id" to "call-1"), "session-1"),
"session-1",
)
val second =
ChatWsEventReducer.reduce(
first.state,
first.streamingState,
WsEvent.ToolStart("terminal", mapOf("tool_id" to "call-2"), "session-1"),
"session-1",
)
val result =
ChatWsEventReducer.reduce(
second.state,
second.streamingState,
WsEvent.ToolOutputRisk(
toolId = "call-1",
name = "terminal",
risk = "high",
findings = listOf("secret"),
sessionId = "session-1",
),
"session-1",
)
assertEquals(
"high",
result.state.messages[0]
.toolOutputRiskData
?.risk,
)
assertNull(result.state.messages[1].toolOutputRiskData)
}
@Test
fun testHydrateTodosFromMessages_parsesStoredToolMessage() {
val todoMessage =
@@ -281,6 +281,21 @@ class ToolViewBuilderTest {
assertTrue(view.detail.contains("sleep 100"))
}
@Test
fun `running process_manage shows pending detail instead of completion`() {
val view =
build(
"process_manage",
"""{"action":"wait","session_id":"proc-1"}""",
"""{"tool_id":"call-1","name":"process_manage","args":{"action":"wait"}}""",
running = true,
)
assertEquals(ToolViewStatus.RUNNING, view.status)
assertEquals("⏳ wait in progress", view.detail)
assertFalse(view.detail.contains("done"))
}
@Test
fun `todo_list alias activates TodoRenderer`() {
val view =