Fixed tool calling history
CI / Gateway plugin tests (push) Successful in 5m30s
CI / Kotlin tests (android host + desktop) (push) Successful in 6m57s

This commit is contained in:
ARIA committed 2026-08-22 13:22:07 +02:00
1 parent dd43033888
commit 524ed8ce53
11 files changed
+471 -55

No files matched your search

@@ -1,5 +1,9 @@
package iris.data
import iris.protocol.Frame
import iris.protocol.IrisJson
import iris.protocol.TYPE_TOOL_START
import iris.protocol.ToolStartPayload
import kotlin.test.Test
import kotlin.test.assertEquals
@@ -10,13 +14,14 @@ class ChatStoreCacheTest {
store.loadFromCache(
mapOf(
"android:default" to
listOf(
listOf<ChatItem>(
MessageItem(id = "m1", role = "user", text = "hi", ts = 1),
ToolItem(id = "tool_1", index = 0, name = "bash", done = true, anchorId = "m1"),
MessageItem(id = "m2", role = "assistant", text = "hello", ts = 2),
),
),
)
assertEquals(listOf("m1", "m2"), store.lanes.value["android:default"]!!.map { it.id })
assertEquals(listOf("m1", "tool_1", "m2"), store.lanes.value["android:default"]!!.map { it.id })
}
@Test
@@ -26,4 +31,83 @@ class ChatStoreCacheTest {
store.loadFromCache(emptyMap())
assertEquals(1, store.lanes.value["android:default"]!!.size)
}
@Test
fun loadHistoryKeepsToolCardsAtAnchor() {
val store = ChatStore()
// Lane as restored from the cache: user message, tool card anchored
// to it, and the final answer.
store.loadFromCache(
mapOf(
"android:default" to
listOf<ChatItem>(
MessageItem(id = "m1", role = "user", text = "count", ts = 100),
ToolItem(id = "tool_1", index = 0, name = "terminal", done = true, anchorId = "m1"),
MessageItem(id = "m2", role = "assistant", text = "16", ts = 200),
),
),
)
// A history refresh (authoritative messages, no tool cards) must keep
// the tool card between the user message and the answer — not push
// it to the end.
store.loadHistory(
"android:default",
listOf(
MessageItem(id = "m1", role = "user", text = "count", ts = 100),
MessageItem(id = "m2", role = "assistant", text = "16", ts = 200),
),
)
assertEquals(listOf("m1", "tool_1", "m2"), store.lanes.value["android:default"]!!.map { it.id })
}
@Test
fun loadHistoryDedupesAndKeepsUnanchoredToolsLast() {
val store = ChatStore()
store.loadFromCache(
mapOf(
"android:default" to
listOf<ChatItem>(
MessageItem(id = "m1", role = "user", text = "count", ts = 100),
ToolItem(id = "tool_1", index = 0, name = "bash", done = true),
),
),
)
store.loadHistory(
"android:default",
listOf(MessageItem(id = "m1", role = "user", text = "count", ts = 100)),
)
// A card whose anchor is unknown falls to the end (degenerate case).
assertEquals(listOf("m1", "tool_1"), store.lanes.value["android:default"]!!.map { it.id })
}
@Test
fun toolStartIdsNeverCollideWithRestoredCards() {
val store = ChatStore()
// Lane restored from the cache after a restart, holding a persisted
// card minted by the PREVIOUS process.
store.loadFromCache(
mapOf(
"android:default" to
listOf<ChatItem>(
MessageItem(id = "m1", role = "user", text = "hi", ts = 1),
ToolItem(id = "tool_1", index = 0, name = "bash", done = true, anchorId = "m1"),
),
),
)
// A new turn must not re-mint an id the restored lane already holds
// (duplicate LazyColumn key / upsert overwrite under the same PK).
store.onFrame(
Frame(
type = TYPE_TOOL_START,
chatId = "android",
payload =
IrisJson.instance.encodeToJsonElement(
ToolStartPayload.serializer(),
ToolStartPayload(index = 0, name = "terminal", preview = "ls"),
),
),
)
val ids = store.lanes.value["android:default"]!!.map { it.id }
assertEquals(ids.size, ids.toSet().size)
}
}