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