Being open did two things to grouping, and only one of them was wanted. It held a call standing on its own out of the run it belongs to, so a command finishing behind the card being read no longer shuts it and folds it away mid-sentence. It also took a call *out* of the group it was already inside, and that is what made collapsing jump: grouping is what gives a row its identity, so one tap rebuilt the rows around the finger -- opening a call inside a group split the group into two pieces with mismatched keys, and closing one replaced three rows with one, which no anchor survives. Measured at 450px of jump, with the card that was closed going with it. So the held-out set is now the screen's, not the transcript's: a call that has never been drawn inside a group and is open stands out of its run, and a call that has been in one stays in it whatever the reader does to it. Being inside a group once is a fact about what the reader has been shown, which is why the screen is what remembers it. Checked with ktfmtFormat, compileDebugKotlin, lintDebug and testDebugUnitTest, and on the emulator against the sandbox: opening a call inside an open group of six leaves it one group of six and closing it returns every row to the pixel it came from; a call opened while standing alone survives a reply landing behind it, and folds back into "Called 3 tools" when it is closed without moving the rows below it.
168 lines
6.3 KiB
Kotlin
168 lines
6.3 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, the last call in the
|
|
* transcript, and one held out because the reader has it open are drawn on their 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,
|
|
)
|
|
|
|
/** Something that is not a tool call, to put behind the run so its last call folds in. */
|
|
private fun reply() = TranscriptItem.AssistantMsg(seq = ++seq, text = "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 ?: "reply")
|
|
}
|
|
}
|
|
|
|
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),
|
|
call("d", runId = "a"),
|
|
reply(),
|
|
)
|
|
)
|
|
assertEquals(
|
|
listOf(listOf("a", "b"), listOf("c"), listOf("d"), listOf("reply")),
|
|
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"),
|
|
reply(),
|
|
)
|
|
)
|
|
assertEquals(
|
|
listOf(listOf("a"), listOf("b"), listOf("c", "d"), listOf("reply")),
|
|
shape(rows),
|
|
)
|
|
assertKeysDistinct(rows)
|
|
}
|
|
|
|
/**
|
|
* A call held out is one the reader opened while it stood on its own; being overtaken while
|
|
* they read it does not fold it away, and closing it hands it back to its run.
|
|
*/
|
|
@Test
|
|
fun a_held_out_call_stays_out_of_its_group() {
|
|
val calls =
|
|
listOf(
|
|
call("a"),
|
|
call("b", runId = "a"),
|
|
call("c", runId = "a"),
|
|
call("d", runId = "a"),
|
|
reply(),
|
|
)
|
|
|
|
val whileHeld = groupToolRuns(calls, heldOut = setOf("d"))
|
|
val afterItCloses = groupToolRuns(calls)
|
|
|
|
assertEquals(listOf(listOf("a", "b", "c"), listOf("d"), listOf("reply")), shape(whileHeld))
|
|
assertTrue(whileHeld[1] is TranscriptRow.Single, "$whileHeld")
|
|
assertKeysDistinct(whileHeld)
|
|
assertEquals(listOf(listOf("a", "b", "c", "d"), listOf("reply")), shape(afterItCloses))
|
|
assertTrue(afterItCloses.first() is TranscriptRow.Tools, "$afterItCloses")
|
|
}
|
|
|
|
/**
|
|
* 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)
|
|
}
|
|
|
|
@Test
|
|
fun a_run_that_reappears_after_another_row_keeps_distinct_keys() {
|
|
val rows =
|
|
groupToolRuns(
|
|
listOf(
|
|
call("older", runId = "exec-1"),
|
|
call("older-2", runId = "exec-1"),
|
|
reply(),
|
|
call("exec-1", runId = "exec-1"),
|
|
call("newer", runId = "exec-1"),
|
|
reply(),
|
|
)
|
|
)
|
|
|
|
assertTrue(rows[0] is TranscriptRow.Tools, "$rows")
|
|
assertTrue(rows[2] is TranscriptRow.Tools, "$rows")
|
|
assertKeysDistinct(rows)
|
|
assertEquals("exec-1", rows[0].key)
|
|
assertEquals("exec-1/exec-1", rows[2].key)
|
|
}
|
|
|
|
/**
|
|
* Finishing is not what folds a call back in -- being overtaken is. A session that has run its
|
|
* last command and is writing its reply leaves that command standing until the reply starts.
|
|
*/
|
|
@Test
|
|
fun the_last_call_stays_out_when_it_finishes_and_folds_in_when_something_follows() {
|
|
val a = call("a")
|
|
val running = call("b", runId = "a", done = false)
|
|
val finished = running.copy(done = true)
|
|
val whileRunning = groupToolRuns(listOf(a, running))
|
|
val afterItEnds = groupToolRuns(listOf(a, finished))
|
|
val afterTheReply = groupToolRuns(listOf(a, finished, reply()))
|
|
assertEquals(listOf(listOf("a"), listOf("b")), shape(whileRunning))
|
|
assertEquals(listOf(listOf("a"), listOf("b")), shape(afterItEnds))
|
|
assertEquals(listOf(listOf("a", "b"), listOf("reply")), shape(afterTheReply))
|
|
// The run keeps the key it was drawn under throughout, so the list rebuilds a row rather
|
|
// than losing its anchor.
|
|
assertEquals(whileRunning.first().key, afterItEnds.first().key)
|
|
assertEquals(whileRunning.first().key, afterTheReply.first().key)
|
|
}
|
|
|
|
@Test
|
|
fun a_run_of_finished_calls_is_one_group_once_something_follows_it() {
|
|
val rows =
|
|
groupToolRuns(
|
|
listOf(call("a"), call("b", runId = "a"), call("c", runId = "a"), reply())
|
|
)
|
|
assertEquals(listOf(listOf("a", "b", "c"), listOf("reply")), shape(rows))
|
|
assertTrue(rows.first() is TranscriptRow.Tools, "$rows")
|
|
}
|
|
}
|