1package tui
2
3import (
4 "encoding/json"
5 "strings"
6 "testing"
7
8 "github.com/patriceckhart/zot/packages/provider"
9)
10
11func TestLiveToolOverlayRemainsAfterAssistantToolUse(t *testing.T) {
12 args := json.RawMessage(`{"command":"sleep 1"}`)
13 v := View{
14 Theme: Dark,
15 Messages: []provider.Message{
16 {
17 Role: provider.RoleAssistant,
18 Content: []provider.Content{
19 provider.ToolCallBlock{ID: "toolu_1", Name: "bash", Arguments: args},
20 },
21 },
22 },
23 ToolCalls: []ToolCallView{
24 {ID: "toolu_1", Name: "bash", Args: ShortArgs("bash", args), Done: false},
25 },
26 }
27
28 plain := stripANSI(strings.Join(v.Build(80), "\n"))
29 if !strings.Contains(plain, "bash sleep 1") {
30 t.Fatalf("live tool overlay disappeared after assistant tool_use was appended:\n%s", plain)
31 }
32}
33
34func TestLiveToolOverlayShowsFullBashCommandBeforeResult(t *testing.T) {
35 args := json.RawMessage(`{"command":"printf 'start' && sleep 60 && printf 'done with a command long enough to exceed the header truncation limit'"}`)
36 v := View{
37 Theme: Dark,
38 ToolCalls: []ToolCallView{
39 {
40 ID: "toolu_1",
41 Name: "bash",
42 Args: ShortArgs("bash", args),
43 RawJSONBuf: string(args),
44 },
45 },
46 }
47
48 plain := stripANSI(strings.Join(v.BuildLive(100), "\n"))
49 if !strings.Contains(plain, "$ printf 'start' && sleep 60") || !strings.Contains(plain, "header truncation limit") {
50 t.Fatalf("bash command was not visible before result arrived:\n%s", plain)
51 }
52}
53
54func TestLiveToolOverlayHeightDoesNotShrinkMidStream(t *testing.T) {
55 v := View{Theme: Dark}
56 tall := json.RawMessage(`{"command":"line1\nline2\nline3\nline4\nline5"}`)
57 v.ToolCalls = []ToolCallView{
58 {ID: "toolu_1", Name: "bash", Args: ShortArgs("bash", tall), RawJSONBuf: string(tall)},
59 }
60 tallRows := len(v.Build(80))
61 if tallRows == 0 {
62 t.Fatal("expected rows for the tall command")
63 }
64
65 // Same call id, but the streamed command shrinks (a transient
66 // mid-stream state). The box must keep its high-water height so the
67 // editor/status band below doesn't bounce.
68 short := json.RawMessage(`{"command":"echo hi"}`)
69 v.ToolCalls = []ToolCallView{
70 {ID: "toolu_1", Name: "bash", Args: ShortArgs("bash", short), RawJSONBuf: string(short)},
71 }
72 if got := len(v.Build(80)); got < tallRows {
73 t.Fatalf("live box shrank mid-stream from %d to %d rows", tallRows, got)
74 }
75}
76
77func TestLiveToolReservationDoesNotLeakToNextCall(t *testing.T) {
78 v := View{Theme: Dark}
79 tall := json.RawMessage(`{"command":"line1\nline2\nline3\nline4\nline5"}`)
80 v.ToolCalls = []ToolCallView{
81 {ID: "toolu_1", Name: "bash", Args: ShortArgs("bash", tall), RawJSONBuf: string(tall)},
82 }
83 tallRows := len(v.Build(80))
84
85 // A *different* tool call that is short must NOT inherit the tall
86 // call's reservation; it renders at its own natural height. (This
87 // is the regression that produced a giant empty box.)
88 short := json.RawMessage(`{"command":"echo hi"}`)
89 v.ToolCalls = []ToolCallView{
90 {ID: "toolu_2", Name: "bash", Args: ShortArgs("bash", short), RawJSONBuf: string(short)},
91 }
92 if got := len(v.Build(80)); got >= tallRows {
93 t.Fatalf("short call inherited prior call's height: %d rows (tall was %d)", got, tallRows)
94 }
95}
96
97func TestLiveToolReservationDropsOnResult(t *testing.T) {
98 v := View{Theme: Dark}
99 tall := json.RawMessage(`{"command":"line1\nline2\nline3\nline4\nline5"}`)
100 v.ToolCalls = []ToolCallView{
101 {ID: "toolu_1", Name: "bash", Args: ShortArgs("bash", tall), RawJSONBuf: string(tall)},
102 }
103 tallRows := len(v.Build(80))
104
105 // Same id, now finalised with a tiny error result. The box must
106 // collapse to the natural result height instead of staying padded.
107 v.ToolCalls = []ToolCallView{
108 {ID: "toolu_1", Name: "bash", Args: ShortArgs("bash", tall), RawJSONBuf: string(tall), Done: true, Error: true, Result: "boom"},
109 }
110 if got := len(v.Build(80)); got >= tallRows {
111 t.Fatalf("finalised box kept live reservation: %d rows (live was %d)", got, tallRows)
112 }
113}
114
115func TestLiveToolOverlayKeepsWritePreviewAfterArgsEnd(t *testing.T) {
116 args := json.RawMessage(`{"path":"/tmp/sample.ts","content":"export const n = 1\n"}`)
117 v := View{
118 Theme: Dark,
119 Messages: []provider.Message{
120 {
121 Role: provider.RoleAssistant,
122 Content: []provider.Content{
123 provider.ToolCallBlock{ID: "toolu_1", Name: "write", Arguments: args},
124 },
125 },
126 },
127 ToolCalls: []ToolCallView{
128 {
129 ID: "toolu_1",
130 Name: "write",
131 Args: ShortArgs("write", args),
132 Streaming: false,
133 RawJSONBuf: string(args),
134 LivePath: "/tmp/sample.ts",
135 },
136 },
137 }
138
139 plain := stripANSI(strings.Join(v.Build(80), "\n"))
140 if !strings.Contains(plain, "export const n = 1") {
141 t.Fatalf("write preview collapsed after tool args ended but before tool_result arrived:\n%s", plain)
142 }
143}
144
145func TestLiveToolOverlayHidesAfterToolResult(t *testing.T) {
146 args := json.RawMessage(`{"command":"sleep 1"}`)
147 v := View{
148 Theme: Dark,
149 Messages: []provider.Message{
150 {
151 Role: provider.RoleAssistant,
152 Content: []provider.Content{
153 provider.ToolCallBlock{ID: "toolu_1", Name: "bash", Arguments: args},
154 },
155 },
156 {
157 Role: provider.RoleTool,
158 Content: []provider.Content{
159 provider.ToolResultBlock{
160 CallID: "toolu_1",
161 Content: []provider.Content{provider.TextBlock{Text: "done"}},
162 },
163 },
164 },
165 },
166 ToolCalls: []ToolCallView{
167 {ID: "toolu_1", Name: "bash", Args: ShortArgs("bash", args), Result: "done", Done: true},
168 },
169 }
170
171 plain := stripANSI(strings.Join(v.BuildLive(80), "\n"))
172 if strings.Contains(plain, "bash sleep 1") {
173 t.Fatalf("live tool overlay still rendered after tool_result was appended:\n%s", plain)
174 }
175}