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 65ca1cb5..2c6ecb7f 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 @@ -3,6 +3,9 @@ package com.m57.hermescontrol.data.ws import android.util.Log import com.m57.hermescontrol.data.model.SubagentListResponse 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.JsonArray import kotlinx.serialization.json.JsonElement @@ -33,12 +36,11 @@ object SubagentRepository { if (sessionId.isBlank()) return null return try { val result = - HermesWsClient - .request( - WsMethods.SUBAGENT_LIST, - mapOf("session_id" to sessionId), - suppressErrorEvent = true, - ).await() + HermesWsClient.call( + RpcMethods.SUBAGENT_LIST, + SessionIdParams(sessionId), + suppressErrorEvent = true, + ) decode(result) } catch (e: Exception) { Log.w(TAG, "subagent.list request failed for session $sessionId: ${e.message}") @@ -58,18 +60,12 @@ object SubagentRepository { ): SubagentTailResponse? { // Contract (SubagentIdParams): session_id and subagent_id are required, extra keys forbidden (#1379). if (subagentId.isBlank() || sessionId.isBlank()) return null - val params = - mapOf( - "session_id" to sessionId, - "subagent_id" to subagentId, - ) val result = - HermesWsClient - .request( - WsMethods.SUBAGENT_TAIL, - params, - suppressErrorEvent = true, - ).await() + HermesWsClient.call( + RpcMethods.SUBAGENT_TAIL, + SubagentTailParams(sessionId, subagentId), + suppressErrorEvent = true, + ) return decode(result) } diff --git a/app/src/main/java/com/m57/hermescontrol/data/ws/contract/RpcMethod.kt b/app/src/main/java/com/m57/hermescontrol/data/ws/contract/RpcMethod.kt index 8b0266f3..fdf91765 100644 --- a/app/src/main/java/com/m57/hermescontrol/data/ws/contract/RpcMethod.kt +++ b/app/src/main/java/com/m57/hermescontrol/data/ws/contract/RpcMethod.kt @@ -331,6 +331,22 @@ object RpcMethods { JsonElement.serializer(), ) + // Passthrough JsonElement results: callers keep their existing legacy decoders. + val SUBAGENT_LIST: RpcMethod = + RpcMethod( + WsMethods.SUBAGENT_LIST, + SessionIdParams.serializer(), + JsonElement.serializer(), + ) + + // Passthrough JsonElement results: callers keep their existing legacy decoders. + val SUBAGENT_TAIL: RpcMethod = + RpcMethod( + WsMethods.SUBAGENT_TAIL, + SubagentTailParams.serializer(), + JsonElement.serializer(), + ) + /** Every typed method. GatewayContractTest iterates this; future migrations append here. */ val all: List> = listOf( @@ -374,5 +390,7 @@ object RpcMethods { CONNECTORS_TOOLS, CONNECTION_RESPOND, CONNECTORS_OPERATION_WAKE, + SUBAGENT_LIST, + SUBAGENT_TAIL, ) } diff --git a/app/src/main/java/com/m57/hermescontrol/data/ws/contract/SubagentTailParams.kt b/app/src/main/java/com/m57/hermescontrol/data/ws/contract/SubagentTailParams.kt new file mode 100644 index 00000000..8ecebb25 --- /dev/null +++ b/app/src/main/java/com/m57/hermescontrol/data/ws/contract/SubagentTailParams.kt @@ -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, +) diff --git a/app/src/test/java/com/m57/hermescontrol/data/ws/RawRpcParamsRatchetTest.kt b/app/src/test/java/com/m57/hermescontrol/data/ws/RawRpcParamsRatchetTest.kt index 0eef3578..3831b09f 100644 --- a/app/src/test/java/com/m57/hermescontrol/data/ws/RawRpcParamsRatchetTest.kt +++ b/app/src/test/java/com/m57/hermescontrol/data/ws/RawRpcParamsRatchetTest.kt @@ -322,8 +322,6 @@ class RawRpcParamsRatchetTest { "data/ws/PluginManageRepository.kt::PLUGINS_MANAGE", "data/ws/PluginRemovalRepository.kt::PLUGINS_MANAGE", "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", 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 f1559c81..d44eb665 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,13 +3,17 @@ 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 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.slot import io.mockk.unmockkAll -import io.mockk.verify -import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.runBlocking +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.put import org.junit.After import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse @@ -24,17 +28,31 @@ class SubagentRepositoryTest { unmockkAll() } + @Test + fun listSubagentsPreservesSuppressedErrorsAndDecodesResult() { + val captured = slot() + 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 fun tailSubagentSendsExactlySessionIdAndSubagentId() { // #1379: the contract forbids extra keys (max_bytes) and requires session_id. - val captured = slot>() + val captured = slot() mockkObject(HermesWsClient) - every { HermesWsClient.request(WsMethods.SUBAGENT_TAIL, capture(captured), any(), any()) } returns - CompletableDeferred(mapOf("subagent_id" to "sub-1")) + coEvery { HermesWsClient.call(RpcMethods.SUBAGENT_TAIL, capture(captured), any(), true) } returns + buildJsonObject { put("subagent_id", "sub-1") } runBlocking { SubagentRepository.tailSubagent("s1", "sub-1") } - assertEquals(mapOf("session_id" to "s1", "subagent_id" to "sub-1"), captured.captured) + assertEquals(SubagentTailParams("s1", "sub-1"), captured.captured) } @Test @@ -44,7 +62,7 @@ class SubagentRepositoryTest { assertNull(SubagentRepository.tailSubagent("", "sub-1")) 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 diff --git a/app/src/test/java/com/m57/hermescontrol/data/ws/contract/SubagentTypesTest.kt b/app/src/test/java/com/m57/hermescontrol/data/ws/contract/SubagentTypesTest.kt new file mode 100644 index 00000000..9c888002 --- /dev/null +++ b/app/src/test/java/com/m57/hermescontrol/data/ws/contract/SubagentTypesTest.kt @@ -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)) + } +}