Skip to content

Commit 31006cd

Browse files
committed
test(chat): align process-fold tests with the card contract
The card model (one card per run of ≥ 2 steps, closed the moment reply text arrives, expanded by the card's own key) landed with tests still asserting the old round rules: ≤ 2 steps never fold, a streaming message keeps its card running after the reply, and expansion is keyed by the bare message id. Those assertions could not pass against the card projection, which is why pr-check stayed red. Pin the card rules instead: a two-step run cards and a single step stays inline, reply text settles the card even while the message streams, and expansion uses the card key the toggle actually stores.
1 parent d7d4132 commit 31006cd

2 files changed

Lines changed: 66 additions & 35 deletions

File tree

‎src/android/app/src/test/java/com/openminis/app/ui/chat/ChatFlatItemsProcessFoldTest.kt‎

Lines changed: 57 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,18 @@ import org.junit.Assert.assertTrue
77
import org.junit.Test
88

99
/**
10-
* [T-android-round-collapse] Fold contracts under the round model.
10+
* [P-process-card] Fold contracts under the process-card model.
1111
*
1212
* A round = one user message + the assistant/system messages after it up to
13-
* the next user message. Fold rules:
14-
* 1. rounds with ≤ 2 foldable steps never fold (a bar for two rows is noise);
15-
* 2. a RUNNING round is always fully open (effectiveExpanded =
16-
* isRunning || expanded) — completion auto-collapses with no trigger;
17-
* 3. a collapsed round keeps its LATEST 2 steps visible;
18-
* 4. one bar per round, owned by the round's LAST assistant message
19-
* (messageId == that message's id, which is also the expansion key).
13+
* the next user message. Inside a round, reply text splits the process into
14+
* runs, and each run of ≥ 2 steps (thinking or tool, including a tool that
15+
* is still running) becomes one card. A single step stays inline.
16+
*
17+
* Cards own their steps: collapsed or expanded, the steps render inside the
18+
* card, never back in the list. The expansion key is the card's own key
19+
* (`<round key>--<first step id>`), not the message id, so opening one card
20+
* never opens another. A running card reports the live step; reply text ends
21+
* the card immediately.
2022
*/
2123
class ChatFlatItemsProcessFoldTest {
2224

@@ -85,19 +87,29 @@ class ChatFlatItemsProcessFoldTest {
8587
}
8688

8789
@Test
88-
fun `round with at most two foldable steps never folds`() {
89-
// Rule 1: a fold bar summarising two rows is noise — the round stays
90-
// open even with folding enabled.
91-
val items = buildFlatChatItems(
90+
fun `a two step run becomes a card and a single step stays inline`() {
91+
// [P-process-card] The card threshold is the RUN, not the round:
92+
// two process steps are enough to card, one step is noise and stays
93+
// inline. The old round rule (≤ 2 steps never fold) predates cards.
94+
val carded = buildFlatChatItems(
9295
listOf(assistant(blocks = listOf(thinking(), tool(), text()))),
9396
showCompletedToolCards = false,
9497
foldAiProcess = true,
9598
)
96-
val k = kinds(items)
97-
assertFalse(k.contains("summary"))
98-
assertTrue(k.contains("thinking"))
99-
assertTrue(k.contains("tool:bash"))
100-
assertTrue(k.contains("md"))
99+
val k = kinds(carded)
100+
assertEquals(listOf("header", "summary", "md"), k.filter { it in setOf("header", "summary", "md", "thinking", "tool:bash") })
101+
assertFalse(k.contains("thinking"))
102+
assertFalse(k.contains("tool:bash"))
103+
104+
val inline = buildFlatChatItems(
105+
listOf(assistant(blocks = listOf(thinking(), text()))),
106+
showCompletedToolCards = false,
107+
foldAiProcess = true,
108+
)
109+
val ik = kinds(inline)
110+
assertFalse(ik.contains("summary"))
111+
assertTrue(ik.contains("thinking"))
112+
assertTrue(ik.contains("md"))
101113
}
102114

103115
@Test
@@ -186,63 +198,74 @@ class ChatFlatItemsProcessFoldTest {
186198
foldAiProcess = true,
187199
)
188200
val k = kinds(items)
189-
// A running run of ≥ 2 steps is already a card; its steps render
190-
// inside the card, not in the list. The reply text stays outside.
201+
// Reply text ends the card the moment it arrives, even while the
202+
// message is still streaming — "输出中" was dropped because it would
203+
// flash for one frame. The steps render inside the card.
191204
assertTrue(k.contains("summary"))
192205
assertFalse(k.contains("thinking"))
193206
assertFalse(k.contains("tool:bash"))
194207
assertTrue(k.contains("md"))
195208
val summary = items.filterIsInstance<FlatChatItem.AssistantProcessSummary>().single()
196-
assertTrue(summary.running)
209+
assertFalse(summary.running)
197210
}
198211

199212
@Test
200213
fun `running round stays open and folds when it finishes`() {
201-
// The auto-collapse needs no trigger of its own: the round simply
202-
// stops forcing itself open the moment it stops running.
203-
val blocks = listOf(thinking(), tool(id = "t1"), tool(id = "t2"), text())
214+
// The card is live only while one of its own steps is still running.
215+
// The moment the reply text arrives the card settles, whether or not
216+
// the message itself is still streaming.
217+
val blocks = listOf(thinking(), tool(id = "t1"), tool(id = "t2", status = ToolBlockStatus.RUNNING))
204218
val running = buildFlatChatItems(
205219
listOf(assistant(streaming = true, blocks = blocks)),
206220
showCompletedToolCards = false,
207221
foldAiProcess = true,
208222
)
209223
val rk = kinds(running)
210224
assertTrue(rk.contains("summary"))
225+
assertFalse(rk.contains("thinking"))
211226
assertTrue(running.filterIsInstance<FlatChatItem.AssistantProcessSummary>().single().running)
212227

213228
val finished = buildFlatChatItems(
214-
listOf(assistant(streaming = false, blocks = blocks)),
229+
listOf(assistant(streaming = true, blocks = blocks + text())),
215230
showCompletedToolCards = false,
216231
foldAiProcess = true,
217232
)
218233
val fk = kinds(finished)
219234
assertTrue(fk.contains("summary"))
220235
assertFalse(fk.contains("thinking"))
236+
assertFalse(finished.filterIsInstance<FlatChatItem.AssistantProcessSummary>().single().running)
221237
}
222238

223239
@Test
224240
fun `fold on awaiting keeps the running round open`() {
225-
// Awaiting (between tool rounds) is still a running round.
241+
// Awaiting is a running round, but a card closes when its reply text
242+
// arrives. The steps are inside the card; the reply stays outside.
226243
val items = buildFlatChatItems(
227244
listOf(assistant(awaiting = true, blocks = listOf(thinking(), tool(), text()))),
228245
showCompletedToolCards = false,
229246
foldAiProcess = true,
230247
)
231248
val k = kinds(items)
232-
// Awaiting is still a running round, so the card is live.
233249
assertTrue(k.contains("summary"))
234-
assertTrue(items.filterIsInstance<FlatChatItem.AssistantProcessSummary>().single().running)
250+
assertFalse(items.filterIsInstance<FlatChatItem.AssistantProcessSummary>().single().running)
235251
assertFalse(k.contains("thinking"))
236252
assertTrue(k.contains("md"))
237253
}
238254

239255
@Test
240256
fun `tap expand restores thinking and completed tools`() {
257+
// The toggle stores the card's own key (round key + first step),
258+
// never the bare message id — one message can hold several cards.
259+
val collapsed = buildFlatChatItems(
260+
listOf(assistant(blocks = listOf(thinking(), tool(id = "t1"), tool(id = "t2"), text()))),
261+
showCompletedToolCards = false,
262+
foldAiProcess = true,
263+
).filterIsInstance<FlatChatItem.AssistantProcessSummary>().single()
241264
val items = buildFlatChatItems(
242265
listOf(assistant(blocks = listOf(thinking(), tool(id = "t1"), tool(id = "t2"), text()))),
243266
showCompletedToolCards = false,
244267
foldAiProcess = true,
245-
expandedProcessIds = setOf("m1"),
268+
expandedProcessIds = setOf(collapsed.messageId),
246269
)
247270
val k = kinds(items)
248271
// Expanding renders the steps INSIDE the card, never back in the list.
@@ -440,11 +463,16 @@ class ChatFlatItemsProcessFoldTest {
440463
tool(id = "tool2", name = "bash", status = ToolBlockStatus.SUCCESS),
441464
text(),
442465
)
466+
val cardKey = buildFlatChatItems(
467+
listOf(assistant(blocks = blocks)),
468+
showCompletedToolCards = false,
469+
foldAiProcess = true,
470+
).filterIsInstance<FlatChatItem.AssistantProcessSummary>().single().messageId
443471
val expanded = buildFlatChatItems(
444472
listOf(assistant(blocks = blocks)),
445473
showCompletedToolCards = false,
446474
foldAiProcess = true,
447-
expandedProcessIds = setOf("m1"),
475+
expandedProcessIds = setOf(cardKey),
448476
)
449477
val k = kinds(expanded)
450478
val summary = expanded.filterIsInstance<FlatChatItem.AssistantProcessSummary>().single()

‎src/android/app/src/test/java/com/openminis/app/ui/chat/ChatFlatItemsToolOnlyFoldTest.kt‎

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,10 @@ import org.junit.Test
1010
* consisting ONLY of tool calls (no reply text) rendered a large blank band —
1111
* the standalone AssistantHeader row above an otherwise empty collapsed turn.
1212
*
13-
* [T-android-round-collapse] Under the round model the blank band is
14-
* structurally impossible for single-message rounds (folding needs ≥ 3
15-
* foldable steps and always keeps the latest 2), so the header-hidden path
16-
* only fires for a message whose blocks all folded into a multi-message
17-
* round's bar — which is what these tests pin down.
13+
* [P-process-card] A tool-only run of ≥ 2 steps is one card and the message
14+
* has nothing left to render outside it, so the header hides. Expanding the
15+
* card keeps the steps inside the card — the header stays hidden. That is
16+
* the path these tests pin down.
1817
*/
1918
class ChatFlatItemsToolOnlyFoldTest {
2019

@@ -81,10 +80,14 @@ class ChatFlatItemsToolOnlyFoldTest {
8180

8281
@Test
8382
fun `expanded round restores header and tool cards`() {
83+
val cardKey = buildFlatChatItems(
84+
listOf(assistant(blocks = listOf(tool("t1"), tool("t2"), tool("t3")))),
85+
foldAiProcess = true,
86+
).filterIsInstance<FlatChatItem.AssistantProcessSummary>().single().messageId
8487
val items = buildFlatChatItems(
8588
listOf(assistant(blocks = listOf(tool("t1"), tool("t2"), tool("t3")))),
8689
foldAiProcess = true,
87-
expandedProcessIds = setOf("m1"),
90+
expandedProcessIds = setOf(cardKey),
8891
)
8992
val k = kinds(items)
9093
// Expanding keeps the steps inside the card, so the header stays

0 commit comments

Comments
 (0)