From c96a5ff4daefca3f2d34aa5d0942f7ca08b77891 Mon Sep 17 00:00:00 2001 From: M57 <41645758+Hy4ri@users.noreply.github.com> Date: Sat, 19 Sep 2026 20:08:11 +0300 Subject: [PATCH] fix(#1197): advertise server request capability (#1198) --- .../hermescontrol/data/ws/HermesWsClient.kt | 8 ++++ .../m57/hermescontrol/data/ws/WsMethods.kt | 1 + .../data/ws/HermesWsClientTest.kt | 45 +++++++++++++++++++ 3 files changed, 54 insertions(+) diff --git a/app/src/main/java/com/m57/hermescontrol/data/ws/HermesWsClient.kt b/app/src/main/java/com/m57/hermescontrol/data/ws/HermesWsClient.kt index 57b98eef..76aa7c87 100644 --- a/app/src/main/java/com/m57/hermescontrol/data/ws/HermesWsClient.kt +++ b/app/src/main/java/com/m57/hermescontrol/data/ws/HermesWsClient.kt @@ -354,6 +354,14 @@ object HermesWsClient { wsScope.launch { events.collect { event -> if (event is WsEvent.GatewayReady) { + // Advertise support for gateway server→client requests (issue #1197). + // Older gateways may reject this method; that is harmless. + runCatching { + send( + WsMethods.CLIENT_CAPABILITIES, + mapOf("server_requests" to true), + ) + } val epoch = event.data?.get("replay_epoch") as? String if (!epoch.isNullOrEmpty()) { if (replayEpoch != null && replayEpoch != epoch) { diff --git a/app/src/main/java/com/m57/hermescontrol/data/ws/WsMethods.kt b/app/src/main/java/com/m57/hermescontrol/data/ws/WsMethods.kt index 45727e0a..bb81bd5c 100644 --- a/app/src/main/java/com/m57/hermescontrol/data/ws/WsMethods.kt +++ b/app/src/main/java/com/m57/hermescontrol/data/ws/WsMethods.kt @@ -66,6 +66,7 @@ object WsMethods { /** Inline WebSocket read-loop ping, bypasses server thread pool dispatch. */ const val GATEWAY_PING = "gateway.ping" + const val CLIENT_CAPABILITIES = "client.capabilities" // ── Commands catalog ────────────────────────────────────────────────── const val COMMANDS_CATALOG = "commands.catalog" diff --git a/app/src/test/java/com/m57/hermescontrol/data/ws/HermesWsClientTest.kt b/app/src/test/java/com/m57/hermescontrol/data/ws/HermesWsClientTest.kt index 1de377e9..cfaf1a57 100644 --- a/app/src/test/java/com/m57/hermescontrol/data/ws/HermesWsClientTest.kt +++ b/app/src/test/java/com/m57/hermescontrol/data/ws/HermesWsClientTest.kt @@ -176,6 +176,51 @@ class HermesWsClientTest { assertTrue(msg.contains(id)) } + @Test + fun gatewayReadyAdvertisesServerRequestCapability() { + val capabilityLatch = CountDownLatch(1) + var capabilityFrame: String? = null + + mockWebServer.enqueue( + MockResponse().withWebSocketUpgrade( + object : WebSocketListener() { + override fun onOpen( + webSocket: WebSocket, + response: okhttp3.Response, + ) { + webSocket.send( + """{"jsonrpc":"2.0","method":"event","params":{"type":"gateway.ready","payload":{}}}""", + ) + } + + override fun onMessage( + webSocket: WebSocket, + text: String, + ) { + val frame = Json.parseToJsonElement(text).jsonObject + if (frame["method"]?.jsonPrimitive?.content == WsMethods.CLIENT_CAPABILITIES) { + capabilityFrame = text + capabilityLatch.countDown() + } + } + }, + ), + ) + + HermesWsClient.connect() + + assertTrue("Capability advertisement not sent", capabilityLatch.await(5, TimeUnit.SECONDS)) + val frame = Json.parseToJsonElement(capabilityFrame ?: "{}").jsonObject + assertEquals(WsMethods.CLIENT_CAPABILITIES, frame["method"]?.jsonPrimitive?.content) + assertTrue( + frame["params"] + ?.jsonObject + ?.get("server_requests") + ?.jsonPrimitive + ?.content == "true", + ) + } + @Test fun testOpenRequestsReplayUsesTheLiveServerRequestDispatcher() = runBlocking {