This commit is contained in:
11 files changed
+610
-62
No files matched your search
@@ -3,6 +3,8 @@ package com.m57.hermescontrol.data.ws
|
|||||||
import com.m57.hermescontrol.data.model.ActiveSessionsResponse
|
import com.m57.hermescontrol.data.model.ActiveSessionsResponse
|
||||||
import com.m57.hermescontrol.data.model.LiveSessionSnapshot
|
import com.m57.hermescontrol.data.model.LiveSessionSnapshot
|
||||||
import com.m57.hermescontrol.data.model.SessionLiveStatus
|
import com.m57.hermescontrol.data.model.SessionLiveStatus
|
||||||
|
import com.m57.hermescontrol.data.ws.contract.RpcMethods
|
||||||
|
import com.m57.hermescontrol.data.ws.contract.SessionActiveListParams
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
@@ -148,8 +150,8 @@ object SessionLiveStatusDecoder {
|
|||||||
* Production implementation of [SessionLiveStatusSource] backed by [HermesWsClient].
|
* Production implementation of [SessionLiveStatusSource] backed by [HermesWsClient].
|
||||||
*/
|
*/
|
||||||
class HermesSessionLiveStatusSource(
|
class HermesSessionLiveStatusSource(
|
||||||
private val rpcRequest: suspend (method: String, params: Map<String, Any>) -> Any? = { method, params ->
|
private val rpcRequest: suspend () -> Any? = {
|
||||||
HermesWsClient.request(method, params).await()
|
HermesWsClient.call(RpcMethods.SESSION_ACTIVE_LIST, SessionActiveListParams())
|
||||||
},
|
},
|
||||||
eventsProvider: () -> Flow<WsEvent> = { HermesWsClient.events },
|
eventsProvider: () -> Flow<WsEvent> = { HermesWsClient.events },
|
||||||
connectionStatusProvider: () -> StateFlow<ConnectionStatus> = { HermesWsClient.connectionStatus },
|
connectionStatusProvider: () -> StateFlow<ConnectionStatus> = { HermesWsClient.connectionStatus },
|
||||||
@@ -159,7 +161,7 @@ class HermesSessionLiveStatusSource(
|
|||||||
|
|
||||||
override suspend fun fetchActiveSessionsSnapshot(): LiveSessionSnapshot? =
|
override suspend fun fetchActiveSessionsSnapshot(): LiveSessionSnapshot? =
|
||||||
try {
|
try {
|
||||||
val raw = rpcRequest(WsMethods.SESSION_ACTIVE_LIST, emptyMap())
|
val raw = rpcRequest()
|
||||||
SessionLiveStatusDecoder.decodeSnapshot(raw)
|
SessionLiveStatusDecoder.decodeSnapshot(raw)
|
||||||
} catch (e: CancellationException) {
|
} catch (e: CancellationException) {
|
||||||
throw e
|
throw e
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package com.m57.hermescontrol.data.ws.contract
|
||||||
|
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class ApprovalPendingParams(
|
||||||
|
@SerialName("session_id") val sessionId: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class ApprovalReceivedParams(
|
||||||
|
@SerialName("session_id") val sessionId: String,
|
||||||
|
@SerialName("request_id") val requestId: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class ApprovalRespondParams(
|
||||||
|
@SerialName("session_id") val sessionId: String,
|
||||||
|
@SerialName("choice") val choice: String? = null,
|
||||||
|
@SerialName("all") val all: Boolean? = null,
|
||||||
|
@SerialName("request_id") val requestId: String? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class SessionActiveListParams(
|
||||||
|
@SerialName("current_session_id") val currentSessionId: String? = null,
|
||||||
|
)
|
||||||
@@ -10,6 +10,14 @@ class RpcMethod<P, R>(
|
|||||||
val result: KSerializer<R>,
|
val result: KSerializer<R>,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
interface TypedRpcSender {
|
||||||
|
fun <P> send(
|
||||||
|
method: RpcMethod<P, *>,
|
||||||
|
params: P,
|
||||||
|
onSent: ((String) -> Unit)? = null,
|
||||||
|
): String
|
||||||
|
}
|
||||||
|
|
||||||
object RpcMethods {
|
object RpcMethods {
|
||||||
val SESSION_EVENTS_SINCE: RpcMethod<SessionEventsSinceParams, SessionEventsSinceResult> =
|
val SESSION_EVENTS_SINCE: RpcMethod<SessionEventsSinceParams, SessionEventsSinceResult> =
|
||||||
RpcMethod(
|
RpcMethod(
|
||||||
@@ -110,6 +118,38 @@ object RpcMethods {
|
|||||||
PromptBtwResult.serializer(),
|
PromptBtwResult.serializer(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Passthrough JsonElement results: callers keep their existing legacy parsers.
|
||||||
|
val APPROVAL_PENDING: RpcMethod<ApprovalPendingParams, JsonElement> =
|
||||||
|
RpcMethod(
|
||||||
|
WsMethods.APPROVAL_PENDING,
|
||||||
|
ApprovalPendingParams.serializer(),
|
||||||
|
JsonElement.serializer(),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Passthrough JsonElement results: callers keep their existing legacy parsers.
|
||||||
|
val APPROVAL_RECEIVED: RpcMethod<ApprovalReceivedParams, JsonElement> =
|
||||||
|
RpcMethod(
|
||||||
|
WsMethods.APPROVAL_RECEIVED,
|
||||||
|
ApprovalReceivedParams.serializer(),
|
||||||
|
JsonElement.serializer(),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Passthrough JsonElement results: callers keep their existing legacy parsers.
|
||||||
|
val APPROVAL_RESPOND: RpcMethod<ApprovalRespondParams, JsonElement> =
|
||||||
|
RpcMethod(
|
||||||
|
WsMethods.APPROVAL_RESPOND,
|
||||||
|
ApprovalRespondParams.serializer(),
|
||||||
|
JsonElement.serializer(),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Passthrough JsonElement results: callers keep their existing legacy parsers.
|
||||||
|
val SESSION_ACTIVE_LIST: RpcMethod<SessionActiveListParams, JsonElement> =
|
||||||
|
RpcMethod(
|
||||||
|
WsMethods.SESSION_ACTIVE_LIST,
|
||||||
|
SessionActiveListParams.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(
|
||||||
@@ -127,5 +167,9 @@ object RpcMethods {
|
|||||||
SESSION_CONTEXT_BREAKDOWN,
|
SESSION_CONTEXT_BREAKDOWN,
|
||||||
SESSION_USAGE,
|
SESSION_USAGE,
|
||||||
PROMPT_BTW,
|
PROMPT_BTW,
|
||||||
|
APPROVAL_PENDING,
|
||||||
|
APPROVAL_RECEIVED,
|
||||||
|
APPROVAL_RESPOND,
|
||||||
|
SESSION_ACTIVE_LIST,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -2,6 +2,11 @@ package com.m57.hermescontrol.ui.chat
|
|||||||
|
|
||||||
import com.m57.hermescontrol.data.ws.WsEvent
|
import com.m57.hermescontrol.data.ws.WsEvent
|
||||||
import com.m57.hermescontrol.data.ws.WsMethods
|
import com.m57.hermescontrol.data.ws.WsMethods
|
||||||
|
import com.m57.hermescontrol.data.ws.contract.ApprovalPendingParams
|
||||||
|
import com.m57.hermescontrol.data.ws.contract.ApprovalReceivedParams
|
||||||
|
import com.m57.hermescontrol.data.ws.contract.ApprovalRespondParams
|
||||||
|
import com.m57.hermescontrol.data.ws.contract.RpcMethods
|
||||||
|
import com.m57.hermescontrol.data.ws.contract.TypedRpcSender
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
@@ -22,7 +27,7 @@ class ChatApprovalsDelegate(
|
|||||||
private val ioDispatcher: CoroutineDispatcher,
|
private val ioDispatcher: CoroutineDispatcher,
|
||||||
private val uiState: MutableStateFlow<ChatUiState>,
|
private val uiState: MutableStateFlow<ChatUiState>,
|
||||||
private val runtimeSessionId: () -> String?,
|
private val runtimeSessionId: () -> String?,
|
||||||
private val wsSend: (method: String, params: Map<String, Any>, onSent: ((String) -> Unit)?) -> Unit,
|
private val rpc: TypedRpcSender,
|
||||||
private val trackRequest: (id: String, method: String) -> Unit,
|
private val trackRequest: (id: String, method: String) -> Unit,
|
||||||
private val addSystemMessage: (text: String) -> Unit,
|
private val addSystemMessage: (text: String) -> Unit,
|
||||||
private val respondToServerRequest: ((String, JsonElement) -> Unit)? = null,
|
private val respondToServerRequest: ((String, JsonElement) -> Unit)? = null,
|
||||||
@@ -99,13 +104,12 @@ class ChatApprovalsDelegate(
|
|||||||
if (event.serverRequestId == null && requestId != null && sessionId != null) {
|
if (event.serverRequestId == null && requestId != null && sessionId != null) {
|
||||||
scope.launch(ioDispatcher) {
|
scope.launch(ioDispatcher) {
|
||||||
runCatching {
|
runCatching {
|
||||||
wsSend(
|
rpc.send(
|
||||||
WsMethods.APPROVAL_RECEIVED,
|
RpcMethods.APPROVAL_RECEIVED,
|
||||||
mapOf(
|
ApprovalReceivedParams(
|
||||||
"session_id" to sessionId,
|
sessionId = sessionId,
|
||||||
"request_id" to requestId,
|
requestId = requestId,
|
||||||
),
|
),
|
||||||
null,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -115,9 +119,9 @@ class ChatApprovalsDelegate(
|
|||||||
fun replayPendingApproval(sessionId: String) {
|
fun replayPendingApproval(sessionId: String) {
|
||||||
val targetSessionId = runtimeSessionId() ?: sessionId
|
val targetSessionId = runtimeSessionId() ?: sessionId
|
||||||
scope.launch(ioDispatcher) {
|
scope.launch(ioDispatcher) {
|
||||||
wsSend(
|
rpc.send(
|
||||||
WsMethods.APPROVAL_PENDING,
|
RpcMethods.APPROVAL_PENDING,
|
||||||
mapOf("session_id" to targetSessionId),
|
ApprovalPendingParams(sessionId = targetSessionId),
|
||||||
) { id -> trackRequest(id, WsMethods.APPROVAL_PENDING) }
|
) { id -> trackRequest(id, WsMethods.APPROVAL_PENDING) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -190,16 +194,14 @@ class ChatApprovalsDelegate(
|
|||||||
)
|
)
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
val params =
|
rpc.send(
|
||||||
mutableMapOf<String, Any>(
|
RpcMethods.APPROVAL_RESPOND,
|
||||||
"session_id" to sessionId,
|
ApprovalRespondParams(
|
||||||
"choice" to choice,
|
sessionId = sessionId,
|
||||||
"all" to false,
|
choice = choice,
|
||||||
)
|
all = false,
|
||||||
if (requestId != null) params["request_id"] = requestId
|
requestId = requestId,
|
||||||
wsSend(
|
),
|
||||||
WsMethods.APPROVAL_RESPOND,
|
|
||||||
params,
|
|
||||||
) { id -> trackRequest(id, WsMethods.APPROVAL_RESPOND) }
|
) { id -> trackRequest(id, WsMethods.APPROVAL_RESPOND) }
|
||||||
// The queue can hold more pendings — surface the next one.
|
// The queue can hold more pendings — surface the next one.
|
||||||
replayPendingApproval(sessionId)
|
replayPendingApproval(sessionId)
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ import com.m57.hermescontrol.data.ws.JsonRpcError
|
|||||||
import com.m57.hermescontrol.data.ws.WsEvent
|
import com.m57.hermescontrol.data.ws.WsEvent
|
||||||
import com.m57.hermescontrol.data.ws.WsMethods
|
import com.m57.hermescontrol.data.ws.WsMethods
|
||||||
import com.m57.hermescontrol.data.ws.contract.PromptBtwParams
|
import com.m57.hermescontrol.data.ws.contract.PromptBtwParams
|
||||||
|
import com.m57.hermescontrol.data.ws.contract.RpcMethod
|
||||||
import com.m57.hermescontrol.data.ws.contract.RpcMethods
|
import com.m57.hermescontrol.data.ws.contract.RpcMethods
|
||||||
import com.m57.hermescontrol.data.ws.contract.SessionBranchParams
|
import com.m57.hermescontrol.data.ws.contract.SessionBranchParams
|
||||||
import com.m57.hermescontrol.data.ws.contract.SessionBranchWholeParams
|
import com.m57.hermescontrol.data.ws.contract.SessionBranchWholeParams
|
||||||
@@ -48,6 +49,7 @@ import com.m57.hermescontrol.data.ws.contract.SessionIdParams
|
|||||||
import com.m57.hermescontrol.data.ws.contract.SessionInterruptParams
|
import com.m57.hermescontrol.data.ws.contract.SessionInterruptParams
|
||||||
import com.m57.hermescontrol.data.ws.contract.SessionListParams
|
import com.m57.hermescontrol.data.ws.contract.SessionListParams
|
||||||
import com.m57.hermescontrol.data.ws.contract.SessionResumeParams
|
import com.m57.hermescontrol.data.ws.contract.SessionResumeParams
|
||||||
|
import com.m57.hermescontrol.data.ws.contract.TypedRpcSender
|
||||||
import com.m57.hermescontrol.data.ws.toAny
|
import com.m57.hermescontrol.data.ws.toAny
|
||||||
import com.m57.hermescontrol.data.ws.toJsonElement
|
import com.m57.hermescontrol.data.ws.toJsonElement
|
||||||
import com.m57.hermescontrol.notification.ReplyNotificationTracker
|
import com.m57.hermescontrol.notification.ReplyNotificationTracker
|
||||||
@@ -711,7 +713,14 @@ class ChatViewModel(
|
|||||||
ioDispatcher = ioDispatcher,
|
ioDispatcher = ioDispatcher,
|
||||||
uiState = _uiState,
|
uiState = _uiState,
|
||||||
runtimeSessionId = { runtimeSessionId },
|
runtimeSessionId = { runtimeSessionId },
|
||||||
wsSend = { method, params, onSent -> wsClient.send(method, params, onSent) },
|
rpc =
|
||||||
|
object : TypedRpcSender {
|
||||||
|
override fun <P> send(
|
||||||
|
method: RpcMethod<P, *>,
|
||||||
|
params: P,
|
||||||
|
onSent: ((String) -> Unit)?,
|
||||||
|
): String = wsClient.send(method, params, onSent)
|
||||||
|
},
|
||||||
trackRequest = { id, method -> trackRequest(id, method) },
|
trackRequest = { id, method -> trackRequest(id, method) },
|
||||||
addSystemMessage = { text -> addSystemMessage(text) },
|
addSystemMessage = { text -> addSystemMessage(text) },
|
||||||
respondToServerRequest = { id, result -> wsClient.respondToServerRequest(id, result) },
|
respondToServerRequest = { id, result -> wsClient.respondToServerRequest(id, result) },
|
||||||
|
|||||||
@@ -0,0 +1,237 @@
|
|||||||
|
package com.m57.hermescontrol.data.ws
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import com.m57.hermescontrol.data.local.AuthManager
|
||||||
|
import com.m57.hermescontrol.data.remote.CookieManager
|
||||||
|
import com.m57.hermescontrol.data.remote.DashboardSessionTokenRefresher
|
||||||
|
import com.m57.hermescontrol.data.remote.OkHttpProvider
|
||||||
|
import com.m57.hermescontrol.data.remote.buildFakePersistentCookieJar
|
||||||
|
import com.m57.hermescontrol.data.ws.contract.ApprovalRespondParams
|
||||||
|
import com.m57.hermescontrol.data.ws.contract.RpcMethods
|
||||||
|
import com.m57.hermescontrol.data.ws.contract.SessionActiveListParams
|
||||||
|
import io.mockk.coEvery
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.mockk
|
||||||
|
import io.mockk.mockkObject
|
||||||
|
import io.mockk.mockkStatic
|
||||||
|
import io.mockk.unmockkAll
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import kotlinx.coroutines.withTimeout
|
||||||
|
import kotlinx.coroutines.withTimeoutOrNull
|
||||||
|
import kotlinx.serialization.json.JsonElement
|
||||||
|
import kotlinx.serialization.json.JsonObject
|
||||||
|
import kotlinx.serialization.json.JsonPrimitive
|
||||||
|
import kotlinx.serialization.json.booleanOrNull
|
||||||
|
import kotlinx.serialization.json.buildJsonObject
|
||||||
|
import kotlinx.serialization.json.put
|
||||||
|
import okhttp3.Response
|
||||||
|
import okhttp3.WebSocket
|
||||||
|
import okhttp3.WebSocketListener
|
||||||
|
import okhttp3.mockwebserver.MockResponse
|
||||||
|
import okhttp3.mockwebserver.MockWebServer
|
||||||
|
import org.junit.After
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertFalse
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
import java.util.concurrent.CountDownLatch
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
import java.util.concurrent.atomic.AtomicReference
|
||||||
|
|
||||||
|
class HermesWsClientApprovalsTest {
|
||||||
|
private lateinit var mockWebServer: MockWebServer
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setUp() {
|
||||||
|
mockkStatic(Log::class)
|
||||||
|
every { Log.d(any<String>(), any<String>()) } returns 0
|
||||||
|
every { Log.i(any<String>(), any<String>()) } returns 0
|
||||||
|
every { Log.w(any<String>(), any<String>()) } returns 0
|
||||||
|
every { Log.w(any<String>(), any<String>(), any<Throwable>()) } returns 0
|
||||||
|
every { Log.e(any<String>(), any<String>(), any()) } returns 0
|
||||||
|
|
||||||
|
mockWebServer = MockWebServer()
|
||||||
|
mockWebServer.start()
|
||||||
|
mockkObject(AuthManager)
|
||||||
|
every { AuthManager.wsUrl() } returns mockWebServer.url("/").toString().replace("http://", "ws://")
|
||||||
|
every { AuthManager.isAutoReconnect() } returns false
|
||||||
|
every { AuthManager.getSessionCookie() } returns null
|
||||||
|
every { AuthManager.serverStore } returns
|
||||||
|
mockk<com.m57.hermescontrol.data.config.ServerStore>().also {
|
||||||
|
every { it.getLatestState() } returns
|
||||||
|
com.m57.hermescontrol.data.config
|
||||||
|
.ServerStoreState()
|
||||||
|
}
|
||||||
|
CookieManager.setJarForTest(buildFakePersistentCookieJar())
|
||||||
|
mockkObject(DashboardSessionTokenRefresher)
|
||||||
|
coEvery { DashboardSessionTokenRefresher.refreshAsync() } returns null
|
||||||
|
every { DashboardSessionTokenRefresher.refresh() } returns null
|
||||||
|
|
||||||
|
HermesWsClient.disconnect(clearPendingMessages = true)
|
||||||
|
HermesWsClient.releaseExternalActivityConnectionLease()
|
||||||
|
HermesWsClient.releaseBackgroundConnectionLease()
|
||||||
|
HermesWsClient.setAppForeground(true)
|
||||||
|
AuthManager.resetAuthStateForTest()
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
fun tearDown() {
|
||||||
|
AuthManager.resetAuthStateForTest()
|
||||||
|
HermesWsClient.releaseExternalActivityConnectionLease()
|
||||||
|
HermesWsClient.releaseBackgroundConnectionLease()
|
||||||
|
HermesWsClient.disconnect(clearPendingMessages = true)
|
||||||
|
runBlocking {
|
||||||
|
withTimeoutOrNull(5000) {
|
||||||
|
HermesWsClient.connectionStatus.first { it == ConnectionStatus.DISCONNECTED }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
runCatching { mockWebServer.shutdown() }
|
||||||
|
unmockkAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun connectClient() {
|
||||||
|
HermesWsClient.connect()
|
||||||
|
runBlocking {
|
||||||
|
withTimeout(5000) { HermesWsClient.connectionStatus.first { it == ConnectionStatus.CONNECTED } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun typedSendApprovalRespondPutsExactParamsOnWire() {
|
||||||
|
val opened = CountDownLatch(1)
|
||||||
|
val captured = CountDownLatch(1)
|
||||||
|
val frameRef = AtomicReference<JsonObject?>(null)
|
||||||
|
|
||||||
|
mockWebServer.enqueue(
|
||||||
|
MockResponse().withWebSocketUpgrade(
|
||||||
|
object : WebSocketListener() {
|
||||||
|
override fun onOpen(
|
||||||
|
webSocket: WebSocket,
|
||||||
|
response: Response,
|
||||||
|
) = opened.countDown()
|
||||||
|
|
||||||
|
override fun onMessage(
|
||||||
|
webSocket: WebSocket,
|
||||||
|
text: String,
|
||||||
|
) {
|
||||||
|
val json = OkHttpProvider.json.parseToJsonElement(text) as? JsonObject ?: return
|
||||||
|
val name = (json["method"] as? JsonPrimitive)?.content
|
||||||
|
if (name == "approval.respond") {
|
||||||
|
frameRef.set(json)
|
||||||
|
captured.countDown()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
connectClient()
|
||||||
|
assertTrue(opened.await(5, TimeUnit.SECONDS))
|
||||||
|
|
||||||
|
var onSentId: String? = null
|
||||||
|
val returnedId =
|
||||||
|
HermesWsClient.send(
|
||||||
|
method = RpcMethods.APPROVAL_RESPOND,
|
||||||
|
params =
|
||||||
|
ApprovalRespondParams(
|
||||||
|
sessionId = "sess-appr-wire-1",
|
||||||
|
choice = "deny",
|
||||||
|
all = false,
|
||||||
|
requestId = "req-appr-wire-42",
|
||||||
|
),
|
||||||
|
onSent = { id -> onSentId = id },
|
||||||
|
)
|
||||||
|
|
||||||
|
assertTrue(returnedId.isNotBlank())
|
||||||
|
assertEquals(returnedId, onSentId)
|
||||||
|
|
||||||
|
assertTrue("Frame must reach the server", captured.await(5, TimeUnit.SECONDS))
|
||||||
|
val frame = requireNotNull(frameRef.get())
|
||||||
|
|
||||||
|
assertFalse("Outgoing frames have NO jsonrpc field", frame.containsKey("jsonrpc"))
|
||||||
|
assertEquals("approval.respond", (frame["method"] as? JsonPrimitive)?.content)
|
||||||
|
assertEquals(returnedId, (frame["id"] as? JsonPrimitive)?.content)
|
||||||
|
|
||||||
|
val params = frame["params"] as? JsonObject
|
||||||
|
val expectedParams =
|
||||||
|
buildJsonObject {
|
||||||
|
put("session_id", "sess-appr-wire-1")
|
||||||
|
put("choice", "deny")
|
||||||
|
put("all", false)
|
||||||
|
put("request_id", "req-appr-wire-42")
|
||||||
|
}
|
||||||
|
assertEquals(expectedParams, params)
|
||||||
|
assertEquals(false, (params?.get("all") as? JsonPrimitive)?.booleanOrNull)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun typedCallSessionActiveListReturnsRawServerJsonUntouched() {
|
||||||
|
val opened = CountDownLatch(1)
|
||||||
|
val captured = CountDownLatch(1)
|
||||||
|
val frameRef = AtomicReference<JsonObject?>(null)
|
||||||
|
|
||||||
|
val serverResultJson =
|
||||||
|
buildJsonObject {
|
||||||
|
put("active_count", 2)
|
||||||
|
put(
|
||||||
|
"sessions",
|
||||||
|
buildJsonObject {
|
||||||
|
put("primary", "sess-alpha")
|
||||||
|
put("secondary", "sess-beta")
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
mockWebServer.enqueue(
|
||||||
|
MockResponse().withWebSocketUpgrade(
|
||||||
|
object : WebSocketListener() {
|
||||||
|
override fun onOpen(
|
||||||
|
webSocket: WebSocket,
|
||||||
|
response: Response,
|
||||||
|
) = opened.countDown()
|
||||||
|
|
||||||
|
override fun onMessage(
|
||||||
|
webSocket: WebSocket,
|
||||||
|
text: String,
|
||||||
|
) {
|
||||||
|
val json = OkHttpProvider.json.parseToJsonElement(text) as? JsonObject ?: return
|
||||||
|
val name = (json["method"] as? JsonPrimitive)?.content
|
||||||
|
if (name == "session.active_list") {
|
||||||
|
frameRef.set(json)
|
||||||
|
captured.countDown()
|
||||||
|
val id = (json["id"] as? JsonPrimitive)?.content ?: "1"
|
||||||
|
webSocket.send(
|
||||||
|
"""{"jsonrpc":"2.0","id":"$id","result":$serverResultJson}""",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
connectClient()
|
||||||
|
assertTrue(opened.await(5, TimeUnit.SECONDS))
|
||||||
|
|
||||||
|
val result: JsonElement =
|
||||||
|
runBlocking {
|
||||||
|
HermesWsClient.call(
|
||||||
|
RpcMethods.SESSION_ACTIVE_LIST,
|
||||||
|
SessionActiveListParams(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
assertTrue("Frame must reach the server", captured.await(5, TimeUnit.SECONDS))
|
||||||
|
val frame = requireNotNull(frameRef.get())
|
||||||
|
assertEquals("session.active_list", (frame["method"] as? JsonPrimitive)?.content)
|
||||||
|
|
||||||
|
// An empty params object is omitted on wire or empty
|
||||||
|
val params = frame["params"] as? JsonObject
|
||||||
|
assertTrue(
|
||||||
|
"session.active_list with null currentSessionId carries no params",
|
||||||
|
params == null || params.isEmpty(),
|
||||||
|
)
|
||||||
|
assertEquals(serverResultJson, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -339,7 +339,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/SessionLiveStatusSource.kt::SESSION_ACTIVE_LIST",
|
|
||||||
"data/ws/SubagentRepository.kt::SUBAGENT_LIST",
|
"data/ws/SubagentRepository.kt::SUBAGENT_LIST",
|
||||||
"data/ws/SubagentRepository.kt::SUBAGENT_TAIL",
|
"data/ws/SubagentRepository.kt::SUBAGENT_TAIL",
|
||||||
"data/ws/VaultRepository.kt::VAULT_LIST",
|
"data/ws/VaultRepository.kt::VAULT_LIST",
|
||||||
@@ -355,9 +354,6 @@ class RawRpcParamsRatchetTest {
|
|||||||
"ui/bots/group/GroupChatViewModel.kt::PROFILES_CONFIGURE",
|
"ui/bots/group/GroupChatViewModel.kt::PROFILES_CONFIGURE",
|
||||||
"ui/bots/group/GroupChatViewModel.kt::PROFILES_CONFIGURE",
|
"ui/bots/group/GroupChatViewModel.kt::PROFILES_CONFIGURE",
|
||||||
"ui/bots/group/GroupChatViewModel.kt::PROFILES_LIST",
|
"ui/bots/group/GroupChatViewModel.kt::PROFILES_LIST",
|
||||||
"ui/chat/ChatApprovalsDelegate.kt::APPROVAL_PENDING",
|
|
||||||
"ui/chat/ChatApprovalsDelegate.kt::APPROVAL_RECEIVED",
|
|
||||||
"ui/chat/ChatApprovalsDelegate.kt::APPROVAL_RESPOND",
|
|
||||||
"ui/chat/ChatConnectionOperationDelegate.kt::CONNECTION_RESPOND",
|
"ui/chat/ChatConnectionOperationDelegate.kt::CONNECTION_RESPOND",
|
||||||
"ui/chat/ChatConnectionOperationDelegate.kt::CONNECTION_RESPOND",
|
"ui/chat/ChatConnectionOperationDelegate.kt::CONNECTION_RESPOND",
|
||||||
"ui/chat/ChatConnectionOperationDelegate.kt::CONNECTORS_OPERATION_WAKE",
|
"ui/chat/ChatConnectionOperationDelegate.kt::CONNECTORS_OPERATION_WAKE",
|
||||||
|
|||||||
@@ -121,14 +121,12 @@ class SessionLiveStatusSourceTest {
|
|||||||
@Test
|
@Test
|
||||||
fun `HermesSessionLiveStatusSource fetches and decodes snapshot via RPC`() =
|
fun `HermesSessionLiveStatusSource fetches and decodes snapshot via RPC`() =
|
||||||
runBlocking {
|
runBlocking {
|
||||||
var capturedMethod: String? = null
|
var rpcCalls = 0
|
||||||
var capturedParams: Map<String, Any>? = null
|
|
||||||
|
|
||||||
val source =
|
val source =
|
||||||
HermesSessionLiveStatusSource(
|
HermesSessionLiveStatusSource(
|
||||||
rpcRequest = { method, params ->
|
rpcRequest = {
|
||||||
capturedMethod = method
|
rpcCalls++
|
||||||
capturedParams = params
|
|
||||||
mapOf(
|
mapOf(
|
||||||
"sessions" to
|
"sessions" to
|
||||||
listOf(
|
listOf(
|
||||||
@@ -146,8 +144,8 @@ class SessionLiveStatusSourceTest {
|
|||||||
|
|
||||||
val snapshot = source.fetchActiveSessionsSnapshot()
|
val snapshot = source.fetchActiveSessionsSnapshot()
|
||||||
|
|
||||||
assertEquals(WsMethods.SESSION_ACTIVE_LIST, capturedMethod)
|
// Method name and empty params are pinned on the wire in HermesWsClientApprovalsTest.
|
||||||
assertTrue(capturedParams!!.isEmpty())
|
assertEquals(1, rpcCalls)
|
||||||
assertNotNull(snapshot)
|
assertNotNull(snapshot)
|
||||||
assertEquals(SessionLiveStatus.WORKING, snapshot!!.statusByStoredId["st-1"])
|
assertEquals(SessionLiveStatus.WORKING, snapshot!!.statusByStoredId["st-1"])
|
||||||
}
|
}
|
||||||
@@ -157,7 +155,7 @@ class SessionLiveStatusSourceTest {
|
|||||||
runBlocking {
|
runBlocking {
|
||||||
val source =
|
val source =
|
||||||
HermesSessionLiveStatusSource(
|
HermesSessionLiveStatusSource(
|
||||||
rpcRequest = { _, _ -> throw RuntimeException("RPC error") },
|
rpcRequest = { throw RuntimeException("RPC error") },
|
||||||
eventsProvider = { MutableSharedFlow() },
|
eventsProvider = { MutableSharedFlow() },
|
||||||
connectionStatusProvider = { MutableStateFlow(ConnectionStatus.CONNECTED) },
|
connectionStatusProvider = { MutableStateFlow(ConnectionStatus.CONNECTED) },
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,209 @@
|
|||||||
|
package com.m57.hermescontrol.data.ws.contract
|
||||||
|
|
||||||
|
import com.m57.hermescontrol.data.remote.OkHttpProvider
|
||||||
|
import kotlinx.serialization.json.JsonElement
|
||||||
|
import kotlinx.serialization.json.JsonObject
|
||||||
|
import kotlinx.serialization.json.buildJsonObject
|
||||||
|
import kotlinx.serialization.json.decodeFromJsonElement
|
||||||
|
import kotlinx.serialization.json.encodeToJsonElement
|
||||||
|
import kotlinx.serialization.json.put
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class ApprovalsTypesTest {
|
||||||
|
@Test
|
||||||
|
fun approvalPendingParamsEncodesSessionIdExactly() {
|
||||||
|
val params = ApprovalPendingParams(sessionId = "sess-appr-pending-1")
|
||||||
|
val encoded = OkHttpProvider.json.encodeToJsonElement(ApprovalPendingParams.serializer(), params)
|
||||||
|
|
||||||
|
val expected =
|
||||||
|
buildJsonObject {
|
||||||
|
put("session_id", "sess-appr-pending-1")
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals(expected, encoded)
|
||||||
|
assertTrue(encoded is JsonObject)
|
||||||
|
assertEquals(setOf("session_id"), (encoded as JsonObject).keys)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun approvalReceivedParamsEncodesSessionIdAndRequestId() {
|
||||||
|
val params =
|
||||||
|
ApprovalReceivedParams(
|
||||||
|
sessionId = "sess-appr-rec-1",
|
||||||
|
requestId = "req-appr-rec-99",
|
||||||
|
)
|
||||||
|
val encoded = OkHttpProvider.json.encodeToJsonElement(ApprovalReceivedParams.serializer(), params)
|
||||||
|
|
||||||
|
val expected =
|
||||||
|
buildJsonObject {
|
||||||
|
put("session_id", "sess-appr-rec-1")
|
||||||
|
put("request_id", "req-appr-rec-99")
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals(expected, encoded)
|
||||||
|
assertTrue(encoded is JsonObject)
|
||||||
|
assertEquals(setOf("session_id", "request_id"), (encoded as JsonObject).keys)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun approvalRespondParamsEncodesOnlySessionIdWhenOptionalsNull() {
|
||||||
|
val params = ApprovalRespondParams(sessionId = "sess-respond-1")
|
||||||
|
val encoded = OkHttpProvider.json.encodeToJsonElement(ApprovalRespondParams.serializer(), params)
|
||||||
|
|
||||||
|
val expected =
|
||||||
|
buildJsonObject {
|
||||||
|
put("session_id", "sess-respond-1")
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals(expected, encoded)
|
||||||
|
assertTrue(encoded is JsonObject)
|
||||||
|
assertEquals(setOf("session_id"), (encoded as JsonObject).keys)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun approvalRespondParamsEncodesAllFieldsWhenPresent() {
|
||||||
|
val params =
|
||||||
|
ApprovalRespondParams(
|
||||||
|
sessionId = "sess-respond-2",
|
||||||
|
choice = "allow",
|
||||||
|
all = true,
|
||||||
|
requestId = "req-respond-2",
|
||||||
|
)
|
||||||
|
val encoded = OkHttpProvider.json.encodeToJsonElement(ApprovalRespondParams.serializer(), params)
|
||||||
|
|
||||||
|
val expected =
|
||||||
|
buildJsonObject {
|
||||||
|
put("session_id", "sess-respond-2")
|
||||||
|
put("choice", "allow")
|
||||||
|
put("all", true)
|
||||||
|
put("request_id", "req-respond-2")
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals(expected, encoded)
|
||||||
|
assertTrue(encoded is JsonObject)
|
||||||
|
assertEquals(setOf("session_id", "choice", "all", "request_id"), (encoded as JsonObject).keys)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun approvalRespondParamsEncodesExplicitFalseForAll() {
|
||||||
|
val params =
|
||||||
|
ApprovalRespondParams(
|
||||||
|
sessionId = "sess-respond-3",
|
||||||
|
choice = "deny",
|
||||||
|
all = false,
|
||||||
|
requestId = "req-respond-3",
|
||||||
|
)
|
||||||
|
val encoded = OkHttpProvider.json.encodeToJsonElement(ApprovalRespondParams.serializer(), params)
|
||||||
|
|
||||||
|
val expected =
|
||||||
|
buildJsonObject {
|
||||||
|
put("session_id", "sess-respond-3")
|
||||||
|
put("choice", "deny")
|
||||||
|
put("all", false)
|
||||||
|
put("request_id", "req-respond-3")
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals(expected, encoded)
|
||||||
|
assertTrue(encoded is JsonObject)
|
||||||
|
assertEquals(setOf("session_id", "choice", "all", "request_id"), (encoded as JsonObject).keys)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun approvalRespondParamsOmitsOnlyNullFields() {
|
||||||
|
val params =
|
||||||
|
ApprovalRespondParams(
|
||||||
|
sessionId = "sess-respond-4",
|
||||||
|
choice = "allow",
|
||||||
|
all = null,
|
||||||
|
requestId = null,
|
||||||
|
)
|
||||||
|
val encoded = OkHttpProvider.json.encodeToJsonElement(ApprovalRespondParams.serializer(), params)
|
||||||
|
|
||||||
|
val expected =
|
||||||
|
buildJsonObject {
|
||||||
|
put("session_id", "sess-respond-4")
|
||||||
|
put("choice", "allow")
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals(expected, encoded)
|
||||||
|
assertTrue(encoded is JsonObject)
|
||||||
|
assertEquals(setOf("session_id", "choice"), (encoded as JsonObject).keys)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun sessionActiveListParamsEncodesToEmptyJsonObjectWhenCurrentSessionIdNull() {
|
||||||
|
val params = SessionActiveListParams()
|
||||||
|
val encoded = OkHttpProvider.json.encodeToJsonElement(SessionActiveListParams.serializer(), params)
|
||||||
|
|
||||||
|
val expected = buildJsonObject {}
|
||||||
|
assertEquals(expected, encoded)
|
||||||
|
assertTrue(encoded is JsonObject)
|
||||||
|
assertTrue((encoded as JsonObject).isEmpty())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun sessionActiveListParamsEncodesCurrentSessionIdWhenSet() {
|
||||||
|
val params = SessionActiveListParams(currentSessionId = "sess-active-curr-1")
|
||||||
|
val encoded = OkHttpProvider.json.encodeToJsonElement(SessionActiveListParams.serializer(), params)
|
||||||
|
|
||||||
|
val expected =
|
||||||
|
buildJsonObject {
|
||||||
|
put("current_session_id", "sess-active-curr-1")
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals(expected, encoded)
|
||||||
|
assertTrue(encoded is JsonObject)
|
||||||
|
assertEquals(setOf("current_session_id"), (encoded as JsonObject).keys)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun rpcMethodsRegistrationMatchesContract() {
|
||||||
|
assertEquals("approval.pending", RpcMethods.APPROVAL_PENDING.name)
|
||||||
|
assertEquals("approval.received", RpcMethods.APPROVAL_RECEIVED.name)
|
||||||
|
assertEquals("approval.respond", RpcMethods.APPROVAL_RESPOND.name)
|
||||||
|
assertEquals("session.active_list", RpcMethods.SESSION_ACTIVE_LIST.name)
|
||||||
|
|
||||||
|
assertTrue(RpcMethods.all.contains(RpcMethods.APPROVAL_PENDING))
|
||||||
|
assertTrue(RpcMethods.all.contains(RpcMethods.APPROVAL_RECEIVED))
|
||||||
|
assertTrue(RpcMethods.all.contains(RpcMethods.APPROVAL_RESPOND))
|
||||||
|
assertTrue(RpcMethods.all.contains(RpcMethods.SESSION_ACTIVE_LIST))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun jsonElementPassthroughMethodsDecodeResultUntouched() {
|
||||||
|
val arbitraryPayload =
|
||||||
|
buildJsonObject {
|
||||||
|
put("status", "pending")
|
||||||
|
put("count", 3)
|
||||||
|
put("nested", buildJsonObject { put("detail", "value") })
|
||||||
|
}
|
||||||
|
|
||||||
|
val decodedPending: JsonElement =
|
||||||
|
OkHttpProvider.json.decodeFromJsonElement(
|
||||||
|
RpcMethods.APPROVAL_PENDING.result,
|
||||||
|
arbitraryPayload,
|
||||||
|
)
|
||||||
|
val decodedReceived: JsonElement =
|
||||||
|
OkHttpProvider.json.decodeFromJsonElement(
|
||||||
|
RpcMethods.APPROVAL_RECEIVED.result,
|
||||||
|
arbitraryPayload,
|
||||||
|
)
|
||||||
|
val decodedRespond: JsonElement =
|
||||||
|
OkHttpProvider.json.decodeFromJsonElement(
|
||||||
|
RpcMethods.APPROVAL_RESPOND.result,
|
||||||
|
arbitraryPayload,
|
||||||
|
)
|
||||||
|
val decodedActiveList: JsonElement =
|
||||||
|
OkHttpProvider.json.decodeFromJsonElement(
|
||||||
|
RpcMethods.SESSION_ACTIVE_LIST.result,
|
||||||
|
arbitraryPayload,
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals(arbitraryPayload, decodedPending)
|
||||||
|
assertEquals(arbitraryPayload, decodedReceived)
|
||||||
|
assertEquals(arbitraryPayload, decodedRespond)
|
||||||
|
assertEquals(arbitraryPayload, decodedActiveList)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,10 @@
|
|||||||
package com.m57.hermescontrol.ui.chat
|
package com.m57.hermescontrol.ui.chat
|
||||||
|
|
||||||
|
import com.m57.hermescontrol.data.remote.OkHttpProvider
|
||||||
import com.m57.hermescontrol.data.ws.WsEvent
|
import com.m57.hermescontrol.data.ws.WsEvent
|
||||||
import com.m57.hermescontrol.data.ws.WsMethods
|
import com.m57.hermescontrol.data.ws.WsMethods
|
||||||
|
import com.m57.hermescontrol.data.ws.contract.RpcMethod
|
||||||
|
import com.m57.hermescontrol.data.ws.contract.TypedRpcSender
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.test.StandardTestDispatcher
|
import kotlinx.coroutines.test.StandardTestDispatcher
|
||||||
@@ -9,6 +12,8 @@ import kotlinx.coroutines.test.TestScope
|
|||||||
import kotlinx.coroutines.test.advanceUntilIdle
|
import kotlinx.coroutines.test.advanceUntilIdle
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import kotlinx.serialization.json.JsonElement
|
import kotlinx.serialization.json.JsonElement
|
||||||
|
import kotlinx.serialization.json.JsonObject
|
||||||
|
import kotlinx.serialization.json.JsonPrimitive
|
||||||
import kotlinx.serialization.json.jsonObject
|
import kotlinx.serialization.json.jsonObject
|
||||||
import kotlinx.serialization.json.jsonPrimitive
|
import kotlinx.serialization.json.jsonPrimitive
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
@@ -25,7 +30,7 @@ class ChatApprovalsDelegateTest {
|
|||||||
private val uiState = MutableStateFlow(ChatUiState(currentSessionId = "storage-session-1"))
|
private val uiState = MutableStateFlow(ChatUiState(currentSessionId = "storage-session-1"))
|
||||||
private var runtimeId: String? = null
|
private var runtimeId: String? = null
|
||||||
private val sentMethods = mutableListOf<String>()
|
private val sentMethods = mutableListOf<String>()
|
||||||
private val sentParams = mutableListOf<Map<String, Any>>()
|
private val sentParams = mutableListOf<JsonObject>()
|
||||||
private val trackedRequests = mutableListOf<Pair<String, String>>()
|
private val trackedRequests = mutableListOf<Pair<String, String>>()
|
||||||
private val systemMessages = mutableListOf<String>()
|
private val systemMessages = mutableListOf<String>()
|
||||||
|
|
||||||
@@ -35,10 +40,21 @@ class ChatApprovalsDelegateTest {
|
|||||||
ioDispatcher = testDispatcher,
|
ioDispatcher = testDispatcher,
|
||||||
uiState = uiState,
|
uiState = uiState,
|
||||||
runtimeSessionId = { runtimeId },
|
runtimeSessionId = { runtimeId },
|
||||||
wsSend = { method, params, onSent ->
|
rpc =
|
||||||
sentMethods.add(method)
|
object : TypedRpcSender {
|
||||||
sentParams.add(params)
|
override fun <P> send(
|
||||||
onSent?.invoke("req-id-${sentMethods.size}")
|
method: RpcMethod<P, *>,
|
||||||
|
params: P,
|
||||||
|
onSent: ((String) -> Unit)?,
|
||||||
|
): String {
|
||||||
|
sentMethods.add(method.name)
|
||||||
|
sentParams.add(
|
||||||
|
OkHttpProvider.json.encodeToJsonElement(method.params, params) as JsonObject,
|
||||||
|
)
|
||||||
|
val id = "req-id-${sentMethods.size}"
|
||||||
|
onSent?.invoke(id)
|
||||||
|
return id
|
||||||
|
}
|
||||||
},
|
},
|
||||||
trackRequest = { id, method -> trackedRequests.add(id to method) },
|
trackRequest = { id, method -> trackedRequests.add(id to method) },
|
||||||
addSystemMessage = { text -> systemMessages.add(text) },
|
addSystemMessage = { text -> systemMessages.add(text) },
|
||||||
@@ -98,8 +114,8 @@ class ChatApprovalsDelegateTest {
|
|||||||
assertFalse(uiState.value.isAgentTyping)
|
assertFalse(uiState.value.isAgentTyping)
|
||||||
|
|
||||||
assertEquals(listOf(WsMethods.APPROVAL_RECEIVED), sentMethods)
|
assertEquals(listOf(WsMethods.APPROVAL_RECEIVED), sentMethods)
|
||||||
assertEquals("runtime-session-xyz", sentParams.first()["session_id"])
|
assertEquals(JsonPrimitive("runtime-session-xyz"), sentParams.first()["session_id"])
|
||||||
assertEquals("req-123", sentParams.first()["request_id"])
|
assertEquals(JsonPrimitive("req-123"), sentParams.first()["request_id"])
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -125,11 +141,11 @@ class ChatApprovalsDelegateTest {
|
|||||||
|
|
||||||
advanceUntilIdle()
|
advanceUntilIdle()
|
||||||
assertEquals(listOf(WsMethods.APPROVAL_RESPOND, WsMethods.APPROVAL_PENDING), sentMethods)
|
assertEquals(listOf(WsMethods.APPROVAL_RESPOND, WsMethods.APPROVAL_PENDING), sentMethods)
|
||||||
assertEquals("once", sentParams[0]["choice"])
|
assertEquals(JsonPrimitive("once"), sentParams[0]["choice"])
|
||||||
assertEquals("req-999", sentParams[0]["request_id"])
|
assertEquals(JsonPrimitive("req-999"), sentParams[0]["request_id"])
|
||||||
assertEquals(false, sentParams[0]["all"])
|
assertEquals(JsonPrimitive(false), sentParams[0]["all"])
|
||||||
assertEquals("runtime-session-xyz", sentParams[0]["session_id"])
|
assertEquals(JsonPrimitive("runtime-session-xyz"), sentParams[0]["session_id"])
|
||||||
assertEquals("runtime-session-xyz", sentParams[1]["session_id"])
|
assertEquals(JsonPrimitive("runtime-session-xyz"), sentParams[1]["session_id"])
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -270,7 +286,14 @@ class ChatApprovalsDelegateTest {
|
|||||||
ioDispatcher = testDispatcher,
|
ioDispatcher = testDispatcher,
|
||||||
uiState = uiState,
|
uiState = uiState,
|
||||||
runtimeSessionId = { "session-1" },
|
runtimeSessionId = { "session-1" },
|
||||||
wsSend = { _, _, _ -> error("new approval must not use legacy response") },
|
rpc =
|
||||||
|
object : TypedRpcSender {
|
||||||
|
override fun <P> send(
|
||||||
|
method: RpcMethod<P, *>,
|
||||||
|
params: P,
|
||||||
|
onSent: ((String) -> Unit)?,
|
||||||
|
): String = error("new approval must not use legacy response")
|
||||||
|
},
|
||||||
trackRequest = { _, _ -> },
|
trackRequest = { _, _ -> },
|
||||||
addSystemMessage = { },
|
addSystemMessage = { },
|
||||||
respondToServerRequest = { id, result -> responses += id to result },
|
respondToServerRequest = { id, result -> responses += id to result },
|
||||||
|
|||||||
@@ -6619,9 +6619,9 @@ class ChatViewModelTest {
|
|||||||
HermesWsClient.send(
|
HermesWsClient.send(
|
||||||
WsMethods.APPROVAL_RESPOND,
|
WsMethods.APPROVAL_RESPOND,
|
||||||
withArg { params ->
|
withArg { params ->
|
||||||
assertEquals(sessionId, params["session_id"])
|
assertEquals(JsonPrimitive(sessionId), params["session_id"])
|
||||||
assertEquals("once", params["choice"])
|
assertEquals(JsonPrimitive("once"), params["choice"])
|
||||||
assertEquals("req-42", params["request_id"])
|
assertEquals(JsonPrimitive("req-42"), params["request_id"])
|
||||||
},
|
},
|
||||||
any(),
|
any(),
|
||||||
)
|
)
|
||||||
@@ -6653,8 +6653,8 @@ class ChatViewModelTest {
|
|||||||
HermesWsClient.send(
|
HermesWsClient.send(
|
||||||
WsMethods.APPROVAL_RESPOND,
|
WsMethods.APPROVAL_RESPOND,
|
||||||
withArg { params ->
|
withArg { params ->
|
||||||
assertEquals("session", params["choice"])
|
assertEquals(JsonPrimitive("session"), params["choice"])
|
||||||
assertEquals("req-7", params["request_id"])
|
assertEquals(JsonPrimitive("req-7"), params["request_id"])
|
||||||
},
|
},
|
||||||
any(),
|
any(),
|
||||||
)
|
)
|
||||||
@@ -6684,7 +6684,7 @@ class ChatViewModelTest {
|
|||||||
HermesWsClient.send(
|
HermesWsClient.send(
|
||||||
WsMethods.APPROVAL_RESPOND,
|
WsMethods.APPROVAL_RESPOND,
|
||||||
withArg { params ->
|
withArg { params ->
|
||||||
assertEquals("deny", params["choice"])
|
assertEquals(JsonPrimitive("deny"), params["choice"])
|
||||||
assertFalse(params.containsKey("request_id"))
|
assertFalse(params.containsKey("request_id"))
|
||||||
},
|
},
|
||||||
any(),
|
any(),
|
||||||
@@ -6717,7 +6717,7 @@ class ChatViewModelTest {
|
|||||||
HermesWsClient.send(
|
HermesWsClient.send(
|
||||||
WsMethods.APPROVAL_PENDING,
|
WsMethods.APPROVAL_PENDING,
|
||||||
withArg { params ->
|
withArg { params ->
|
||||||
assertEquals(sessionId, params["session_id"])
|
assertEquals(JsonPrimitive(sessionId), params["session_id"])
|
||||||
},
|
},
|
||||||
any(),
|
any(),
|
||||||
)
|
)
|
||||||
@@ -6763,8 +6763,8 @@ class ChatViewModelTest {
|
|||||||
HermesWsClient.send(
|
HermesWsClient.send(
|
||||||
WsMethods.APPROVAL_RECEIVED,
|
WsMethods.APPROVAL_RECEIVED,
|
||||||
withArg { params ->
|
withArg { params ->
|
||||||
assertEquals("runtime-sid-999", params["session_id"])
|
assertEquals(JsonPrimitive("runtime-sid-999"), params["session_id"])
|
||||||
assertEquals("req-runtime-test", params["request_id"])
|
assertEquals(JsonPrimitive("req-runtime-test"), params["request_id"])
|
||||||
},
|
},
|
||||||
any(),
|
any(),
|
||||||
)
|
)
|
||||||
@@ -6777,16 +6777,16 @@ class ChatViewModelTest {
|
|||||||
HermesWsClient.send(
|
HermesWsClient.send(
|
||||||
WsMethods.APPROVAL_RESPOND,
|
WsMethods.APPROVAL_RESPOND,
|
||||||
withArg { params ->
|
withArg { params ->
|
||||||
assertEquals("runtime-sid-999", params["session_id"])
|
assertEquals(JsonPrimitive("runtime-sid-999"), params["session_id"])
|
||||||
assertEquals("once", params["choice"])
|
assertEquals(JsonPrimitive("once"), params["choice"])
|
||||||
assertEquals("req-runtime-test", params["request_id"])
|
assertEquals(JsonPrimitive("req-runtime-test"), params["request_id"])
|
||||||
},
|
},
|
||||||
any(),
|
any(),
|
||||||
)
|
)
|
||||||
HermesWsClient.send(
|
HermesWsClient.send(
|
||||||
WsMethods.APPROVAL_PENDING,
|
WsMethods.APPROVAL_PENDING,
|
||||||
withArg { params ->
|
withArg { params ->
|
||||||
assertEquals("runtime-sid-999", params["session_id"])
|
assertEquals(JsonPrimitive("runtime-sid-999"), params["session_id"])
|
||||||
},
|
},
|
||||||
any(),
|
any(),
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in new issue
Block a user