Files
ai-app/app/androidApp/src/test/kotlin/com/example/aiapp/ToolRowsTest.kt
T
iris-ai 036eb375aa Keep the running tool call outside its group
A run of adjacent calls is drawn as one collapsed card, which hid the one
thing worth seeing without opening anything: the command the session is
running right now. It is a row of its own while it runs and folds back into
the run when it ends.

Grouping stays a display decision, so the pieces a running call cuts a run
into are keyed there. The first piece keeps the run's name -- that name is
what survives a page of history landing in front of it -- and later pieces
take their own first call's id behind it, since the call a run was named
after can itself be the one running.

The echo rig's /tools gap now runs between a call's start and its end rather
than between one call and the next, which is where a real session's time goes
and what makes the running state observable at all.

Checked with ktfmtFormat, compileDebugKotlin, testDebugUnitTest (new
ToolRowsTest) and lintDebug, cargo fmt/clippy/test, and on the emulator
against the sandbox: "Called 2 tools" with the live Bash card beneath it.
2026-09-15 01:13:03 -04:00

94 lines
3.5 KiB
Kotlin

package com.example.aiapp
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue
/**
* How a run of tool calls is cut into rows: the call still running is drawn on its own, and every
* piece the cut leaves behind still has a key of its own -- two rows sharing one key take the app
* down, and a key that moves takes the reader's place with it.
*/
class ToolRowsTest {
private var seq = 0L
private fun call(id: String, runId: String = id, done: Boolean = true) =
TranscriptItem.ToolRun(
seq = ++seq,
id = id,
runId = runId,
tool = "Bash",
input = "{}",
output = if (done) "ok" else "",
done = done,
)
private fun shape(rows: List<TranscriptRow>) = rows.map { row ->
when (row) {
is TranscriptRow.Tools -> row.calls.map { it.id }
is TranscriptRow.Single -> listOf((row.item as TranscriptItem.ToolRun).id)
}
}
private fun assertKeysDistinct(rows: List<TranscriptRow>) =
assertEquals(rows.size, rows.map { it.key }.toSet().size, "$rows")
@Test
fun the_call_still_running_is_a_row_of_its_own() {
val rows =
groupToolRuns(
listOf(call("a"), call("b", runId = "a"), call("c", runId = "a", done = false))
)
assertEquals(listOf(listOf("a", "b"), listOf("c")), shape(rows))
assertKeysDistinct(rows)
}
@Test
fun a_call_running_in_the_middle_of_its_run_splits_the_group_in_two() {
val rows =
groupToolRuns(
listOf(
call("a"),
call("b", runId = "a", done = false),
call("c", runId = "a"),
call("d", runId = "a"),
)
)
assertEquals(listOf(listOf("a"), listOf("b"), listOf("c", "d")), shape(rows))
assertKeysDistinct(rows)
}
/**
* The one case where the run's name is a call that is not in the run's first row: a page of
* history joined onto a run whose own first call is still going ([joinPages] renames the older
* calls to the newer run's name). Both rows would key on that name.
*/
@Test
fun the_run_keeps_its_name_even_when_the_call_it_is_named_after_is_the_one_running() {
val rows = groupToolRuns(listOf(call("a", runId = "b"), call("b", done = false)))
assertEquals(listOf(listOf("a"), listOf("b")), shape(rows))
assertKeysDistinct(rows)
assertEquals("b", rows.first().key)
}
/** What happens the moment a command finishes: it folds back into the run it was cut out of. */
@Test
fun a_call_that_finishes_rejoins_its_run_without_moving_the_run() {
val running = listOf(call("a"), call("b", runId = "a", done = false))
val finished = listOf(running[0], (running[1] as TranscriptItem.ToolRun).copy(done = true))
val before = groupToolRuns(running)
val after = groupToolRuns(finished)
assertEquals(listOf(listOf("a", "b")), shape(after))
// The run keeps the key it was drawn under, so the list rebuilds a row rather than losing
// its anchor.
assertEquals(before.first().key, after.first().key)
}
@Test
fun a_run_of_finished_calls_is_still_one_group() {
val rows = groupToolRuns(listOf(call("a"), call("b", runId = "a"), call("c", runId = "a")))
assertEquals(listOf(listOf("a", "b", "c")), shape(rows))
assertTrue(rows.single() is TranscriptRow.Tools, "$rows")
}
}