@@ -55,17 +55,14 @@ object SubagentRepository {
|
|||||||
suspend fun tailSubagent(
|
suspend fun tailSubagent(
|
||||||
sessionId: String,
|
sessionId: String,
|
||||||
subagentId: String,
|
subagentId: String,
|
||||||
maxBytes: Int = 16384,
|
|
||||||
): SubagentTailResponse? {
|
): 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 =
|
val params =
|
||||||
mutableMapOf<String, Any>(
|
mapOf<String, Any>(
|
||||||
|
"session_id" to sessionId,
|
||||||
"subagent_id" to subagentId,
|
"subagent_id" to subagentId,
|
||||||
"max_bytes" to maxBytes,
|
|
||||||
)
|
)
|
||||||
if (sessionId.isNotBlank()) {
|
|
||||||
params["session_id"] = sessionId
|
|
||||||
}
|
|
||||||
val result =
|
val result =
|
||||||
HermesWsClient
|
HermesWsClient
|
||||||
.request(
|
.request(
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ class ChatSubagentsDelegate(
|
|||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "ChatSubagentsDelegate"
|
private const val TAG = "ChatSubagentsDelegate"
|
||||||
const val SUBAGENT_TAIL_MAX_BYTES = 16384
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private var pollingJob: Job? = null
|
private var pollingJob: Job? = null
|
||||||
@@ -130,7 +129,6 @@ class ChatSubagentsDelegate(
|
|||||||
subagentRepository.tailSubagent(
|
subagentRepository.tailSubagent(
|
||||||
sessionId = sessionId,
|
sessionId = sessionId,
|
||||||
subagentId = subagentId,
|
subagentId = subagentId,
|
||||||
maxBytes = SUBAGENT_TAIL_MAX_BYTES,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if (tailResponse != null) {
|
if (tailResponse != null) {
|
||||||
|
|||||||
@@ -3,6 +3,14 @@ package com.m57.hermescontrol.data.ws
|
|||||||
import com.m57.hermescontrol.data.model.SubagentListItem
|
import com.m57.hermescontrol.data.model.SubagentListItem
|
||||||
import com.m57.hermescontrol.data.model.SubagentListResponse
|
import com.m57.hermescontrol.data.model.SubagentListResponse
|
||||||
import com.m57.hermescontrol.data.model.SubagentTailResponse
|
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.assertEquals
|
||||||
import org.junit.Assert.assertFalse
|
import org.junit.Assert.assertFalse
|
||||||
import org.junit.Assert.assertNotNull
|
import org.junit.Assert.assertNotNull
|
||||||
@@ -11,6 +19,34 @@ import org.junit.Assert.assertTrue
|
|||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
class SubagentRepositoryTest {
|
class SubagentRepositoryTest {
|
||||||
|
@After
|
||||||
|
fun tearDown() {
|
||||||
|
unmockkAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun tailSubagentSendsExactlySessionIdAndSubagentId() {
|
||||||
|
// #1379: the contract forbids extra keys (max_bytes) and requires session_id.
|
||||||
|
val captured = slot<Map<String, Any>>()
|
||||||
|
mockkObject(HermesWsClient)
|
||||||
|
every { HermesWsClient.request(WsMethods.SUBAGENT_TAIL, capture(captured), any(), any()) } returns
|
||||||
|
CompletableDeferred<Any?>(mapOf("subagent_id" to "sub-1"))
|
||||||
|
|
||||||
|
runBlocking { SubagentRepository.tailSubagent("s1", "sub-1") }
|
||||||
|
|
||||||
|
assertEquals(mapOf<String, Any>("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
|
@Test
|
||||||
fun testWsMethodsConstants() {
|
fun testWsMethodsConstants() {
|
||||||
assertEquals("subagent.list", WsMethods.SUBAGENT_LIST)
|
assertEquals("subagent.list", WsMethods.SUBAGENT_LIST)
|
||||||
|
|||||||
@@ -448,7 +448,7 @@ class ChatSubagentsDelegateTest {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
coEvery { mockRepository.tailSubagent("session-123", "sub-1", 16384) } returnsMany
|
coEvery { mockRepository.tailSubagent("session-123", "sub-1") } returnsMany
|
||||||
listOf(
|
listOf(
|
||||||
SubagentTailResponse(subagentId = "sub-1", text = "Line 1\n", truncated = false),
|
SubagentTailResponse(subagentId = "sub-1", text = "Line 1\n", truncated = false),
|
||||||
SubagentTailResponse(subagentId = "sub-1", text = "Line 1\nLine 2\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)
|
SubagentTailResponse(subagentId = "sub-1", text = "Finished output", truncated = false)
|
||||||
|
|
||||||
delegate.inspectSubagentTranscript("sub-1")
|
delegate.inspectSubagentTranscript("sub-1")
|
||||||
@@ -507,7 +507,7 @@ class ChatSubagentsDelegateTest {
|
|||||||
|
|
||||||
advanceTimeBy(3000)
|
advanceTimeBy(3000)
|
||||||
// Verify tailSubagent was called at most once or twice, not indefinitely
|
// 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()
|
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")
|
SubagentTailResponse(subagentId = "sub-1", text = "Output")
|
||||||
|
|
||||||
delegate.inspectSubagentTranscript("sub-1")
|
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")
|
RuntimeException("Gateway connection error")
|
||||||
|
|
||||||
delegate.inspectSubagentTranscript("sub-1")
|
delegate.inspectSubagentTranscript("sub-1")
|
||||||
@@ -565,7 +565,7 @@ class ChatSubagentsDelegateTest {
|
|||||||
assertFalse(transcript?.isLoading == true)
|
assertFalse(transcript?.isLoading == true)
|
||||||
|
|
||||||
// Now recover on retry
|
// 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")
|
SubagentTailResponse(subagentId = "sub-1", text = "Recovered logs")
|
||||||
|
|
||||||
delegate.retryTranscript()
|
delegate.retryTranscript()
|
||||||
|
|||||||
Reference in New Issue
Block a user