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