refactor(#1375): type subagent list and tail RPC params (#1389)

This commit is contained in:
m57 authored and GitHub committed 2026-09-29 22:02:07 +03:00
1 parent c8edf3665f
commit 27ce528005
6 files changed
+111 -27

No files matched your search

@@ -3,6 +3,9 @@ package com.m57.hermescontrol.data.ws
import android.util.Log import android.util.Log
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 com.m57.hermescontrol.data.ws.contract.RpcMethods
import com.m57.hermescontrol.data.ws.contract.SessionIdParams
import com.m57.hermescontrol.data.ws.contract.SubagentTailParams
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonArray import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.JsonElement
@@ -33,12 +36,11 @@ object SubagentRepository {
if (sessionId.isBlank()) return null if (sessionId.isBlank()) return null
return try { return try {
val result = val result =
HermesWsClient HermesWsClient.call(
.request( RpcMethods.SUBAGENT_LIST,
WsMethods.SUBAGENT_LIST, SessionIdParams(sessionId),
mapOf("session_id" to sessionId), suppressErrorEvent = true,
suppressErrorEvent = true, )
).await()
decode<SubagentListResponse>(result) decode<SubagentListResponse>(result)
} catch (e: Exception) { } catch (e: Exception) {
Log.w(TAG, "subagent.list request failed for session $sessionId: ${e.message}") Log.w(TAG, "subagent.list request failed for session $sessionId: ${e.message}")
@@ -58,18 +60,12 @@ object SubagentRepository {
): SubagentTailResponse? { ): SubagentTailResponse? {
// Contract (SubagentIdParams): session_id and subagent_id are required, extra keys forbidden (#1379). // Contract (SubagentIdParams): session_id and subagent_id are required, extra keys forbidden (#1379).
if (subagentId.isBlank() || sessionId.isBlank()) return null if (subagentId.isBlank() || sessionId.isBlank()) return null
val params =
mapOf<String, Any>(
"session_id" to sessionId,
"subagent_id" to subagentId,
)
val result = val result =
HermesWsClient HermesWsClient.call(
.request( RpcMethods.SUBAGENT_TAIL,
WsMethods.SUBAGENT_TAIL, SubagentTailParams(sessionId, subagentId),
params, suppressErrorEvent = true,
suppressErrorEvent = true, )
).await()
return decode<SubagentTailResponse>(result) return decode<SubagentTailResponse>(result)
} }
@@ -331,6 +331,22 @@ object RpcMethods {
JsonElement.serializer(), JsonElement.serializer(),
) )
// Passthrough JsonElement results: callers keep their existing legacy decoders.
val SUBAGENT_LIST: RpcMethod<SessionIdParams, JsonElement> =
RpcMethod(
WsMethods.SUBAGENT_LIST,
SessionIdParams.serializer(),
JsonElement.serializer(),
)
// Passthrough JsonElement results: callers keep their existing legacy decoders.
val SUBAGENT_TAIL: RpcMethod<SubagentTailParams, JsonElement> =
RpcMethod(
WsMethods.SUBAGENT_TAIL,
SubagentTailParams.serializer(),
JsonElement.serializer(),
)
/** Every typed method. GatewayContractTest iterates this; future migrations append here. */ /** Every typed method. GatewayContractTest iterates this; future migrations append here. */
val all: List<RpcMethod<*, *>> = val all: List<RpcMethod<*, *>> =
listOf( listOf(
@@ -374,5 +390,7 @@ object RpcMethods {
CONNECTORS_TOOLS, CONNECTORS_TOOLS,
CONNECTION_RESPOND, CONNECTION_RESPOND,
CONNECTORS_OPERATION_WAKE, CONNECTORS_OPERATION_WAKE,
SUBAGENT_LIST,
SUBAGENT_TAIL,
) )
} }
@@ -0,0 +1,10 @@
package com.m57.hermescontrol.data.ws.contract
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class SubagentTailParams(
@SerialName("session_id") val sessionId: String,
@SerialName("subagent_id") val subagentId: String,
)
@@ -322,8 +322,6 @@ class RawRpcParamsRatchetTest {
"data/ws/PluginManageRepository.kt::PLUGINS_MANAGE", "data/ws/PluginManageRepository.kt::PLUGINS_MANAGE",
"data/ws/PluginRemovalRepository.kt::PLUGINS_MANAGE", "data/ws/PluginRemovalRepository.kt::PLUGINS_MANAGE",
"data/ws/ProjectsSource.kt::PROJECTS_LIST", "data/ws/ProjectsSource.kt::PROJECTS_LIST",
"data/ws/SubagentRepository.kt::SUBAGENT_LIST",
"data/ws/SubagentRepository.kt::SUBAGENT_TAIL",
"ui/bots/BotsViewModel.kt::PROFILES_CONFIGURE", "ui/bots/BotsViewModel.kt::PROFILES_CONFIGURE",
"ui/bots/BotsViewModel.kt::PROFILES_CONFIGURE", "ui/bots/BotsViewModel.kt::PROFILES_CONFIGURE",
"ui/bots/BotsViewModel.kt::PROFILES_CONFIGURE", "ui/bots/BotsViewModel.kt::PROFILES_CONFIGURE",
@@ -3,13 +3,17 @@ 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 com.m57.hermescontrol.data.ws.contract.RpcMethods
import com.m57.hermescontrol.data.ws.contract.SessionIdParams
import com.m57.hermescontrol.data.ws.contract.SubagentTailParams
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.mockkObject import io.mockk.mockkObject
import io.mockk.slot import io.mockk.slot
import io.mockk.unmockkAll import io.mockk.unmockkAll
import io.mockk.verify
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.put
import org.junit.After import org.junit.After
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse import org.junit.Assert.assertFalse
@@ -24,17 +28,31 @@ class SubagentRepositoryTest {
unmockkAll() unmockkAll()
} }
@Test
fun listSubagentsPreservesSuppressedErrorsAndDecodesResult() {
val captured = slot<SessionIdParams>()
mockkObject(HermesWsClient)
coEvery { HermesWsClient.call(RpcMethods.SUBAGENT_LIST, capture(captured), any(), true) } returns
buildJsonObject { put("subagents", kotlinx.serialization.json.buildJsonArray { }) }
val response = runBlocking { SubagentRepository.listSubagents("s1") }
assertEquals(SessionIdParams("s1"), captured.captured)
assertNotNull(response)
assertTrue(response?.subagents?.isEmpty() == true)
}
@Test @Test
fun tailSubagentSendsExactlySessionIdAndSubagentId() { fun tailSubagentSendsExactlySessionIdAndSubagentId() {
// #1379: the contract forbids extra keys (max_bytes) and requires session_id. // #1379: the contract forbids extra keys (max_bytes) and requires session_id.
val captured = slot<Map<String, Any>>() val captured = slot<SubagentTailParams>()
mockkObject(HermesWsClient) mockkObject(HermesWsClient)
every { HermesWsClient.request(WsMethods.SUBAGENT_TAIL, capture(captured), any(), any()) } returns coEvery { HermesWsClient.call(RpcMethods.SUBAGENT_TAIL, capture(captured), any(), true) } returns
CompletableDeferred<Any?>(mapOf("subagent_id" to "sub-1")) buildJsonObject { put("subagent_id", "sub-1") }
runBlocking { SubagentRepository.tailSubagent("s1", "sub-1") } runBlocking { SubagentRepository.tailSubagent("s1", "sub-1") }
assertEquals(mapOf<String, Any>("session_id" to "s1", "subagent_id" to "sub-1"), captured.captured) assertEquals(SubagentTailParams("s1", "sub-1"), captured.captured)
} }
@Test @Test
@@ -44,7 +62,7 @@ class SubagentRepositoryTest {
assertNull(SubagentRepository.tailSubagent("", "sub-1")) assertNull(SubagentRepository.tailSubagent("", "sub-1"))
assertNull(SubagentRepository.tailSubagent("s1", " ")) assertNull(SubagentRepository.tailSubagent("s1", " "))
} }
verify(exactly = 0) { HermesWsClient.request(any(), any(), any(), any()) } coVerify(exactly = 0) { HermesWsClient.call(RpcMethods.SUBAGENT_TAIL, any(), any(), any()) }
} }
@Test @Test
@@ -0,0 +1,44 @@
package com.m57.hermescontrol.data.ws.contract
import com.m57.hermescontrol.data.remote.OkHttpProvider
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.put
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
class SubagentTypesTest {
@Test
fun listEncodesOnlySessionId() {
val encoded = OkHttpProvider.json.encodeToJsonElement(SessionIdParams.serializer(), SessionIdParams("s1"))
assertEquals(buildJsonObject { put("session_id", "s1") }, encoded)
}
@Test
fun tailEncodesExactlyTheTwoRequiredIds() {
val encoded =
OkHttpProvider.json.encodeToJsonElement(
SubagentTailParams.serializer(),
SubagentTailParams("s1", "sub-1"),
)
assertEquals(
buildJsonObject {
put("session_id", "s1")
put("subagent_id", "sub-1")
},
encoded,
)
}
@Test
fun descriptorsAreRegisteredAndPreserveRawResults() {
assertTrue(RpcMethods.SUBAGENT_LIST in RpcMethods.all)
assertTrue(RpcMethods.SUBAGENT_TAIL in RpcMethods.all)
assertEquals("subagent.list", RpcMethods.SUBAGENT_LIST.name)
assertEquals("subagent.tail", RpcMethods.SUBAGENT_TAIL.name)
val raw: JsonElement = buildJsonObject { put("future_field", "retained") }
assertEquals(raw, OkHttpProvider.json.decodeFromJsonElement(RpcMethods.SUBAGENT_LIST.result, raw))
assertEquals(raw, OkHttpProvider.json.decodeFromJsonElement(RpcMethods.SUBAGENT_TAIL.result, raw))
}
}