diff --git a/app/src/main/java/com/m57/hermescontrol/data/ws/SubagentRepository.kt b/app/src/main/java/com/m57/hermescontrol/data/ws/SubagentRepository.kt index 1cda13ec..65ca1cb5 100644 --- a/app/src/main/java/com/m57/hermescontrol/data/ws/SubagentRepository.kt +++ b/app/src/main/java/com/m57/hermescontrol/data/ws/SubagentRepository.kt @@ -55,17 +55,14 @@ object SubagentRepository { suspend fun tailSubagent( sessionId: String, subagentId: String, - maxBytes: Int = 16384, ): SubagentTailResponse? { - if (subagentId.isBlank()) return null + // Contract (SubagentIdParams): session_id and subagent_id are required, extra keys forbidden (#1379). + if (subagentId.isBlank() || sessionId.isBlank()) return null val params = - mutableMapOf( + mapOf( + "session_id" to sessionId, "subagent_id" to subagentId, - "max_bytes" to maxBytes, ) - if (sessionId.isNotBlank()) { - params["session_id"] = sessionId - } val result = HermesWsClient .request( diff --git a/app/src/main/java/com/m57/hermescontrol/ui/chat/ChatSubagentsDelegate.kt b/app/src/main/java/com/m57/hermescontrol/ui/chat/ChatSubagentsDelegate.kt index eaa56386..0685c0ea 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/chat/ChatSubagentsDelegate.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/chat/ChatSubagentsDelegate.kt @@ -27,7 +27,6 @@ class ChatSubagentsDelegate( ) { companion object { private const val TAG = "ChatSubagentsDelegate" - const val SUBAGENT_TAIL_MAX_BYTES = 16384 } private var pollingJob: Job? = null @@ -130,7 +129,6 @@ class ChatSubagentsDelegate( subagentRepository.tailSubagent( sessionId = sessionId, subagentId = subagentId, - maxBytes = SUBAGENT_TAIL_MAX_BYTES, ) if (tailResponse != null) { diff --git a/app/src/test/java/com/m57/hermescontrol/data/ws/SubagentRepositoryTest.kt b/app/src/test/java/com/m57/hermescontrol/data/ws/SubagentRepositoryTest.kt index 10fc76d9..f1559c81 100644 --- a/app/src/test/java/com/m57/hermescontrol/data/ws/SubagentRepositoryTest.kt +++ b/app/src/test/java/com/m57/hermescontrol/data/ws/SubagentRepositoryTest.kt @@ -3,6 +3,14 @@ package com.m57.hermescontrol.data.ws import com.m57.hermescontrol.data.model.SubagentListItem import com.m57.hermescontrol.data.model.SubagentListResponse import com.m57.hermescontrol.data.model.SubagentTailResponse +import io.mockk.every +import io.mockk.mockkObject +import io.mockk.slot +import io.mockk.unmockkAll +import io.mockk.verify +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.runBlocking +import org.junit.After import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertNotNull @@ -11,6 +19,34 @@ import org.junit.Assert.assertTrue import org.junit.Test class SubagentRepositoryTest { + @After + fun tearDown() { + unmockkAll() + } + + @Test + fun tailSubagentSendsExactlySessionIdAndSubagentId() { + // #1379: the contract forbids extra keys (max_bytes) and requires session_id. + val captured = slot>() + mockkObject(HermesWsClient) + every { HermesWsClient.request(WsMethods.SUBAGENT_TAIL, capture(captured), any(), any()) } returns + CompletableDeferred(mapOf("subagent_id" to "sub-1")) + + runBlocking { SubagentRepository.tailSubagent("s1", "sub-1") } + + assertEquals(mapOf("session_id" to "s1", "subagent_id" to "sub-1"), captured.captured) + } + + @Test + fun tailSubagentSkipsTheCallWhenAnIdIsBlank() { + mockkObject(HermesWsClient) + runBlocking { + assertNull(SubagentRepository.tailSubagent("", "sub-1")) + assertNull(SubagentRepository.tailSubagent("s1", " ")) + } + verify(exactly = 0) { HermesWsClient.request(any(), any(), any(), any()) } + } + @Test fun testWsMethodsConstants() { assertEquals("subagent.list", WsMethods.SUBAGENT_LIST) diff --git a/app/src/test/java/com/m57/hermescontrol/ui/chat/ChatSubagentsDelegateTest.kt b/app/src/test/java/com/m57/hermescontrol/ui/chat/ChatSubagentsDelegateTest.kt index a3179f5d..cf4a55ab 100644 --- a/app/src/test/java/com/m57/hermescontrol/ui/chat/ChatSubagentsDelegateTest.kt +++ b/app/src/test/java/com/m57/hermescontrol/ui/chat/ChatSubagentsDelegateTest.kt @@ -448,7 +448,7 @@ class ChatSubagentsDelegateTest { ), ) - coEvery { mockRepository.tailSubagent("session-123", "sub-1", 16384) } returnsMany + coEvery { mockRepository.tailSubagent("session-123", "sub-1") } returnsMany listOf( SubagentTailResponse(subagentId = "sub-1", text = "Line 1\n", truncated = false), SubagentTailResponse(subagentId = "sub-1", text = "Line 1\nLine 2\n", truncated = false), @@ -485,7 +485,7 @@ class ChatSubagentsDelegateTest { ), ) - coEvery { mockRepository.tailSubagent("session-123", "sub-1", 16384) } returns + coEvery { mockRepository.tailSubagent("session-123", "sub-1") } returns SubagentTailResponse(subagentId = "sub-1", text = "Finished output", truncated = false) delegate.inspectSubagentTranscript("sub-1") @@ -507,7 +507,7 @@ class ChatSubagentsDelegateTest { advanceTimeBy(3000) // Verify tailSubagent was called at most once or twice, not indefinitely - coVerify(atMost = 2) { mockRepository.tailSubagent("session-123", "sub-1", 16384) } + coVerify(atMost = 2) { mockRepository.tailSubagent("session-123", "sub-1") } delegate.closeSubagentTranscript() } @@ -526,7 +526,7 @@ class ChatSubagentsDelegateTest { ), ) - coEvery { mockRepository.tailSubagent("session-123", "sub-1", 16384) } returns + coEvery { mockRepository.tailSubagent("session-123", "sub-1") } returns SubagentTailResponse(subagentId = "sub-1", text = "Output") delegate.inspectSubagentTranscript("sub-1") @@ -553,7 +553,7 @@ class ChatSubagentsDelegateTest { ), ) - coEvery { mockRepository.tailSubagent("session-123", "sub-1", 16384) } throws + coEvery { mockRepository.tailSubagent("session-123", "sub-1") } throws RuntimeException("Gateway connection error") delegate.inspectSubagentTranscript("sub-1") @@ -565,7 +565,7 @@ class ChatSubagentsDelegateTest { assertFalse(transcript?.isLoading == true) // Now recover on retry - coEvery { mockRepository.tailSubagent("session-123", "sub-1", 16384) } returns + coEvery { mockRepository.tailSubagent("session-123", "sub-1") } returns SubagentTailResponse(subagentId = "sub-1", text = "Recovered logs") delegate.retryTranscript()