1package tui
2
3import (
4 "encoding/json"
5 "strings"
6 "testing"
7
8 "github.com/patriceckhart/zot/packages/provider"
9)
10
11func TestCollapseToolCallPreservesLayout(t *testing.T) {
12 for _, layout := range []string{"box", "flat", "compact"} {
13 for _, tool := range []struct {
14 name, args, preview string
15 }{
16 {"bash", `{"command":"printf hello"}`, "$ printf hello"},
17 {"write", `{"path":"test.txt","content":"hello"}`, "hello"},
18 {"edit", `{"path":"test.txt","edits":[{"oldText":"old","newText":"hello"}]}`, "hello"},
19 } {
20 t.Run(layout+"/"+tool.name, func(t *testing.T) {
21 v := View{Theme: Dark, CollapseToolCall: true, FlatTools: layout == "flat", CompactMode: layout == "compact"}
22 for _, expanded := range []bool{false, true} {
23 v.ExpandAll = expanded
24 rows := v.RenderToolCall(ToolCallView{Name: tool.name, RawJSONBuf: tool.args}, 80)
25 found := false
26 for _, row := range rows {
27 plain := stripANSI(row)
28 if strings.Contains(plain, tool.preview) {
29 found = true
30 want := 0
31 if layout == "box" {
32 want = 2
33 }
34 if got := strings.Count(plain, "│"); got != want {
35 t.Errorf("expanded=%v: got %d borders, want %d: %s", expanded, got, want, plain)
36 }
37 }
38 if layout != "box" && strings.ContainsAny(plain, "┌┐└┘│") {
39 t.Errorf("expanded=%v: unexpected frame: %s", expanded, plain)
40 }
41 if visibleWidth(row) > 80 {
42 t.Errorf("expanded=%v: row exceeds terminal width: %s", expanded, plain)
43 }
44 }
45 if !found {
46 t.Fatalf("expanded=%v: missing preview: %v", expanded, rows)
47 }
48 }
49 })
50 }
51 }
52}
53
54func TestCollapseToolCallPreservesErrors(t *testing.T) {
55 for _, layout := range []string{"box", "flat", "compact"} {
56 for _, text := range []string{"", "failed", strings.Repeat("output\n", ToolCollapseLines+1)} {
57 for _, live := range []bool{false, true} {
58 v := View{Theme: Dark, CollapseToolCall: true, FlatTools: layout == "flat", CompactMode: layout == "compact"}
59 var rows []string
60 if live {
61 rows = v.RenderToolCall(ToolCallView{Name: "bash", Result: text, Error: true}, 80)
62 } else {
63 rows = v.renderMessage(provider.Message{Role: provider.RoleTool, Content: []provider.Content{
64 provider.ToolResultBlock{IsError: true, Content: []provider.Content{provider.TextBlock{Text: text}}},
65 }}, 80, false)
66 }
67 rendered := strings.Join(rows, "\n")
68 if !strings.Contains(rendered, v.Theme.FG256(v.Theme.Error, "error")) {
69 t.Errorf("%s live=%v: missing error indicator: %s", layout, live, stripANSI(rendered))
70 }
71 if text == "failed" && !strings.Contains(rendered, v.Theme.FG256(v.Theme.Error, text)) {
72 t.Errorf("%s live=%v: missing error styling: %q", layout, live, rendered)
73 }
74 if layout != "box" && strings.ContainsAny(stripANSI(rendered), "┌┐└┘│") {
75 t.Errorf("%s live=%v: unexpected frame: %s", layout, live, stripANSI(rendered))
76 }
77 }
78 }
79 }
80}
81
82func TestCollapseToolCallShowsLastBoxPreviewLine(t *testing.T) {
83 args := json.RawMessage(`{"command":"echo preview"}`)
84 v := View{Theme: Dark, CollapseToolCall: true, ToolCalls: []ToolCallView{{
85 ID: "toolu_1", Name: "bash", Args: ShortArgs("bash", args), Result: "first-output\nlast-output\n",
86 }}}
87 plain := stripANSI(strings.Join(v.BuildLive(80), "\n"))
88 if !strings.Contains(plain, "bash") {
89 t.Fatalf("collapsed render lost tool header:\n%s", plain)
90 }
91 if !strings.Contains(plain, "last-output") || strings.Contains(plain, "first-output") {
92 t.Fatalf("collapsed render did not show only the last result line:\n%s", plain)
93 }
94 if !strings.ContainsAny(plain, "┌┐") || !strings.ContainsAny(plain, "└┘") || !strings.Contains(plain, "│") {
95 t.Fatalf("collapsed box render did not retain the complete empty frame:\n%s", plain)
96 }
97 v.ExpandAll = true
98 expanded := stripANSI(strings.Join(v.BuildLive(80), "\n"))
99 if !strings.Contains(expanded, "first-output") {
100 t.Fatalf("ctrl-o expansion did not reveal non-compact collapsed tool result:\n%s", expanded)
101 }
102}
103
104func TestCollapseToolCallShowsLastCompactPreviewLine(t *testing.T) {
105 args := json.RawMessage(`{"command":"echo preview"}`)
106 v := View{
107 Theme: Dark,
108 CompactMode: true,
109 CollapseToolCall: true,
110 Messages: []provider.Message{
111 {Role: provider.RoleAssistant, Content: []provider.Content{
112 provider.ToolCallBlock{ID: "toolu_1", Name: "bash", Arguments: args},
113 }},
114 {Role: provider.RoleTool, Content: []provider.Content{
115 provider.ToolResultBlock{CallID: "toolu_1", Content: []provider.Content{provider.TextBlock{Text: "first-output\nlast-output"}}},
116 }},
117 },
118 }
119 plain := stripANSI(strings.Join(v.Build(80), "\n"))
120 if !strings.Contains(plain, "bash") {
121 t.Fatalf("collapsed compact render lost tool header:\n%s", plain)
122 }
123 if !strings.Contains(plain, "last-output") || strings.Contains(plain, "first-output") {
124 t.Fatalf("collapsed compact render did not show only the last result line:\n%s", plain)
125 }
126 v.ExpandAll = true
127 expanded := stripANSI(strings.Join(v.Build(80), "\n"))
128 if !strings.Contains(expanded, "first-output") {
129 t.Fatalf("ctrl-o expansion did not reveal collapsed tool result:\n%s", expanded)
130 }
131}
132
133func TestCollapseToolCallShowsLastLivePreviewLine(t *testing.T) {
134 args := json.RawMessage(`{"command":"echo preview"}`)
135 v := View{Theme: Dark, CollapseToolCall: true, ToolCalls: []ToolCallView{{
136 ID: "toolu_1", Name: "bash", Args: ShortArgs("bash", args), RawJSONBuf: string(args),
137 }}}
138 plain := stripANSI(strings.Join(v.BuildLive(80), "\n"))
139 if !strings.Contains(plain, "$ echo") {
140 t.Fatalf("collapsed live render lost its one-line preview:\n%s", plain)
141 }
142 if !strings.Contains(plain, "bash") {
143 t.Fatalf("collapsed live render lost tool header:\n%s", plain)
144 }
145}