返回 DeepSeek-Reasonix
consecutive_tool_markers_test.go
根目录 / internal / cli / consecutive_tool_markers_test.go
1 package cli
2
3 import (
4 "strings"
5 "testing"
6
7 "reasonix/internal/event"
8 )
9
10 // 3 parallel Bash(ls) in one turn, ids "call_<n>" (no "shell-" prefix), each
11 // streams 22 lines then finishes. Every card must keep its own "⎿ 22 lines"
12 // marker. Regression: collapseShellSlot's late path recovered the count only
13 // from shellOutputs ("shell-" ids), so call_N ids fell through to "-1 lines".
14 func TestParallelBashMarkersKeepOwnLineCount(t *testing.T) {
15 m := newTestChatTUI()
16 ids := []string{"call_1", "call_2", "call_3"}
17 for _, id := range ids {
18 m.ingestEvent(event.Event{Kind: event.ToolDispatch, Tool: event.Tool{ID: id, Name: "bash", Partial: true}})
19 }
20 for _, id := range ids {
21 m.ingestEvent(event.Event{Kind: event.ToolDispatch, Tool: event.Tool{ID: id, Name: "bash", Args: `{"command":"ls"}`, Partial: false}})
22 }
23 for _, id := range ids {
24 for i := 0; i < 22; i++ {
25 m.ingestEvent(event.Event{Kind: event.ToolProgress, Tool: event.Tool{ID: id, Output: "line\n"}})
26 }
27 }
28 for _, id := range ids {
29 m.ingestEvent(event.Event{Kind: event.ToolResult, Tool: event.Tool{ID: id, Name: "bash", Output: strings.Repeat("line\n", 22)}})
30 }
31 transcript := m.transcript
32 if joined := strings.Join(transcript, "\n"); strings.Contains(joined, "-1 lines") {
33 t.Fatalf("transcript must not contain a negative line count:\n%s", joined)
34 }
35 // Locate each card and assert the marker directly below it contains the
36 // correct line count. With 22 lines of output per call the summary is
37 // "⎿ 22 lines".
38 cardIdx := map[string]int{}
39 for i, ln := range transcript {
40 if idx, ok := cardIdx["c1"]; !ok && strings.Contains(ln, "Bash(ls)") {
41 _ = idx
42 if _, seen := cardIdx["c1"]; !seen {
43 cardIdx["c1"] = i
44 continue
45 }
46 }
47 if _, seen := cardIdx["c2"]; !seen && len(cardIdx) == 1 && strings.Contains(ln, "Bash(ls)") {
48 cardIdx["c2"] = i
49 continue
50 }
51 if _, seen := cardIdx["c3"]; !seen && len(cardIdx) == 2 && strings.Contains(ln, "Bash(ls)") {
52 cardIdx["c3"] = i
53 }
54 }
55 if len(cardIdx) != 3 {
56 t.Fatalf("expected three bash cards in transcript, got %v\n%s", cardIdx, strings.Join(transcript, "\n"))
57 }
58 for name, idx := range cardIdx {
59 marker := transcript[idx+1]
60 if !strings.Contains(marker, "⎿") {
61 t.Fatalf("%s: marker slot at transcript[%d] should contain ⎿, got %q\nfull transcript:\n%s",
62 name, idx+1, marker, strings.Join(transcript, "\n"))
63 }
64 if !strings.Contains(marker, "22 lines") {
65 t.Fatalf("%s: marker slot at transcript[%d] should report 22 lines, got %q\nfull transcript:\n%s",
66 name, idx+1, marker, strings.Join(transcript, "\n"))
67 }
68 }
69 }
70
71 // No-streaming variant: a second Bash dispatches before the first emits any
72 // ToolProgress, and the first's result lands last. The slot must still show
73 // "⎿ N lines" driven by the ToolResult's own Output, not "-1 lines" or blank.
74 func TestNonShellToolLateResultShowsCorrectCount(t *testing.T) {
75 m := newTestChatTUI()
76 m.ingestEvent(event.Event{Kind: event.ToolDispatch, Tool: event.Tool{ID: "call_a", Name: "bash", Args: `{"command":"echo a"}`}})
77 m.ingestEvent(event.Event{Kind: event.ToolDispatch, Tool: event.Tool{ID: "call_b", Name: "bash", Args: `{"command":"echo b"}`}})
78 // No ToolProgress for either; the result is the only signal.
79 m.ingestEvent(event.Event{Kind: event.ToolResult, Tool: event.Tool{ID: "call_a", Name: "bash", Output: "a\nsecond\nthird\n"}})
80 m.ingestEvent(event.Event{Kind: event.ToolResult, Tool: event.Tool{ID: "call_b", Name: "bash", Output: "b\n"}})
81 transcript := m.transcript
82 joined := strings.Join(transcript, "\n")
83 if strings.Contains(joined, "-1 lines") {
84 t.Fatalf("transcript must not contain a negative line count:\n%s", joined)
85 }
86 wantSubstrings := map[string]string{
87 "call_a": "3 lines", // a\nsecond\nthird\n → 3 lines
88 "call_b": "1 lines", // b\n → 1 line
89 }
90 cardIdx := map[string]int{}
91 for i, ln := range transcript {
92 if strings.Contains(ln, "echo a") {
93 cardIdx["call_a"] = i
94 }
95 if strings.Contains(ln, "echo b") {
96 cardIdx["call_b"] = i
97 }
98 }
99 for id, want := range wantSubstrings {
100 idx, ok := cardIdx[id]
101 if !ok {
102 t.Fatalf("missing %s card in transcript:\n%s", id, joined)
103 }
104 marker := transcript[idx+1]
105 if !strings.Contains(marker, "⎿") {
106 t.Fatalf("%s: marker at transcript[%d] should contain ⎿, got %q", id, idx+1, marker)
107 }
108 if !strings.Contains(marker, want) {
109 t.Fatalf("%s: marker at transcript[%d] should report %s, got %q", id, idx+1, want, marker)
110 }
111 }
112 }
113
113 lines GO