1package tui
2
3import (
4 "strings"
5 "testing"
6
7 "github.com/bnema/zut/packages/provider"
8)
9
10// TestStatusBarAlwaysTwoLines verifies the status bar always emits
11// two lines when a cwd is present, regardless of terminal width, and
12// that the cwd is indented with the 2-space pad so it lines up under
13// the "(provider)" column on line 1.
14func TestStatusBarAlwaysTwoLines(t *testing.T) {
15 // Wide terminal that would previously combine into one line.
16 lines := StatusBar(StatusBarParams{
17 Theme: Dark,
18 Provider: "anthropic",
19 Model: "claude-opus-4-7",
20 CWD: "/tmp/x",
21 Usage: provider.Usage{
22 InputTokens: 476_000,
23 OutputTokens: 3_400,
24 CostUSD: 1.242,
25 },
26 Subscription: true,
27 ContextUsed: 55_000,
28 ContextMax: 1_000_000,
29 Cols: 500, // very wide
30 })
31 if len(lines) != 2 {
32 t.Fatalf("want 2 lines, got %d: %q", len(lines), lines)
33 }
34 if !strings.Contains(lines[0], "claude-opus-4-7") {
35 t.Errorf("line 1 should contain model, got %q", lines[0])
36 }
37 // Line 2 must start with 2-space indent.
38 if !strings.HasPrefix(lines[1], " ") {
39 t.Errorf("line 2 should start with 2-space indent, got %q", lines[1])
40 }
41 if !strings.Contains(lines[1], "/tmp/x") {
42 t.Errorf("line 2 should contain cwd, got %q", lines[1])
43 }
44}
45
46// TestStatusBarNoCWD verifies an empty cwd stays single-line.
47func TestStatusBarNoCWD(t *testing.T) {
48 lines := StatusBar(StatusBarParams{
49 Theme: Dark,
50 Provider: "openai",
51 Model: "gpt-5.4",
52 CWD: "",
53 Cols: 200,
54 })
55 if len(lines) != 1 {
56 t.Fatalf("empty cwd: want 1 line, got %d: %q", len(lines), lines)
57 }
58}
59
60func TestStatusBarShowsFastMode(t *testing.T) {
61 lines := StatusBar(StatusBarParams{
62 Theme: Dark,
63 Provider: "openai-codex",
64 Model: "gpt-5.6-luna",
65 FastMode: true,
66 CWD: "/tmp/x",
67 Cols: 200,
68 })
69 if len(lines) != 2 {
70 t.Fatalf("want 2 lines, got %d: %q", len(lines), lines)
71 }
72 plain := stripANSI(lines[0])
73 if !strings.Contains(plain, "fast mode") {
74 t.Fatalf("fast mode should be visible in status bar: %q", plain)
75 }
76}
77
78func TestStatusBarReasoningLevelBetweenModelAndStats(t *testing.T) {
79 lines := StatusBar(StatusBarParams{
80 Theme: Dark,
81 Provider: "openai-codex",
82 Model: "gpt-5.5",
83 Reasoning: "minimum",
84 CWD: "/tmp/x",
85 Usage: provider.Usage{
86 InputTokens: 4_300_000,
87 OutputTokens: 2,
88 },
89 Cols: 500,
90 })
91 if len(lines) != 2 {
92 t.Fatalf("want 2 lines, got %d: %q", len(lines), lines)
93 }
94 plain := stripANSI(lines[0])
95 modelIdx := strings.Index(plain, "(openai-codex) gpt-5.5")
96 reasoningIdx := strings.Index(plain, "reasoning: minimal")
97 statsIdx := strings.Index(plain, "↑4.3M")
98 if modelIdx < 0 || reasoningIdx < 0 || statsIdx < 0 {
99 t.Fatalf("line should contain model, reasoning level, and stats, got %q", plain)
100 }
101 if modelIdx >= reasoningIdx || reasoningIdx >= statsIdx {
102 t.Fatalf("reasoning level should sit between model and stats, got %q", plain)
103 }
104}
105
106func TestStatusBarUsesReasoningMaxThemeColor(t *testing.T) {
107 th := Dark
108 th.ThinkingMax = Color256(201)
109 lines := StatusBar(StatusBarParams{
110 Theme: th, Provider: "openai", Model: "gpt-5.6-sol", Reasoning: "max", Cols: 200,
111 })
112 if len(lines) != 1 {
113 t.Fatalf("lines = %d, want 1", len(lines))
114 }
115 if !strings.Contains(lines[0], "\x1b[38;5;201m") || !strings.Contains(stripANSI(lines[0]), "reasoning: max") {
116 t.Fatalf("max reasoning style missing: %q", lines[0])
117 }
118}
119
120func TestStatusBarNarrowKeepsModelAndReasoningTogetherWhenTheyFit(t *testing.T) {
121 lines := StatusBar(StatusBarParams{
122 Theme: Dark,
123 Provider: "openai-codex",
124 Model: "gpt-5.5",
125 Reasoning: "xhigh",
126 CWD: "/tmp/x",
127 Usage: provider.Usage{
128 CostUSD: 0,
129 },
130 Subscription: true,
131 ContextUsed: 100,
132 ContextMax: 1_000_000,
133 Cols: 64,
134 })
135 if len(lines) != 3 {
136 t.Fatalf("narrow status with model+thinking fit: want 3 lines, got %d: %q", len(lines), lines)
137 }
138 plain := make([]string, len(lines))
139 for i, line := range lines {
140 plain[i] = stripANSI(line)
141 }
142 if !strings.Contains(plain[0], "(openai-codex) gpt-5.5 reasoning: xhigh") {
143 t.Fatalf("line 1 should contain model and reasoning level, got %q", plain[0])
144 }
145 if !strings.Contains(plain[1], "$0.000 (sub)") || strings.Contains(plain[1], "reasoning level") {
146 t.Fatalf("line 2 should contain only stats, got %q", plain[1])
147 }
148 if !strings.Contains(plain[2], "/tmp/x") {
149 t.Fatalf("line 3 should contain cwd, got %q", plain[2])
150 }
151}
152
153func TestStatusBarNarrowSplitsAfterReasoningLevel(t *testing.T) {
154 lines := StatusBar(StatusBarParams{
155 Theme: Dark,
156 Provider: "openai-codex",
157 Model: "gpt-5.5",
158 Reasoning: "minimum",
159 CWD: "/tmp/x",
160 Usage: provider.Usage{
161 InputTokens: 4_300_000,
162 OutputTokens: 2,
163 },
164 Cols: 40,
165 })
166 if len(lines) != 4 {
167 t.Fatalf("narrow status with reasoning: want 4 lines, got %d: %q", len(lines), lines)
168 }
169 plain := make([]string, len(lines))
170 for i, line := range lines {
171 plain[i] = stripANSI(line)
172 }
173 if !strings.Contains(plain[0], "(openai-codex) gpt-5.5") {
174 t.Fatalf("line 1 should contain model info, got %q", plain[0])
175 }
176 if !strings.Contains(plain[1], "reasoning: minimal") || strings.Contains(plain[1], "↑4.3M") {
177 t.Fatalf("line 2 should contain only reasoning level, got %q", plain[1])
178 }
179 if !strings.Contains(plain[2], "↑4.3M ↓2") {
180 t.Fatalf("line 3 should contain stats, got %q", plain[2])
181 }
182 if !strings.Contains(plain[3], "/tmp/x") {
183 t.Fatalf("line 4 should contain cwd, got %q", plain[3])
184 }
185}
186
187func TestStatusBarVeryNarrowSplitsAfterReasoningLevel(t *testing.T) {
188 lines := StatusBar(StatusBarParams{
189 Theme: Dark,
190 Provider: "openai-codex",
191 Model: "gpt-5.5",
192 Reasoning: "minimum",
193 CWD: "/tmp/x",
194 Usage: provider.Usage{
195 InputTokens: 4_300_000,
196 OutputTokens: 2,
197 },
198 Cols: 32,
199 })
200 if len(lines) != 4 {
201 t.Fatalf("narrow status with reasoning: want 4 lines, got %d: %q", len(lines), lines)
202 }
203 plain := make([]string, len(lines))
204 for i, line := range lines {
205 plain[i] = stripANSI(line)
206 }
207 if !strings.Contains(plain[0], "(openai-codex) gpt-5.5") {
208 t.Fatalf("line 1 should contain model info, got %q", plain[0])
209 }
210 if !strings.Contains(plain[1], "reasoning: minimal") {
211 t.Fatalf("line 2 should contain reasoning level, got %q", plain[1])
212 }
213 if !strings.Contains(plain[2], "↑4.3M ↓2") {
214 t.Fatalf("line 3 should contain stats, got %q", plain[2])
215 }
216 if !strings.Contains(plain[3], "/tmp/x") {
217 t.Fatalf("line 4 should contain cwd, got %q", plain[3])
218 }
219}
220
221func TestStatusBarNoYoloTagPrecedesCWD(t *testing.T) {
222 lines := StatusBar(StatusBarParams{
223 Theme: Dark,
224 Provider: "openai",
225 Model: "gpt-5.5",
226 CWD: "/tmp/x",
227 NoYolo: true,
228 Cols: 200,
229 })
230 if len(lines) != 2 {
231 t.Fatalf("want 2 lines, got %d: %q", len(lines), lines)
232 }
233 plain := stripANSI(lines[1])
234 if !strings.Contains(plain, "yolo mode disabled - /tmp/x") {
235 t.Fatalf("cwd line should include no-yolo tag before cwd, got %q", plain)
236 }
237}