返回 DeepSeek-Reasonix
status_footer_test.go
根目录 / internal / cli / status_footer_test.go
1 package cli
2
3 import (
4 "strings"
5 "testing"
6
7 "github.com/charmbracelet/colorprofile"
8
9 "github.com/charmbracelet/x/ansi"
10
11 "reasonix/internal/control"
12 "reasonix/internal/event"
13 "reasonix/internal/i18n"
14 "reasonix/internal/provider"
15 )
16
17 func TestTurnReceiptKeepsCompletePerTurnBreakdown(t *testing.T) {
18 defer restoreThemeForTest(activeColorProfile, activeCLITheme)
19 defer i18n.DetectLanguage("en")
20 activeColorProfile = colorprofile.NoTTY
21 configureCLITheme("dark")
22 i18n.DetectLanguage("zh")
23
24 u := &provider.Usage{
25 PromptTokens: 13_625,
26 CompletionTokens: 392,
27 TotalTokens: 14_017,
28 CacheHitTokens: 13_184,
29 CacheMissTokens: 441,
30 ReasoningTokens: 24,
31 }
32 p := &provider.Pricing{CacheHit: .1, Input: 1, Output: 2}
33 got := renderTurnReceipt(u, p, nil)
34 for _, want := range []string{
35 "本轮", "14.0K tok", "in 13.6K", "cached 13.2K", "new 441",
36 "out 392", "reasoning 24", "¥0.0025",
37 } {
38 if !strings.Contains(got, want) {
39 t.Fatalf("turn receipt %q missing %q", got, want)
40 }
41 }
42 if strings.Contains(got, "\033[") {
43 t.Fatalf("NO_COLOR turn receipt contains escapes: %q", got)
44 }
45 }
46
47 func TestTurnReceiptFallsBackToDerivedFreshTokensAndWrapsCleanly(t *testing.T) {
48 defer restoreThemeForTest(activeColorProfile, activeCLITheme)
49 defer i18n.DetectLanguage("en")
50 activeColorProfile = colorprofile.ANSI256
51 configureCLITheme("dark")
52 i18n.DetectLanguage("en")
53
54 got := renderTurnReceipt(&provider.Usage{
55 PromptTokens: 1_200, CompletionTokens: 80, TotalTokens: 1_280, CacheHitTokens: 900,
56 }, nil, &event.CacheDiagnostics{PrefixChanged: true, PrefixChangeReasons: []string{"tools"}})
57 plain := ansi.Strip(got)
58 for _, want := range []string{"TURN", "cached 900", "new 300", "cache prefix changed: tools"} {
59 if !strings.Contains(plain, want) {
60 t.Fatalf("turn receipt %q missing %q", plain, want)
61 }
62 }
63 for i, line := range strings.Split(wrapTranscript(got, 32), "\n") {
64 if width := visibleWidth(line); width > 32 {
65 t.Fatalf("wrapped turn receipt row %d width = %d, want <= 32: %q", i, width, line)
66 }
67 }
68 }
69
70 func TestTurnReceiptIgnoresEmptyUsage(t *testing.T) {
71 if got := renderTurnReceipt(nil, nil, nil); got != "" {
72 t.Fatalf("nil usage receipt = %q, want empty", got)
73 }
74 if got := renderTurnReceipt(&provider.Usage{}, nil, nil); got != "" {
75 t.Fatalf("empty usage receipt = %q, want empty", got)
76 }
77 }
78
79 func TestTurnReceiptMarksEstimatedUsage(t *testing.T) {
80 defer restoreThemeForTest(activeColorProfile, activeCLITheme)
81 defer i18n.DetectLanguage("en")
82 activeColorProfile = colorprofile.NoTTY
83 configureCLITheme("dark")
84 i18n.DetectLanguage("en")
85
86 got := renderTurnReceipt(&provider.Usage{TotalTokens: 1_024, Estimated: true}, nil, nil)
87 for _, want := range []string{"≈1.0K tok", "estimated"} {
88 if !strings.Contains(got, want) {
89 t.Fatalf("estimated turn receipt %q missing %q", got, want)
90 }
91 }
92 }
93
94 func TestTurnReceiptBandUsesSingleQuietBoundary(t *testing.T) {
95 defer restoreThemeForTest(activeColorProfile, activeCLITheme)
96 activeColorProfile = colorprofile.NoTTY
97 configureCLITheme("dark")
98
99 band := renderTurnReceiptBand(" TURN 14.0K tok · in 13.6K", 48)
100 lines := strings.Split(band, "\n")
101 if len(lines) != 2 {
102 t.Fatalf("turn receipt band rows = %d, want top rule and receipt:\n%s", len(lines), band)
103 }
104 if strings.Trim(lines[0], "─ ") != "" {
105 t.Fatalf("turn receipt band boundary is not a rule:\n%s", band)
106 }
107 if got := visibleWidth(lines[0]); got != 48 {
108 t.Fatalf("receipt rule width = %d, want 48: %q", got, lines[0])
109 }
110 if !strings.Contains(lines[1], "TURN 14.0K tok") {
111 t.Fatalf("receipt body missing from quiet band:\n%s", band)
112 }
113 }
114
115 func TestTurnReceiptAdaptsContrastAcrossThemes(t *testing.T) {
116 defer restoreThemeForTest(activeColorProfile, activeCLITheme)
117 defer i18n.DetectLanguage("en")
118 activeColorProfile = colorprofile.ANSI256
119 i18n.DetectLanguage("en")
120
121 for _, tt := range []struct {
122 mode, borderSGR, labelSGR, valueSGR string
123 }{
124 {mode: "dark", borderSGR: "\033[38;5;237m", labelSGR: "\033[38;5;248m", valueSGR: "\033[38;5;251m"},
125 {mode: "light", borderSGR: "\033[38;5;252m", labelSGR: "\033[38;5;241m", valueSGR: "\033[38;5;239m"},
126 } {
127 t.Run(tt.mode, func(t *testing.T) {
128 configureCLITheme(tt.mode)
129 receipt := renderTurnReceipt(&provider.Usage{
130 PromptTokens: 900, CompletionTokens: 100, TotalTokens: 1_000,
131 }, nil, nil)
132 band := renderTurnReceiptBand(receipt, 80)
133 for _, want := range []string{tt.borderSGR + "─", tt.labelSGR + "TURN", tt.valueSGR + "1.0K tok"} {
134 if !strings.Contains(band, want) {
135 t.Fatalf("%s receipt %q missing semantic style %q", tt.mode, band, want)
136 }
137 }
138 if strings.Count(ansi.Strip(band), "\n") != 1 {
139 t.Fatalf("%s receipt should keep one rule and one body row: %q", tt.mode, ansi.Strip(band))
140 }
141 })
142 }
143 }
144
145 func TestStatusFooterSemanticPaletteAcrossThemes(t *testing.T) {
146 t.Setenv("REASONIX_THEME", "")
147 t.Setenv("REASONIX_THEME_STYLE", "")
148 defer restoreThemeForTest(activeColorProfile, activeCLITheme)
149 activeColorProfile = colorprofile.ANSI256
150
151 for _, tt := range []struct {
152 mode, labelSGR, valueSGR, infoSGR, secondarySGR string
153 }{
154 {mode: "dark", labelSGR: "\033[38;5;248m", valueSGR: "\033[38;5;251m", infoSGR: "\033[38;5;80m", secondarySGR: "\033[38;5;141m"},
155 {mode: "light", labelSGR: "\033[38;5;241m", valueSGR: "\033[38;5;239m", infoSGR: "\033[38;5;25m", secondarySGR: "\033[38;5;104m"},
156 } {
157 t.Run(tt.mode, func(t *testing.T) {
158 configureCLITheme(tt.mode)
159 m := newTestChatTUI()
160 m.label = "deepseek-v4-flash"
161 m.effortLevel = "auto"
162 m.runtimeProfile = "full"
163 got := m.statusModelWorkGroup(80)
164 for _, want := range []string{
165 tt.labelSGR + "MODEL",
166 tt.infoSGR + "deepseek-v4-flash",
167 tt.labelSGR + "EFFORT",
168 tt.valueSGR + "auto",
169 tt.labelSGR + "WORK",
170 tt.secondarySGR + "balanced",
171 } {
172 if !strings.Contains(got, want) {
173 t.Fatalf("model/work group %q missing semantic style %q", got, want)
174 }
175 }
176 primary := m.primaryStatusLine(" Auto ", false, false)
177 if !strings.Contains(primary, tt.valueSGR+i18n.M.ChatStatusIdle) ||
178 !strings.Contains(primary, tt.labelSGR+i18n.M.ChatStatusCycleHintCompact) {
179 t.Fatalf("%s interaction hints should use readable semantic contrast: %q", tt.mode, primary)
180 }
181 })
182 }
183 }
184
185 func TestStatusFooterThemesKeepIdenticalGeometry(t *testing.T) {
186 defer restoreThemeForTest(activeColorProfile, activeCLITheme)
187
188 m := newTestChatTUI()
189 m.ctrl = control.New(control.Options{})
190 m.label = "deepseek-v4-flash"
191 m.effortLevel = "max"
192 m.runtimeProfile = "full"
193 m.balance = "¥12.34"
194 m.gitStatus = gitStatus{Repo: "DeepSeek-Reasonix", Branch: "feature/theme-footer", Added: 3}
195
196 render := func(mode string, profile colorprofile.Profile) string {
197 activeColorProfile = profile
198 configureCLITheme(mode)
199 primary := m.primaryStatusLine(" Auto ", false, false)
200 return ansi.Strip(m.renderStatusBlock(primary, 132))
201 }
202 dark := render("dark", colorprofile.ANSI256)
203 light := render("light", colorprofile.ANSI256)
204 plain := render("dark", colorprofile.NoTTY)
205 if dark != light || dark != plain {
206 t.Fatalf("theme modes changed footer geometry:\ndark:\n%s\nlight:\n%s\nplain:\n%s", dark, light, plain)
207 }
208 }
209
210 func TestStatusFooterGitAndDividerAdaptToTheme(t *testing.T) {
211 defer restoreThemeForTest(activeColorProfile, activeCLITheme)
212 activeColorProfile = colorprofile.ANSI256
213
214 for _, tt := range []struct {
215 mode, gitSGR, borderSGR string
216 }{
217 {mode: "dark", gitSGR: "\033[38;5;179m", borderSGR: "\033[38;5;237m"},
218 {mode: "light", gitSGR: "\033[38;5;136m", borderSGR: "\033[38;5;252m"},
219 } {
220 t.Run(tt.mode, func(t *testing.T) {
221 configureCLITheme(tt.mode)
222 m := newTestChatTUI()
223 m.gitStatus = gitStatus{Repo: "DeepSeek-Reasonix", Branch: "db4be5e6", Detached: true}
224 git := m.layoutGitTelemetry(80)
225 if !strings.Contains(git, tt.gitSGR+"DeepSeek-Reasonix") {
226 t.Fatalf("%s Git identity should use warm semantic colour: %q", tt.mode, git)
227 }
228 divider := statusFooterDivider(40)
229 if !strings.Contains(divider, tt.borderSGR) || visibleWidth(divider) != 40 {
230 t.Fatalf("%s divider should use border token at full width: %q", tt.mode, divider)
231 }
232 })
233 }
234 }
235
236 func TestContextFooterColorsOnlyValuesByUrgency(t *testing.T) {
237 defer restoreThemeForTest(activeColorProfile, activeCLITheme)
238 activeColorProfile = colorprofile.ANSI256
239 configureCLITheme("dark")
240
241 normal := strings.Join(renderContextStatusGroups(10, 100, .8), " ")
242 if !strings.Contains(normal, "\033[38;5;248mCTX") || !strings.Contains(normal, "\033[38;5;251m10 (10%)") {
243 t.Fatalf("normal context should use subtle label and neutral value: %q", normal)
244 }
245
246 warning := strings.Join(renderContextStatusGroups(75, 100, .8), " ")
247 if !strings.Contains(warning, "\033[38;5;248mCOMPACT") || !strings.Contains(warning, "\033[38;5;179m5%") {
248 t.Fatalf("near-threshold context should warn only on values: %q", warning)
249 }
250
251 critical := strings.Join(renderContextStatusGroups(80, 100, .8), " ")
252 if !strings.Contains(critical, "\033[38;5;179m80 (80%)") || !strings.Contains(critical, "\033[38;5;167m0%") {
253 t.Fatalf("critical context should keep warning/danger hierarchy: %q", critical)
254 }
255 }
256
257 func TestStatusFooterNoColorKeepsSemanticLabels(t *testing.T) {
258 defer restoreThemeForTest(activeColorProfile, activeCLITheme)
259 activeColorProfile = colorprofile.NoTTY
260 configureCLITheme("dark")
261
262 m := newTestChatTUI()
263 m.label = "deepseek-v4-flash"
264 m.effortLevel = "auto"
265 m.runtimeProfile = "full"
266 m.balance = "¥12.34"
267 block := m.renderStatusBlock(m.primaryStatusLine(" Auto ", false, false), 120)
268 if strings.Contains(block, "\033[") {
269 t.Fatalf("NO_COLOR footer contains escapes: %q", block)
270 }
271 for _, want := range []string{"MODEL deepseek-v4-flash", "EFFORT auto", "WORK balanced", "BAL ¥12.34"} {
272 if !strings.Contains(block, want) {
273 t.Fatalf("NO_COLOR footer missing %q:\n%s", want, block)
274 }
275 }
276 }
277
278 func TestStatusFooterUsesReadableLocalizedHintAndWrapsCleanly(t *testing.T) {
279 defer i18n.DetectLanguage("en")
280 for _, tt := range []struct {
281 lang, compact, session string
282 }{
283 {lang: "en", compact: "Shift+Tab ask/auto/plan · Ctrl+Y YOLO", session: "MODEL deepseek-v4-flash EFFORT auto WORK balanced"},
284 {lang: "zh", compact: "Shift+Tab 询问/自动/计划 · Ctrl+Y YOLO", session: "模型 deepseek-v4-flash 强度 auto 模式 均衡"},
285 {lang: "zh-TW", compact: "Shift+Tab 詢問/自動/計畫 · Ctrl+Y YOLO", session: "模型 deepseek-v4-flash 強度 auto 模式 均衡"},
286 } {
287 t.Run(tt.lang, func(t *testing.T) {
288 i18n.DetectLanguage(tt.lang)
289 m := newTestChatTUI()
290 m.ctrl = control.New(control.Options{})
291 m.label = "deepseek-v4-flash"
292 m.runtimeProfile = "full"
293 m.effortLevel = "auto"
294
295 primary := m.primaryStatusLine(" Auto ", false, false)
296 block := ansi.Strip(m.renderStatusBlock(primary, 100))
297 lines := strings.Split(block, "\n")
298 if len(lines) != 2 {
299 t.Fatalf("localized footer rows = %d, want wrapped primary/session rows without an empty data band:\n%s", len(lines), block)
300 }
301 if !strings.Contains(lines[0], tt.compact) || !strings.Contains(lines[1], tt.session) {
302 t.Fatalf("localized footer did not keep readable shortcut and session groups:\n%s", block)
303 }
304 if strings.Contains(block, "⇧Tab") || strings.Contains(block, "^Y") {
305 t.Fatalf("localized footer fell back to symbolic shortcut notation:\n%s", block)
306 }
307 for row, line := range lines {
308 if width := visibleWidth(line); width > 100 {
309 t.Fatalf("localized footer row %d width = %d, want <= 100: %q", row, width, line)
310 }
311 }
312
313 narrow := ansi.Strip(m.renderStatusBlock(primary, 24))
314 if strings.Contains(narrow, "Shift+Tab") || strings.Contains(narrow, "Ctrl+Y") {
315 t.Fatalf("shortcut help should yield when readable key names cannot fit:\n%s", narrow)
316 }
317 if !strings.Contains(narrow, ansi.Strip(footerValue(i18n.M.ChatStatusIdle))) {
318 t.Fatalf("narrow footer should preserve the idle state:\n%s", narrow)
319 }
320 })
321 }
322 }
323
324 func TestStatusFooterLocalizesMetricLabelsAndKeepsNarrowRows(t *testing.T) {
325 defer i18n.DetectLanguage("en")
326 for _, tt := range []struct {
327 lang string
328 session string
329 telemetry []string
330 }{
331 {
332 lang: "zh",
333 session: "模型 deepseek-v4-flash 强度 auto 模式 均衡",
334 telemetry: []string{"缓存", "上下文", "压缩", "任务", "余额"},
335 },
336 {
337 lang: "zh-TW",
338 session: "模型 deepseek-v4-flash 強度 auto 模式 均衡",
339 telemetry: []string{"快取", "上下文", "壓縮", "任務", "餘額"},
340 },
341 } {
342 t.Run(tt.lang, func(t *testing.T) {
343 i18n.DetectLanguage(tt.lang)
344 m := newTestChatTUI()
345 m.label = "deepseek-v4-flash"
346 m.effortLevel = "auto"
347 m.runtimeProfile = "full"
348 if got := ansi.Strip(m.statusModelWorkGroup(80)); got != tt.session {
349 t.Fatalf("localized session metrics = %q, want %q", got, tt.session)
350 }
351
352 groups := []string{
353 footerMetric(i18n.M.ChatStatusCacheLabel, footerValue("90%")),
354 }
355 groups = append(groups, renderContextStatusGroups(75, 100, .8)...)
356 groups = append(groups,
357 footerMetric(i18n.M.ChatStatusJobsLabel, footerInfo("2")),
358 footerMetric(i18n.M.ChatStatusBalanceLabel, footerValue("¥12.34")),
359 )
360 packed := ansi.Strip(packStatusGroups(groups, 22))
361 for _, label := range tt.telemetry {
362 if !strings.Contains(packed, label+" ") {
363 t.Fatalf("localized telemetry missing %q:\n%s", label, packed)
364 }
365 }
366 for row, line := range strings.Split(packed, "\n") {
367 if width := visibleWidth(line); width > 22 {
368 t.Fatalf("localized telemetry row %d width = %d, want <= 22: %q", row, width, line)
369 }
370 }
371 })
372 }
373 }
374
375 func TestStatusFooterSwapsModelAndGitGroups(t *testing.T) {
376 i18n.DetectLanguage("en")
377
378 m := newTestChatTUI()
379 m.ctrl = control.New(control.Options{})
380 m.label = "deepseek-v4-flash"
381 m.runtimeProfile = "full"
382 m.effortLevel = "auto"
383 m.balance = "¥12.34"
384 m.gitStatus = gitStatus{
385 Repo: "DeepSeek-Reasonix",
386 Branch: "feature/responsive-footer",
387 Added: 1199,
388 Removed: 244,
389 Untracked: 3,
390 }
391
392 primary := m.primaryStatusLine(" Auto ", false, false)
393 lines := strings.Split(ansi.Strip(m.renderStatusBlock(primary, 160)), "\n")
394 if len(lines) != 3 {
395 t.Fatalf("wide status block lines = %d, want two data rows plus divider:\n%s", len(lines), strings.Join(lines, "\n"))
396 }
397 if !strings.Contains(lines[0], "MODEL deepseek-v4-flash EFFORT auto WORK balanced") {
398 t.Fatalf("first row should keep model, effort, and work in one session group:\n%s", strings.Join(lines, "\n"))
399 }
400 if strings.Contains(lines[0], "DeepSeek-Reasonix@") {
401 t.Fatalf("first row should not contain Git identity:\n%s", strings.Join(lines, "\n"))
402 }
403 if strings.Trim(lines[1], "─ ") != "" {
404 t.Fatalf("middle row should be a divider:\n%s", strings.Join(lines, "\n"))
405 }
406 if !strings.Contains(lines[2], "DeepSeek-Reasonix@feature/responsive-footer") || strings.Contains(lines[2], "…") {
407 t.Fatalf("second row should preserve the full Git identity when it fits:\n%s", strings.Join(lines, "\n"))
408 }
409 if !strings.Contains(lines[2], "+1199 -244 ?3") || !strings.HasSuffix(lines[2], "BAL ¥12.34") {
410 t.Fatalf("second row should preserve Git changes and right-anchor telemetry:\n%s", strings.Join(lines, "\n"))
411 }
412 for i, line := range lines {
413 if got := visibleWidth(line); got > 160 {
414 t.Fatalf("row %d width = %d, want <= 160: %q", i, got, line)
415 }
416 }
417 }
418
419 func TestStatusFooterWithoutGitLeftAlignsTelemetry(t *testing.T) {
420 defer i18n.DetectLanguage("en")
421 i18n.DetectLanguage("en")
422
423 m := newTestChatTUI()
424 m.balance = "¥12.34"
425 line := ansi.Strip(m.layoutGitTelemetry(120))
426 if !strings.HasPrefix(line, statusFooterIndent+"BAL ¥12.34") {
427 t.Fatalf("non-Git telemetry should be left aligned, got %q", line)
428 }
429 if visibleWidth(line) >= 120 {
430 t.Fatalf("non-Git telemetry unexpectedly retained right-alignment padding: %q", line)
431 }
432 }
433
434 func TestStatusFooterOmitsEmptyDataBand(t *testing.T) {
435 m := newTestChatTUI()
436 primary := " Auto · ready"
437 block := ansi.Strip(m.renderStatusBlock(primary, 120))
438 if block != primary {
439 t.Fatalf("empty Git/telemetry status block = %q, want only %q", block, primary)
440 }
441 if strings.Contains(block, "─") {
442 t.Fatalf("empty Git/telemetry status block retained a divider: %q", block)
443 }
444 }
445
446 func TestStatusFooterMediumLayoutLeftAlignsModelWork(t *testing.T) {
447 i18n.DetectLanguage("en")
448
449 m := newTestChatTUI()
450 m.ctrl = control.New(control.Options{})
451 m.label = "deepseek-v4-flash"
452 m.runtimeProfile = "full"
453 m.effortLevel = "auto"
454
455 primary := m.primaryStatusLine(" Auto ", false, false)
456 lines := strings.Split(ansi.Strip(m.renderStatusBlock(primary, 82)), "\n")
457 if len(lines) != 2 {
458 t.Fatalf("medium footer rows = %d, want primary plus model/work without an empty data band:\n%s", len(lines), strings.Join(lines, "\n"))
459 }
460 modelRow := lines[1]
461 if !strings.HasPrefix(modelRow, statusFooterIndent+"MODEL deepseek-v4-flash") ||
462 !strings.Contains(modelRow, "EFFORT auto WORK balanced") {
463 t.Fatalf("medium model/effort/work row should be left aligned, got %q:\n%s", modelRow, strings.Join(lines, "\n"))
464 }
465 if strings.Count(strings.TrimLeft(modelRow, " "), "MODEL") != 1 {
466 t.Fatalf("medium model/work row should remain a single semantic group: %q", modelRow)
467 }
468 }
469
470 func TestStatusFooterStacksGitAndTelemetryWithoutFloatingContinuation(t *testing.T) {
471 i18n.DetectLanguage("en")
472
473 m := newTestChatTUI()
474 m.gitStatus = gitStatus{
475 Repo: "DeepSeek-Reasonix", Branch: "feature/responsive-footer", Added: 20, Removed: 4,
476 }
477 m.balance = "¥123.45"
478
479 lines := strings.Split(ansi.Strip(m.layoutGitTelemetry(56)), "\n")
480 if len(lines) != 2 {
481 t.Fatalf("stacked Git/telemetry rows = %d, want 2:\n%s", len(lines), strings.Join(lines, "\n"))
482 }
483 if !strings.HasPrefix(lines[0], statusFooterIndent+"DeepSeek-Reasonix@") || !strings.Contains(lines[0], "+20 -4") {
484 t.Fatalf("Git should own the complete first row:\n%s", strings.Join(lines, "\n"))
485 }
486 if !strings.HasPrefix(lines[1], statusFooterIndent+"BAL ¥123.45") {
487 t.Fatalf("stacked telemetry should be left aligned, got %q", lines[1])
488 }
489 }
490
491 func TestStatusFooterNarrowLayoutBreaksBetweenGroups(t *testing.T) {
492 i18n.DetectLanguage("en")
493
494 m := newTestChatTUI()
495 m.ctrl = control.New(control.Options{})
496 m.label = "provider/" + strings.Repeat("long-model-", 8)
497 m.runtimeProfile = "delivery"
498 m.balance = "¥123.45"
499 m.gitStatus = gitStatus{
500 Repo: "DeepSeek-Reasonix-Workspace",
501 Branch: "feature/" + strings.Repeat("long-branch-", 8),
502 Added: 20,
503 Removed: 4,
504 }
505
506 primary := m.primaryStatusLine(" Auto ", false, false)
507 block := ansi.Strip(m.renderStatusBlock(primary, 40))
508 lines := strings.Split(block, "\n")
509 if len(lines) <= 2 {
510 t.Fatalf("narrow status block lines = %d, want semantic wrapping:\n%s", len(lines), block)
511 }
512 for i, line := range lines {
513 if got := visibleWidth(line); got > 40 {
514 t.Fatalf("row %d width = %d, want <= 40: %q", i, got, line)
515 }
516 }
517 if !strings.Contains(block, "@") || !strings.Contains(block, "+20 -4") || !strings.Contains(block, "¥123.45") {
518 t.Fatalf("narrow layout dropped required information:\n%s", block)
519 }
520 }
521
522 func TestStatusFooterCustomLineStillReplacesBuiltInData(t *testing.T) {
523 i18n.DetectLanguage("en")
524
525 m := newTestChatTUI()
526 m.ctrl = control.New(control.Options{})
527 m.label = "deepseek-v4-flash"
528 m.runtimeProfile = "delivery"
529 m.balance = "¥12.34"
530 m.statuslineCmd = "custom-status"
531 m.statuslineOut = "custom telemetry"
532 m.gitStatus = gitStatus{Repo: "Reasonix", Branch: "main"}
533
534 primary := m.primaryStatusLine(" Auto ", false, false)
535 block := ansi.Strip(m.renderStatusBlock(primary, 120))
536 if strings.Contains(block, "deepseek-v4-flash") || strings.Contains(block, "work delivery") || strings.Contains(block, "¥12.34") {
537 t.Fatalf("custom statusline should replace built-in data fields:\n%s", block)
538 }
539 if !strings.Contains(block, "Reasonix@main") || !strings.Contains(block, "custom telemetry") {
540 t.Fatalf("custom statusline should coexist with Git identity:\n%s", block)
541 }
542 }
543
544 func TestStatusFooterHeightCountUsesRenderedLayout(t *testing.T) {
545 i18n.DetectLanguage("en")
546
547 m := newTestChatTUI()
548 m.ctrl = control.New(control.Options{})
549 m.width = 34
550 m.label = "provider/" + strings.Repeat("long-model-", 6)
551 m.runtimeProfile = "delivery"
552 m.gitStatus = gitStatus{Repo: "VeryLongWorkspaceName", Branch: strings.Repeat("branch/", 8)}
553 m.balance = "¥12.34"
554
555 modeTag := " " + m.modeTagText() + " "
556 primary := m.primaryStatusLine(modeTag, false, false)
557 want := strings.Count(m.renderStatusBlock(primary, m.width), "\n") + 1
558 if got := m.computeStatusLineCount(m.width); got != want {
559 t.Fatalf("computed status rows = %d, rendered rows = %d", got, want)
560 }
561 }
562
562 lines GO