mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-04 12:41:41 +00:00
refactor(android): consolidate chat controller test setup (#117862)
This commit is contained in:
committed by
GitHub
parent
b9ed358f93
commit
4e4959b283
@@ -8,13 +8,13 @@ import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.test.advanceUntilIdle
|
||||
import kotlinx.coroutines.test.runCurrent
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import kotlinx.serialization.json.Json
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class ChatControllerCommandControlsTest {
|
||||
private val json = Json { ignoreUnknownKeys = true }
|
||||
private val json = chatControllerTestJson
|
||||
|
||||
@Test
|
||||
fun parseChatCommandsKeepsTextAliasesAndArgumentFlag() {
|
||||
@@ -52,35 +52,13 @@ class ChatControllerCommandControlsTest {
|
||||
assertEquals(true, commands[1].acceptsArgs)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@Test
|
||||
fun healthEventRefreshesCommandsAfterReconnect() =
|
||||
runTest {
|
||||
val requests = mutableListOf<Pair<String, String?>>()
|
||||
val controller =
|
||||
ChatController(
|
||||
scope = this,
|
||||
json = json,
|
||||
requestGateway = { method, paramsJson ->
|
||||
requests += method to paramsJson
|
||||
when (method) {
|
||||
"chat.metadata" ->
|
||||
"""
|
||||
{
|
||||
"commands": [
|
||||
{
|
||||
"name": "model",
|
||||
"description": "Switch models",
|
||||
"textAliases": ["/model"],
|
||||
"acceptsArgs": true
|
||||
}
|
||||
]
|
||||
}
|
||||
""".trimIndent()
|
||||
else -> "{}"
|
||||
}
|
||||
},
|
||||
)
|
||||
val (controller, requests) =
|
||||
chatControllerTestSetup {
|
||||
respond("chat.metadata", commandResponse("model", "Switch models", acceptsArgs = true))
|
||||
}
|
||||
|
||||
controller.handleGatewayEvent("health", null)
|
||||
advanceUntilIdle()
|
||||
@@ -105,52 +83,21 @@ class ChatControllerCommandControlsTest {
|
||||
assertEquals(2, requests.count { it.first == "chat.metadata" })
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@Test
|
||||
fun commandListScopesToActiveAgentAndRefreshesAfterAgentSwitch() =
|
||||
runTest {
|
||||
val requests = mutableListOf<Pair<String, String?>>()
|
||||
val controller =
|
||||
ChatController(
|
||||
scope = this,
|
||||
json = json,
|
||||
requestGateway = { method, paramsJson ->
|
||||
requests += method to paramsJson
|
||||
when (method) {
|
||||
"chat.metadata" ->
|
||||
if (paramsJson.orEmpty().contains("\"agentId\":\"ops\"")) {
|
||||
"""
|
||||
{
|
||||
"commands": [
|
||||
{
|
||||
"name": "ops",
|
||||
"description": "Ops command",
|
||||
"textAliases": ["/ops"],
|
||||
"acceptsArgs": false
|
||||
}
|
||||
]
|
||||
}
|
||||
""".trimIndent()
|
||||
} else {
|
||||
"""
|
||||
{
|
||||
"commands": [
|
||||
{
|
||||
"name": "main",
|
||||
"description": "Main command",
|
||||
"textAliases": ["/main"],
|
||||
"acceptsArgs": false
|
||||
}
|
||||
]
|
||||
}
|
||||
""".trimIndent()
|
||||
}
|
||||
"chat.history" -> """{"sessionId":"loaded-session","messages":[]}"""
|
||||
"health" -> "{}"
|
||||
else -> "{}"
|
||||
val (controller, requests) =
|
||||
chatControllerTestSetup {
|
||||
respond("chat.metadata") { paramsJson ->
|
||||
if (paramsJson.orEmpty().contains("\"agentId\":\"ops\"")) {
|
||||
commandResponse("ops", "Ops command")
|
||||
} else {
|
||||
commandResponse("main", "Main command")
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
respond("chat.history", """{"sessionId":"loaded-session","messages":[]}""")
|
||||
respond("health", "{}")
|
||||
}
|
||||
|
||||
controller.handleGatewayEvent("health", null)
|
||||
advanceUntilIdle()
|
||||
@@ -175,17 +122,13 @@ class ChatControllerCommandControlsTest {
|
||||
assertTrue(commandRequests.any { it.second.orEmpty().contains("\"agentId\":\"ops\"") })
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@Test
|
||||
fun delayedCommandListFromPreviousGatewayCannotReplaceCurrentCommands() =
|
||||
runTest {
|
||||
var cacheScope = ChatCacheScope(gatewayId = "gateway-a", connectionGeneration = 1)
|
||||
val gatewayAResponse = CompletableDeferred<String>()
|
||||
val controller =
|
||||
ChatController(
|
||||
scope = this,
|
||||
json = json,
|
||||
requestGateway = { _, _ -> error("gateway-bound request expected") },
|
||||
createChatController(
|
||||
requestGatewayForGateway = { gatewayId, method, _ ->
|
||||
require(method == "chat.metadata")
|
||||
if (gatewayId == "gateway-a") {
|
||||
@@ -195,7 +138,7 @@ class ChatControllerCommandControlsTest {
|
||||
}
|
||||
},
|
||||
cacheScope = { cacheScope },
|
||||
)
|
||||
) { _, _ -> error("gateway-bound request expected") }
|
||||
|
||||
controller.refreshCommands()
|
||||
runCurrent()
|
||||
@@ -221,26 +164,16 @@ class ChatControllerCommandControlsTest {
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@Test
|
||||
fun startNewChatCreatesWriteScopedSessionAndReloadsHistory() =
|
||||
runTest {
|
||||
val requests = mutableListOf<Pair<String, String?>>()
|
||||
val controller =
|
||||
ChatController(
|
||||
scope = this,
|
||||
json = json,
|
||||
requestGateway = { method, paramsJson ->
|
||||
requests += method to paramsJson
|
||||
when (method) {
|
||||
"sessions.create" -> """{"ok":true,"key":"agent:main:dashboard:fresh"}"""
|
||||
"chat.history" -> """{"sessionId":"fresh-session","messages":[]}"""
|
||||
"health" -> "{}"
|
||||
"sessions.list" -> """{"sessions":[]}"""
|
||||
else -> "{}"
|
||||
}
|
||||
},
|
||||
)
|
||||
val (controller, requests) =
|
||||
chatControllerTestSetup {
|
||||
respond("sessions.create", """{"ok":true,"key":"agent:main:dashboard:fresh"}""")
|
||||
respond("chat.history", """{"sessionId":"fresh-session","messages":[]}""")
|
||||
respond("health", "{}")
|
||||
respond("sessions.list", """{"sessions":[]}""")
|
||||
}
|
||||
controller.handleGatewayEvent("health", null)
|
||||
controller.load("main")
|
||||
advanceUntilIdle()
|
||||
@@ -259,39 +192,29 @@ class ChatControllerCommandControlsTest {
|
||||
assertTrue(requests.any { it.first == "sessions.list" })
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@Test
|
||||
fun startNewChatRetriesWithoutParentLifecycleAgainstOlderGateway() =
|
||||
runTest {
|
||||
val requests = mutableListOf<Pair<String, String?>>()
|
||||
var createCalls = 0
|
||||
val controller =
|
||||
ChatController(
|
||||
scope = this,
|
||||
json = json,
|
||||
requestGateway = { method, paramsJson ->
|
||||
requests += method to paramsJson
|
||||
when (method) {
|
||||
"sessions.create" -> {
|
||||
createCalls += 1
|
||||
if (createCalls == 1) {
|
||||
throw GatewayRequestRejected(
|
||||
GatewaySession.ErrorShape(
|
||||
code = "INVALID_REQUEST",
|
||||
message =
|
||||
"invalid sessions.create params: at root: unexpected property 'succeedsParent'",
|
||||
),
|
||||
)
|
||||
}
|
||||
"""{"ok":true,"key":"agent:main:dashboard:fresh"}"""
|
||||
}
|
||||
"chat.history" -> """{"sessionId":"fresh-session","messages":[]}"""
|
||||
"health" -> "{}"
|
||||
"sessions.list" -> """{"sessions":[]}"""
|
||||
else -> "{}"
|
||||
val (controller, requests) =
|
||||
chatControllerTestSetup {
|
||||
respond("sessions.create") { paramsJson ->
|
||||
createCalls += 1
|
||||
if (createCalls == 1) {
|
||||
throw GatewayRequestRejected(
|
||||
GatewaySession.ErrorShape(
|
||||
code = "INVALID_REQUEST",
|
||||
message =
|
||||
"invalid sessions.create params: at root: unexpected property 'succeedsParent'",
|
||||
),
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
"""{"ok":true,"key":"agent:main:dashboard:fresh"}"""
|
||||
}
|
||||
respond("chat.history", """{"sessionId":"fresh-session","messages":[]}""")
|
||||
respond("health", "{}")
|
||||
respond("sessions.list", """{"sessions":[]}""")
|
||||
}
|
||||
controller.handleGatewayEvent("health", null)
|
||||
controller.load("main")
|
||||
advanceUntilIdle()
|
||||
@@ -309,26 +232,16 @@ class ChatControllerCommandControlsTest {
|
||||
assertEquals("agent:main:dashboard:fresh", controller.sessionKey.value)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@Test
|
||||
fun startNewChatInWorktreeIncludesWorktreeFlag() =
|
||||
runTest {
|
||||
val requests = mutableListOf<Pair<String, String?>>()
|
||||
val controller =
|
||||
ChatController(
|
||||
scope = this,
|
||||
json = json,
|
||||
requestGateway = { method, paramsJson ->
|
||||
requests += method to paramsJson
|
||||
when (method) {
|
||||
"sessions.create" -> """{"ok":true,"key":"agent:main:dashboard:worktree"}"""
|
||||
"chat.history" -> """{"sessionId":"worktree-session","messages":[]}"""
|
||||
"health" -> "{}"
|
||||
"sessions.list" -> """{"sessions":[]}"""
|
||||
else -> "{}"
|
||||
}
|
||||
},
|
||||
)
|
||||
val (controller, requests) =
|
||||
chatControllerTestSetup {
|
||||
respond("sessions.create", """{"ok":true,"key":"agent:main:dashboard:worktree"}""")
|
||||
respond("chat.history", """{"sessionId":"worktree-session","messages":[]}""")
|
||||
respond("health", "{}")
|
||||
respond("sessions.list", """{"sessions":[]}""")
|
||||
}
|
||||
controller.handleGatewayEvent("health", null)
|
||||
controller.load("main")
|
||||
advanceUntilIdle()
|
||||
@@ -342,20 +255,11 @@ class ChatControllerCommandControlsTest {
|
||||
@Test
|
||||
fun sessionMutationsSendGatewayContractsAndRefresh() =
|
||||
runTest {
|
||||
val requests = mutableListOf<Pair<String, String?>>()
|
||||
val controller =
|
||||
ChatController(
|
||||
scope = this,
|
||||
json = json,
|
||||
requestGateway = { method, paramsJson ->
|
||||
requests += method to paramsJson
|
||||
when (method) {
|
||||
"sessions.list" -> """{"sessions":[]}"""
|
||||
"sessions.delete" -> """{"deleted":true}"""
|
||||
else -> "{}"
|
||||
}
|
||||
},
|
||||
)
|
||||
val (controller, requests) =
|
||||
chatControllerTestSetup {
|
||||
respond("sessions.list", """{"sessions":[]}""")
|
||||
respond("sessions.delete", """{"deleted":true}""")
|
||||
}
|
||||
|
||||
controller.patchSession(
|
||||
key = "main",
|
||||
@@ -386,24 +290,16 @@ class ChatControllerCommandControlsTest {
|
||||
@Test
|
||||
fun renameSessionGroupPatchesEveryMemberIncludingArchivedOnlyOnes() =
|
||||
runTest {
|
||||
val requests = mutableListOf<Pair<String, String?>>()
|
||||
val controller =
|
||||
ChatController(
|
||||
scope = this,
|
||||
json = json,
|
||||
requestGateway = { method, paramsJson ->
|
||||
requests += method to paramsJson
|
||||
when (method) {
|
||||
"sessions.list" ->
|
||||
if (paramsJson.orEmpty().contains("\"archived\":true")) {
|
||||
"""{"sessions":[{"key":"agent:main:active","category":"Work"},{"key":"agent:main:archived","category":" Work "}]}"""
|
||||
} else {
|
||||
"""{"sessions":[{"key":"agent:main:active","category":"Work"},{"key":"agent:main:other","category":"Play"}]}"""
|
||||
}
|
||||
else -> "{}"
|
||||
val (controller, requests) =
|
||||
chatControllerTestSetup {
|
||||
respond("sessions.list") { paramsJson ->
|
||||
if (paramsJson.orEmpty().contains("\"archived\":true")) {
|
||||
"""{"sessions":[{"key":"agent:main:active","category":"Work"},{"key":"agent:main:archived","category":" Work "}]}"""
|
||||
} else {
|
||||
"""{"sessions":[{"key":"agent:main:active","category":"Work"},{"key":"agent:main:other","category":"Play"}]}"""
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
controller.renameSessionGroup(from = "Work", to = "Focus")
|
||||
|
||||
@@ -424,29 +320,21 @@ class ChatControllerCommandControlsTest {
|
||||
@Test
|
||||
fun dissolveSessionGroupClearsCategoriesBestEffort() =
|
||||
runTest {
|
||||
val requests = mutableListOf<Pair<String, String?>>()
|
||||
var patchCount = 0
|
||||
val controller =
|
||||
ChatController(
|
||||
scope = this,
|
||||
json = json,
|
||||
requestGateway = { method, paramsJson ->
|
||||
requests += method to paramsJson
|
||||
when (method) {
|
||||
"sessions.list" ->
|
||||
if (paramsJson.orEmpty().contains("\"archived\":true")) {
|
||||
"""{"sessions":[{"key":"agent:main:archived","category":"Work"}]}"""
|
||||
} else {
|
||||
"""{"sessions":[{"key":"agent:main:a","category":"Work"},{"key":"agent:main:b","category":"Work"}]}"""
|
||||
}
|
||||
"sessions.patch" -> {
|
||||
patchCount += 1
|
||||
if (patchCount == 1) throw RuntimeException("offline") else "{}"
|
||||
}
|
||||
else -> "{}"
|
||||
val (controller, requests) =
|
||||
chatControllerTestSetup {
|
||||
respond("sessions.list") { paramsJson ->
|
||||
if (paramsJson.orEmpty().contains("\"archived\":true")) {
|
||||
"""{"sessions":[{"key":"agent:main:archived","category":"Work"}]}"""
|
||||
} else {
|
||||
"""{"sessions":[{"key":"agent:main:a","category":"Work"},{"key":"agent:main:b","category":"Work"}]}"""
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
respond("sessions.patch") { paramsJson ->
|
||||
patchCount += 1
|
||||
if (patchCount == 1) throw RuntimeException("offline") else "{}"
|
||||
}
|
||||
}
|
||||
|
||||
controller.dissolveSessionGroup("Work")
|
||||
|
||||
@@ -460,20 +348,11 @@ class ChatControllerCommandControlsTest {
|
||||
@Test
|
||||
fun forkSessionReturnsCreatedKeyAndRefreshesActiveSessions() =
|
||||
runTest {
|
||||
val requests = mutableListOf<Pair<String, String?>>()
|
||||
val controller =
|
||||
ChatController(
|
||||
scope = this,
|
||||
json = json,
|
||||
requestGateway = { method, paramsJson ->
|
||||
requests += method to paramsJson
|
||||
when (method) {
|
||||
"sessions.create" -> """{"session":{"key":"agent:main:forked"}}"""
|
||||
"sessions.list" -> """{"sessions":[]}"""
|
||||
else -> "{}"
|
||||
}
|
||||
},
|
||||
)
|
||||
val (controller, requests) =
|
||||
chatControllerTestSetup {
|
||||
respond("sessions.create", """{"session":{"key":"agent:main:forked"}}""")
|
||||
respond("sessions.list", """{"sessions":[]}""")
|
||||
}
|
||||
|
||||
val key = controller.forkSession("main")
|
||||
|
||||
@@ -506,23 +385,13 @@ class ChatControllerCommandControlsTest {
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@Test
|
||||
fun archivedSessionListAndOpenUnreadSessionUsePatchContracts() =
|
||||
runTest {
|
||||
val requests = mutableListOf<Pair<String, String?>>()
|
||||
val controller =
|
||||
ChatController(
|
||||
scope = this,
|
||||
json = json,
|
||||
requestGateway = { method, paramsJson ->
|
||||
requests += method to paramsJson
|
||||
when (method) {
|
||||
"sessions.list" -> """{"sessions":[{"key":"main","unread":true}]}"""
|
||||
else -> "{}"
|
||||
}
|
||||
},
|
||||
)
|
||||
val (controller, requests) =
|
||||
chatControllerTestSetup {
|
||||
respond("sessions.list", """{"sessions":[{"key":"main","unread":true}]}""")
|
||||
}
|
||||
|
||||
controller.refreshSessions(archived = true)
|
||||
advanceUntilIdle()
|
||||
@@ -544,21 +413,13 @@ class ChatControllerCommandControlsTest {
|
||||
assertTrue(patch.contains("\"unread\":false"))
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@Test
|
||||
fun sessionEventsApplyExplicitLabelAndCategoryClears() =
|
||||
runTest {
|
||||
val controller =
|
||||
ChatController(
|
||||
scope = this,
|
||||
json = json,
|
||||
requestGateway = { method, _ ->
|
||||
when (method) {
|
||||
"sessions.list" -> """{"sessions":[{"key":"main","label":"Named","category":"Work"}]}"""
|
||||
else -> "{}"
|
||||
}
|
||||
},
|
||||
)
|
||||
createScriptedChatController {
|
||||
respond("sessions.list", """{"sessions":[{"key":"main","label":"Named","category":"Work"}]}""")
|
||||
}
|
||||
|
||||
controller.refreshSessions()
|
||||
advanceUntilIdle()
|
||||
@@ -580,25 +441,17 @@ class ChatControllerCommandControlsTest {
|
||||
assertEquals(null, merged.category)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@Test
|
||||
fun failedReadAcknowledgementUnlatchesForRetry() =
|
||||
runTest {
|
||||
val requests = mutableListOf<Pair<String, String?>>()
|
||||
var failPatches = true
|
||||
val controller =
|
||||
ChatController(
|
||||
scope = this,
|
||||
json = json,
|
||||
requestGateway = { method, paramsJson ->
|
||||
requests += method to paramsJson
|
||||
when (method) {
|
||||
"sessions.patch" -> if (failPatches) throw RuntimeException("offline") else "{}"
|
||||
"sessions.list" -> """{"sessions":[{"key":"main","unread":true}]}"""
|
||||
else -> "{}"
|
||||
}
|
||||
},
|
||||
)
|
||||
val (controller, requests) =
|
||||
chatControllerTestSetup {
|
||||
respond("sessions.patch") { paramsJson ->
|
||||
if (failPatches) throw RuntimeException("offline") else "{}"
|
||||
}
|
||||
respond("sessions.list", """{"sessions":[{"key":"main","unread":true}]}""")
|
||||
}
|
||||
|
||||
controller.refreshSessions()
|
||||
advanceUntilIdle()
|
||||
@@ -616,24 +469,14 @@ class ChatControllerCommandControlsTest {
|
||||
assertEquals(2, requests.count { it.first == "sessions.patch" })
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@Test
|
||||
fun archivingOrDeletingTheOpenSessionFallsBackToMain() =
|
||||
runTest {
|
||||
val requests = mutableListOf<Pair<String, String?>>()
|
||||
val controller =
|
||||
ChatController(
|
||||
scope = this,
|
||||
json = json,
|
||||
requestGateway = { method, paramsJson ->
|
||||
requests += method to paramsJson
|
||||
when (method) {
|
||||
"sessions.list" -> """{"sessions":[{"key":"agent:main:side"}]}"""
|
||||
"sessions.delete" -> """{"deleted":true}"""
|
||||
else -> "{}"
|
||||
}
|
||||
},
|
||||
)
|
||||
val (controller, requests) =
|
||||
chatControllerTestSetup {
|
||||
respond("sessions.list", """{"sessions":[{"key":"agent:main:side"}]}""")
|
||||
respond("sessions.delete", """{"deleted":true}""")
|
||||
}
|
||||
|
||||
controller.switchSession("agent:main:side")
|
||||
advanceUntilIdle()
|
||||
@@ -650,23 +493,13 @@ class ChatControllerCommandControlsTest {
|
||||
assertEquals("main", controller.sessionKey.value)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@Test
|
||||
fun openSessionReacknowledgesUnreadOncePerEpisode() =
|
||||
runTest {
|
||||
val requests = mutableListOf<Pair<String, String?>>()
|
||||
val controller =
|
||||
ChatController(
|
||||
scope = this,
|
||||
json = json,
|
||||
requestGateway = { method, paramsJson ->
|
||||
requests += method to paramsJson
|
||||
when (method) {
|
||||
"sessions.list" -> """{"sessions":[{"key":"main","unread":false}]}"""
|
||||
else -> "{}"
|
||||
}
|
||||
},
|
||||
)
|
||||
val (controller, requests) =
|
||||
chatControllerTestSetup {
|
||||
respond("sessions.list", """{"sessions":[{"key":"main","unread":false}]}""")
|
||||
}
|
||||
|
||||
controller.refreshSessions()
|
||||
advanceUntilIdle()
|
||||
@@ -699,22 +532,13 @@ class ChatControllerCommandControlsTest {
|
||||
@Test
|
||||
fun startNewChatWithoutLoadedParentCreatesFirstSession() =
|
||||
runTest {
|
||||
val requests = mutableListOf<Pair<String, String?>>()
|
||||
val controller =
|
||||
ChatController(
|
||||
scope = this,
|
||||
json = json,
|
||||
requestGateway = { method, paramsJson ->
|
||||
requests += method to paramsJson
|
||||
when (method) {
|
||||
"sessions.create" -> """{"ok":true,"key":"agent:main:dashboard:first"}"""
|
||||
"chat.history" -> """{"sessionId":"first-session","messages":[]}"""
|
||||
"health" -> "{}"
|
||||
"sessions.list" -> """{"sessions":[]}"""
|
||||
else -> "{}"
|
||||
}
|
||||
},
|
||||
)
|
||||
val (controller, requests) =
|
||||
chatControllerTestSetup {
|
||||
respond("sessions.create", """{"ok":true,"key":"agent:main:dashboard:first"}""")
|
||||
respond("chat.history", """{"sessionId":"first-session","messages":[]}""")
|
||||
respond("health", "{}")
|
||||
respond("sessions.list", """{"sessions":[]}""")
|
||||
}
|
||||
controller.handleGatewayEvent("health", null)
|
||||
|
||||
assertTrue(controller.startNewChatAwait())
|
||||
@@ -726,34 +550,25 @@ class ChatControllerCommandControlsTest {
|
||||
assertEquals("agent:main:dashboard:first", controller.sessionKey.value)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@Test
|
||||
fun startNewChatUsesNextAvailableNewChatLabel() =
|
||||
runTest {
|
||||
val requests = mutableListOf<Pair<String, String?>>()
|
||||
val controller =
|
||||
ChatController(
|
||||
scope = this,
|
||||
json = json,
|
||||
requestGateway = { method, paramsJson ->
|
||||
requests += method to paramsJson
|
||||
when (method) {
|
||||
"sessions.create" -> """{"ok":true,"key":"agent:main:dashboard:fresh-3"}"""
|
||||
"chat.history" -> """{"sessionId":"fresh-session-3","messages":[]}"""
|
||||
"health" -> "{}"
|
||||
"sessions.list" ->
|
||||
"""
|
||||
{
|
||||
"sessions": [
|
||||
{"key":"agent:main:dashboard:fresh","displayName":"New chat"},
|
||||
{"key":"agent:main:dashboard:fresh-2","displayName":"New chat 2"}
|
||||
]
|
||||
}
|
||||
""".trimIndent()
|
||||
else -> "{}"
|
||||
val (controller, requests) =
|
||||
chatControllerTestSetup {
|
||||
respond("sessions.create", """{"ok":true,"key":"agent:main:dashboard:fresh-3"}""")
|
||||
respond("chat.history", """{"sessionId":"fresh-session-3","messages":[]}""")
|
||||
respond("health", "{}")
|
||||
respond("sessions.list") { paramsJson ->
|
||||
"""
|
||||
{
|
||||
"sessions": [
|
||||
{"key":"agent:main:dashboard:fresh","displayName":"New chat"},
|
||||
{"key":"agent:main:dashboard:fresh-2","displayName":"New chat 2"}
|
||||
]
|
||||
}
|
||||
},
|
||||
)
|
||||
""".trimIndent()
|
||||
}
|
||||
}
|
||||
controller.handleGatewayEvent("health", null)
|
||||
controller.refreshSessions()
|
||||
advanceUntilIdle()
|
||||
@@ -765,26 +580,16 @@ class ChatControllerCommandControlsTest {
|
||||
assertEquals("agent:main:dashboard:fresh-3", controller.sessionKey.value)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@Test
|
||||
fun startNewChatScopesCreateToActiveAgentSession() =
|
||||
runTest {
|
||||
val requests = mutableListOf<Pair<String, String?>>()
|
||||
val controller =
|
||||
ChatController(
|
||||
scope = this,
|
||||
json = json,
|
||||
requestGateway = { method, paramsJson ->
|
||||
requests += method to paramsJson
|
||||
when (method) {
|
||||
"sessions.create" -> """{"ok":true,"key":"agent:ops:dashboard:fresh"}"""
|
||||
"chat.history" -> """{"sessionId":"ops-session","messages":[]}"""
|
||||
"health" -> "{}"
|
||||
"sessions.list" -> """{"sessions":[]}"""
|
||||
else -> "{}"
|
||||
}
|
||||
},
|
||||
)
|
||||
val (controller, requests) =
|
||||
chatControllerTestSetup {
|
||||
respond("sessions.create", """{"ok":true,"key":"agent:ops:dashboard:fresh"}""")
|
||||
respond("chat.history", """{"sessionId":"ops-session","messages":[]}""")
|
||||
respond("health", "{}")
|
||||
respond("sessions.list", """{"sessions":[]}""")
|
||||
}
|
||||
|
||||
controller.switchSession("agent:ops:dashboard:parent")
|
||||
advanceUntilIdle()
|
||||
@@ -800,20 +605,11 @@ class ChatControllerCommandControlsTest {
|
||||
@Test
|
||||
fun bareNewSlashCommandUsesGatewayChatCommandPath() =
|
||||
runTest {
|
||||
val requests = mutableListOf<Pair<String, String?>>()
|
||||
val controller =
|
||||
ChatController(
|
||||
scope = this,
|
||||
json = json,
|
||||
requestGateway = { method, paramsJson ->
|
||||
requests += method to paramsJson
|
||||
when (method) {
|
||||
"chat.send" -> """{"runId":"run-new"}"""
|
||||
"health" -> "{}"
|
||||
else -> "{}"
|
||||
}
|
||||
},
|
||||
)
|
||||
val (controller, requests) =
|
||||
chatControllerTestSetup {
|
||||
respond("chat.send", """{"runId":"run-new"}""")
|
||||
respond("health", "{}")
|
||||
}
|
||||
controller.handleGatewayEvent("health", null)
|
||||
|
||||
assertTrue(controller.sendMessageAwaitAcceptance("/new", "off", emptyList()))
|
||||
@@ -826,20 +622,11 @@ class ChatControllerCommandControlsTest {
|
||||
@Test
|
||||
fun startNewChatRejectsWhileRunPending() =
|
||||
runTest {
|
||||
val requests = mutableListOf<Pair<String, String?>>()
|
||||
val controller =
|
||||
ChatController(
|
||||
scope = this,
|
||||
json = json,
|
||||
requestGateway = { method, paramsJson ->
|
||||
requests += method to paramsJson
|
||||
when (method) {
|
||||
"chat.send" -> """{"runId":"run-1"}"""
|
||||
"health" -> "{}"
|
||||
else -> "{}"
|
||||
}
|
||||
},
|
||||
)
|
||||
val (controller, requests) =
|
||||
chatControllerTestSetup {
|
||||
respond("chat.send", """{"runId":"run-1"}""")
|
||||
respond("health", "{}")
|
||||
}
|
||||
controller.handleGatewayEvent("health", null)
|
||||
|
||||
assertTrue(controller.sendMessageAwaitAcceptance("hello", "off", emptyList()))
|
||||
@@ -848,34 +635,24 @@ class ChatControllerCommandControlsTest {
|
||||
assertTrue(requests.none { it.first == "sessions.create" })
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@Test
|
||||
fun startNewChatRejectsDuplicateCreateWhileFirstRequestIsPending() =
|
||||
runTest {
|
||||
val requests = mutableListOf<Pair<String, String?>>()
|
||||
val createEntered = CompletableDeferred<Unit>()
|
||||
val releaseCreate = CompletableDeferred<Unit>()
|
||||
var createCount = 0
|
||||
val controller =
|
||||
ChatController(
|
||||
scope = this,
|
||||
json = json,
|
||||
requestGateway = { method, paramsJson ->
|
||||
requests += method to paramsJson
|
||||
when (method) {
|
||||
"sessions.create" -> {
|
||||
createCount += 1
|
||||
createEntered.complete(Unit)
|
||||
releaseCreate.await()
|
||||
"""{"ok":true,"key":"agent:main:dashboard:fresh"}"""
|
||||
}
|
||||
"chat.history" -> """{"sessionId":"fresh-session","messages":[]}"""
|
||||
"health" -> "{}"
|
||||
"sessions.list" -> """{"sessions":[]}"""
|
||||
else -> "{}"
|
||||
}
|
||||
},
|
||||
)
|
||||
val (controller, requests) =
|
||||
chatControllerTestSetup {
|
||||
respond("sessions.create") { paramsJson ->
|
||||
createCount += 1
|
||||
createEntered.complete(Unit)
|
||||
releaseCreate.await()
|
||||
"""{"ok":true,"key":"agent:main:dashboard:fresh"}"""
|
||||
}
|
||||
respond("chat.history", """{"sessionId":"fresh-session","messages":[]}""")
|
||||
respond("health", "{}")
|
||||
respond("sessions.list", """{"sessions":[]}""")
|
||||
}
|
||||
controller.handleGatewayEvent("health", null)
|
||||
|
||||
val first = async { controller.startNewChatAwait() }
|
||||
@@ -891,30 +668,25 @@ class ChatControllerCommandControlsTest {
|
||||
assertEquals(1, requests.count { it.first == "sessions.create" })
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@Test
|
||||
fun startNewChatIgnoresStaleCreateResponseAfterSessionSwitch() =
|
||||
runTest {
|
||||
val requests = mutableListOf<Pair<String, String?>>()
|
||||
lateinit var controller: ChatController
|
||||
controller =
|
||||
ChatController(
|
||||
scope = this,
|
||||
json = json,
|
||||
requestGateway = { method, paramsJson ->
|
||||
requests += method to paramsJson
|
||||
when (method) {
|
||||
"sessions.create" -> {
|
||||
controller.switchSession("agent:main:dashboard:other")
|
||||
"""{"ok":true,"key":"agent:main:dashboard:fresh"}"""
|
||||
}
|
||||
"chat.history" -> """{"sessionId":"other-session","messages":[]}"""
|
||||
"health" -> "{}"
|
||||
"sessions.list" -> """{"sessions":[]}"""
|
||||
else -> "{}"
|
||||
createChatController { method, paramsJson ->
|
||||
requests += method to paramsJson
|
||||
when (method) {
|
||||
"sessions.create" -> {
|
||||
controller.switchSession("agent:main:dashboard:other")
|
||||
"""{"ok":true,"key":"agent:main:dashboard:fresh"}"""
|
||||
}
|
||||
},
|
||||
)
|
||||
"chat.history" -> """{"sessionId":"other-session","messages":[]}"""
|
||||
"health" -> "{}"
|
||||
"sessions.list" -> """{"sessions":[]}"""
|
||||
else -> "{}"
|
||||
}
|
||||
}
|
||||
controller.handleGatewayEvent("health", null)
|
||||
|
||||
assertEquals(false, controller.startNewChatAwait())
|
||||
@@ -924,5 +696,12 @@ class ChatControllerCommandControlsTest {
|
||||
assertTrue(requests.any { it.first == "sessions.create" })
|
||||
}
|
||||
|
||||
private fun commandResponse(name: String): String = """{"commands":[{"name":"$name","textAliases":["/$name"],"acceptsArgs":false}]}"""
|
||||
private fun commandResponse(
|
||||
name: String,
|
||||
description: String? = null,
|
||||
acceptsArgs: Boolean = false,
|
||||
): String {
|
||||
val descriptionJson = description?.let { ""","description":"$it"""" }.orEmpty()
|
||||
return """{"commands":[{"name":"$name"$descriptionJson,"textAliases":["/$name"],"acceptsArgs":$acceptsArgs}]}"""
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -9,7 +9,6 @@ import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.test.advanceUntilIdle
|
||||
import kotlinx.coroutines.test.runCurrent
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
import org.junit.Assert.assertEquals
|
||||
@@ -18,26 +17,23 @@ import org.junit.Assert.assertNull
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class ChatControllerTerminalAckTest {
|
||||
private val json = Json { ignoreUnknownKeys = true }
|
||||
private val json = chatControllerTestJson
|
||||
|
||||
@Test
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
fun composerOwnerMustMatchBeforeSendAdmission() =
|
||||
runTest {
|
||||
val requestedMethods = mutableListOf<String>()
|
||||
var defaultAgentId: String? = "main"
|
||||
val controller =
|
||||
ChatController(
|
||||
scope = this,
|
||||
json = json,
|
||||
requestGateway = { method, _ ->
|
||||
requestedMethods += method
|
||||
"""{"runId":"run-started","status":"started"}"""
|
||||
},
|
||||
createChatController(
|
||||
cacheScope = { ChatCacheScope(gatewayId = "gateway-a", connectionGeneration = 1) },
|
||||
currentDefaultAgentId = { defaultAgentId },
|
||||
)
|
||||
) { method, _ ->
|
||||
requestedMethods += method
|
||||
"""{"runId":"run-started","status":"started"}"""
|
||||
}
|
||||
controller.handleGatewayEvent("health", null)
|
||||
val ambiguousOwner = ChatComposerOwner(gatewayStableId = "gateway-a", agentId = "main", sessionKey = "main")
|
||||
assertFalse(controller.canSendForOwner(ambiguousOwner))
|
||||
@@ -83,7 +79,6 @@ class ChatControllerTerminalAckTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
fun composerOwnerIsRecheckedAfterPendingSettingsComplete() =
|
||||
runTest {
|
||||
val settingsStarted = CompletableDeferred<Unit>()
|
||||
@@ -91,26 +86,23 @@ class ChatControllerTerminalAckTest {
|
||||
var defaultAgentId: String? = "main"
|
||||
var sendCount = 0
|
||||
val controller =
|
||||
ChatController(
|
||||
scope = this,
|
||||
json = json,
|
||||
requestGateway = { method, _ ->
|
||||
when (method) {
|
||||
"sessions.patch" -> {
|
||||
settingsStarted.complete(Unit)
|
||||
settingsGate.await()
|
||||
"{}"
|
||||
}
|
||||
"chat.send" -> {
|
||||
sendCount += 1
|
||||
"""{"runId":"run-started","status":"started"}"""
|
||||
}
|
||||
else -> "{}"
|
||||
}
|
||||
},
|
||||
createChatController(
|
||||
cacheScope = { ChatCacheScope(gatewayId = "gateway-a", connectionGeneration = 1) },
|
||||
currentDefaultAgentId = { defaultAgentId },
|
||||
)
|
||||
) { method, _ ->
|
||||
when (method) {
|
||||
"sessions.patch" -> {
|
||||
settingsStarted.complete(Unit)
|
||||
settingsGate.await()
|
||||
"{}"
|
||||
}
|
||||
"chat.send" -> {
|
||||
sendCount += 1
|
||||
"""{"runId":"run-started","status":"started"}"""
|
||||
}
|
||||
else -> "{}"
|
||||
}
|
||||
}
|
||||
controller.prepareMainSessionKey("agent:main:node-test")
|
||||
controller.handleGatewayEvent("health", null)
|
||||
controller.setThinkingLevel("high")
|
||||
@@ -139,27 +131,23 @@ class ChatControllerTerminalAckTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
fun unjournaledNotEnqueuedSendRemainsRejectedAfterOwnerChange() =
|
||||
runTest {
|
||||
val requestGate = CompletableDeferred<Unit>()
|
||||
var defaultAgentId: String? = "main"
|
||||
var defaultAgentRevision = 0L
|
||||
val controller =
|
||||
ChatController(
|
||||
scope = this,
|
||||
json = json,
|
||||
requestGateway = { method, _ ->
|
||||
if (method == "chat.send") {
|
||||
requestGate.await()
|
||||
throw GatewayRequestNotEnqueued("not enqueued")
|
||||
}
|
||||
"{}"
|
||||
},
|
||||
createChatController(
|
||||
cacheScope = { ChatCacheScope(gatewayId = "gateway-a", connectionGeneration = 1) },
|
||||
currentDefaultAgentId = { defaultAgentId },
|
||||
currentDefaultAgentRevision = { defaultAgentRevision },
|
||||
)
|
||||
) { method, _ ->
|
||||
if (method == "chat.send") {
|
||||
requestGate.await()
|
||||
throw GatewayRequestNotEnqueued("not enqueued")
|
||||
}
|
||||
"{}"
|
||||
}
|
||||
controller.prepareMainSessionKey("agent:main:node-test")
|
||||
controller.handleGatewayEvent("health", null)
|
||||
|
||||
@@ -188,20 +176,14 @@ class ChatControllerTerminalAckTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
fun terminalTimeoutAckRemovesOptimisticUserEchoAndSurfacesFailedAcceptance() =
|
||||
runTest {
|
||||
var requestedMethod: String? = null
|
||||
val controller =
|
||||
ChatController(
|
||||
scope = this,
|
||||
json = json,
|
||||
requestGateway = { method, _ ->
|
||||
requestedMethod = method
|
||||
"""{"runId":"run-timeout","status":"timeout"}"""
|
||||
},
|
||||
currentDefaultAgentId = { "main" },
|
||||
)
|
||||
createChatController { method, _ ->
|
||||
requestedMethod = method
|
||||
"""{"runId":"run-timeout","status":"timeout"}"""
|
||||
}
|
||||
controller.handleGatewayEvent("health", null)
|
||||
|
||||
val accepted =
|
||||
@@ -219,16 +201,10 @@ class ChatControllerTerminalAckTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
fun nonTerminalStartedAckRetainsOptimisticUserEchoAndPendingRun() =
|
||||
runTest {
|
||||
val controller =
|
||||
ChatController(
|
||||
scope = this,
|
||||
json = json,
|
||||
requestGateway = { _, _ -> """{"runId":"run-started","status":"started"}""" },
|
||||
currentDefaultAgentId = { "main" },
|
||||
)
|
||||
createChatController { _, _ -> """{"runId":"run-started","status":"started"}""" }
|
||||
controller.handleGatewayEvent("health", null)
|
||||
|
||||
val accepted =
|
||||
@@ -245,38 +221,32 @@ class ChatControllerTerminalAckTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
fun canonicalAckRunIdPreservesClientHistoryIdentity() =
|
||||
runTest {
|
||||
var clientRunId: String? = null
|
||||
val controller =
|
||||
ChatController(
|
||||
scope = this,
|
||||
json = json,
|
||||
requestGateway = { method, paramsJson ->
|
||||
when (method) {
|
||||
"chat.send" -> {
|
||||
clientRunId =
|
||||
requireNotNull(paramsJson)
|
||||
.let(json::parseToJsonElement)
|
||||
.jsonObject["idempotencyKey"]
|
||||
?.jsonPrimitive
|
||||
?.content
|
||||
"""{"runId":"canonical-run","status":"started"}"""
|
||||
}
|
||||
"chat.history" ->
|
||||
historyResponse(
|
||||
"session-1",
|
||||
listOf(
|
||||
ReplayHistoryMessage("user", "canonical", 1_000, idempotencyKey = "$clientRunId:user"),
|
||||
ReplayHistoryMessage("assistant", "done", 2_000),
|
||||
),
|
||||
)
|
||||
else -> "{}"
|
||||
createChatController { method, paramsJson ->
|
||||
when (method) {
|
||||
"chat.send" -> {
|
||||
clientRunId =
|
||||
requireNotNull(paramsJson)
|
||||
.let(json::parseToJsonElement)
|
||||
.jsonObject["idempotencyKey"]
|
||||
?.jsonPrimitive
|
||||
?.content
|
||||
"""{"runId":"canonical-run","status":"started"}"""
|
||||
}
|
||||
},
|
||||
currentDefaultAgentId = { "main" },
|
||||
)
|
||||
"chat.history" ->
|
||||
historyResponse(
|
||||
"session-1",
|
||||
listOf(
|
||||
ReplayHistoryMessage("user", "canonical", 1_000, idempotencyKey = "$clientRunId:user"),
|
||||
ReplayHistoryMessage("assistant", "done", 2_000),
|
||||
),
|
||||
)
|
||||
else -> "{}"
|
||||
}
|
||||
}
|
||||
controller.handleGatewayEvent("health", null)
|
||||
|
||||
assertTrue(controller.sendMessageAwaitAcceptance("canonical", "off", emptyList()))
|
||||
@@ -298,32 +268,26 @@ class ChatControllerTerminalAckTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
fun terminalOkAckClearsOptimisticUserEchoAndRefreshesHistory() =
|
||||
runTest {
|
||||
val requestedMethods = mutableListOf<String>()
|
||||
val controller =
|
||||
ChatController(
|
||||
scope = this,
|
||||
json = json,
|
||||
requestGateway = { method, _ ->
|
||||
requestedMethods += method
|
||||
when (method) {
|
||||
"chat.send" -> """{"runId":"run-ok","status":"ok"}"""
|
||||
"chat.history" ->
|
||||
"""
|
||||
{
|
||||
"sessionId": "session-1",
|
||||
"messages": [
|
||||
{ "role": "assistant", "content": "cached success reply", "timestamp": 1 }
|
||||
]
|
||||
}
|
||||
""".trimIndent()
|
||||
else -> "{}"
|
||||
}
|
||||
},
|
||||
currentDefaultAgentId = { "main" },
|
||||
)
|
||||
createChatController { method, _ ->
|
||||
requestedMethods += method
|
||||
when (method) {
|
||||
"chat.send" -> """{"runId":"run-ok","status":"ok"}"""
|
||||
"chat.history" ->
|
||||
"""
|
||||
{
|
||||
"sessionId": "session-1",
|
||||
"messages": [
|
||||
{ "role": "assistant", "content": "cached success reply", "timestamp": 1 }
|
||||
]
|
||||
}
|
||||
""".trimIndent()
|
||||
else -> "{}"
|
||||
}
|
||||
}
|
||||
controller.handleGatewayEvent("health", null)
|
||||
|
||||
val accepted =
|
||||
@@ -346,16 +310,10 @@ class ChatControllerTerminalAckTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
fun terminalErrorAckRemovesOptimisticUserEchoAndSurfacesErrorText() =
|
||||
runTest {
|
||||
val controller =
|
||||
ChatController(
|
||||
scope = this,
|
||||
json = json,
|
||||
requestGateway = { _, _ -> """{"runId":"run-error","status":"error"}""" },
|
||||
currentDefaultAgentId = { "main" },
|
||||
)
|
||||
createChatController { _, _ -> """{"runId":"run-error","status":"error"}""" }
|
||||
controller.handleGatewayEvent("health", null)
|
||||
|
||||
val accepted =
|
||||
@@ -372,18 +330,12 @@ class ChatControllerTerminalAckTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
fun definitiveRpcRejectionRestoresComposerOwnership() =
|
||||
runTest {
|
||||
val controller =
|
||||
ChatController(
|
||||
scope = this,
|
||||
json = json,
|
||||
requestGateway = { _, _ ->
|
||||
throw GatewayRequestRejected(GatewaySession.ErrorShape("INVALID_REQUEST", "message rejected"))
|
||||
},
|
||||
currentDefaultAgentId = { "main" },
|
||||
)
|
||||
createChatController { _, _ ->
|
||||
throw GatewayRequestRejected(GatewaySession.ErrorShape("INVALID_REQUEST", "message rejected"))
|
||||
}
|
||||
controller.handleGatewayEvent("health", null)
|
||||
|
||||
val accepted = controller.sendMessageAwaitAcceptance("rejected", "off", emptyList())
|
||||
@@ -395,16 +347,10 @@ class ChatControllerTerminalAckTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
fun requestNotEnqueuedRestoresComposerOwnership() =
|
||||
runTest {
|
||||
val controller =
|
||||
ChatController(
|
||||
scope = this,
|
||||
json = json,
|
||||
requestGateway = { _, _ -> throw GatewayRequestNotEnqueued("not connected") },
|
||||
currentDefaultAgentId = { "main" },
|
||||
)
|
||||
createChatController { _, _ -> throw GatewayRequestNotEnqueued("not connected") }
|
||||
controller.handleGatewayEvent("health", null)
|
||||
|
||||
val accepted = controller.sendMessageAwaitAcceptance("never sent", "off", emptyList())
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,7 @@
|
||||
package ai.openclaw.app.chat
|
||||
|
||||
import ai.openclaw.app.gateway.GatewaySession
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonArray
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
@@ -7,6 +9,96 @@ import kotlinx.serialization.json.buildJsonObject
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
|
||||
internal val chatControllerTestJson = Json { ignoreUnknownKeys = true }
|
||||
|
||||
internal fun CoroutineScope.createChatController(
|
||||
requestGatewayForGateway: (suspend (gatewayId: String, method: String, paramsJson: String?) -> String)? = null,
|
||||
captureSettingsRequestLease: ((gatewayScope: ChatCacheScope?) -> GatewaySession.RequestLease?)? = null,
|
||||
transcriptCache: ChatTranscriptCache? = null,
|
||||
cacheScope: () -> ChatCacheScope? = { null },
|
||||
currentDefaultAgentId: () -> String? = { "main" },
|
||||
currentDefaultAgentRevision: () -> Long = { 0L },
|
||||
recordModelRecent: (String) -> Unit = {},
|
||||
onSessionDeleted: (ChatSessionDeletion) -> Unit = {},
|
||||
onOfflineDefaultAgentRestored: (String) -> Unit = {},
|
||||
requestGateway: suspend (method: String, paramsJson: String?) -> String = { _, _ -> "{}" },
|
||||
): ChatController {
|
||||
val scopedRequest =
|
||||
requestGatewayForGateway ?: { _, method, paramsJson -> requestGateway(method, paramsJson) }
|
||||
val settingsLease =
|
||||
captureSettingsRequestLease ?: { gatewayScope ->
|
||||
GatewaySession.RequestLease(endpointStableId = gatewayScope?.gatewayId.orEmpty()) { method, paramsJson, _ ->
|
||||
if (gatewayScope == null) {
|
||||
requestGateway(method, paramsJson)
|
||||
} else {
|
||||
scopedRequest(gatewayScope.gatewayId, method, paramsJson)
|
||||
}
|
||||
}
|
||||
}
|
||||
return ChatController(
|
||||
scope = this,
|
||||
json = chatControllerTestJson,
|
||||
requestGateway = requestGateway,
|
||||
requestGatewayForGateway = scopedRequest,
|
||||
captureSettingsRequestLease = settingsLease,
|
||||
transcriptCache = transcriptCache,
|
||||
cacheScope = cacheScope,
|
||||
currentDefaultAgentId = currentDefaultAgentId,
|
||||
currentDefaultAgentRevision = currentDefaultAgentRevision,
|
||||
recordModelRecent = recordModelRecent,
|
||||
onSessionDeleted = onSessionDeleted,
|
||||
onOfflineDefaultAgentRestored = onOfflineDefaultAgentRestored,
|
||||
)
|
||||
}
|
||||
|
||||
internal class ChatControllerTestSetup(
|
||||
private val scope: CoroutineScope,
|
||||
) {
|
||||
val requests = mutableListOf<Pair<String, String?>>()
|
||||
var cacheScope: () -> ChatCacheScope? = { null }
|
||||
var recordModelRecent: (String) -> Unit = {}
|
||||
|
||||
private val handlers = mutableMapOf<String, suspend (String?) -> String>()
|
||||
|
||||
fun respond(
|
||||
method: String,
|
||||
responseJson: String,
|
||||
) {
|
||||
handlers[method] = { responseJson }
|
||||
}
|
||||
|
||||
fun respond(
|
||||
method: String,
|
||||
handler: suspend (paramsJson: String?) -> String,
|
||||
) {
|
||||
handlers[method] = handler
|
||||
}
|
||||
|
||||
val controller: ChatController by lazy {
|
||||
scope.createChatController(
|
||||
cacheScope = cacheScope,
|
||||
recordModelRecent = recordModelRecent,
|
||||
requestGateway = { method, paramsJson ->
|
||||
requests += method to paramsJson
|
||||
// Unscripted methods preserve the original controller-test empty-object fallback.
|
||||
handlers[method]?.invoke(paramsJson) ?: "{}"
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
operator fun component1(): ChatController = controller
|
||||
|
||||
operator fun component2(): MutableList<Pair<String, String?>> = requests
|
||||
}
|
||||
|
||||
internal fun CoroutineScope.chatControllerTestSetup(
|
||||
configure: ChatControllerTestSetup.() -> Unit,
|
||||
): ChatControllerTestSetup = ChatControllerTestSetup(this).apply(configure)
|
||||
|
||||
internal fun CoroutineScope.createScriptedChatController(
|
||||
configure: ChatControllerTestSetup.() -> Unit,
|
||||
): ChatController = chatControllerTestSetup(configure).controller
|
||||
|
||||
/**
|
||||
* Scripted gateway responder for deterministic chat replay tests.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user