返回 DeepSeek-Reasonix
chat_tui_working_line_test.go
根目录 / internal / cli / chat_tui_working_line_test.go
1 package cli
2
3 import (
4 "strings"
5 "testing"
6
7 tea "charm.land/bubbletea/v2"
8
9 "reasonix/internal/control"
10 "reasonix/internal/event"
11 )
12
13 // TestWorkingLineRowsWrapAccounting pins the pure wrap-count helper shared by
14 // the composer cursor offset (rowsAboveBox) and the bottomRows height budget:
15 // the working (spinner) line above the input box occupies more than one
16 // terminal row on narrow terminals, and the helper must report the wrapped
17 // count, not 1.
18 func TestWorkingLineRowsWrapAccounting(t *testing.T) {
19 cases := []struct {
20 name string
21 text string
22 width int
23 want int
24 }{
25 {name: "hidden line", text: "", width: 30, want: 0},
26 {name: "no width", text: "Thinking…", width: 0, want: 0},
27 {name: "fits one row", text: " Thinking… 5s", width: 30, want: 1},
28 {name: "exact fit stays one row", text: strings.Repeat("x", 30), width: 30, want: 1},
29 {name: "wraps to two rows", text: strings.Repeat("x", 60), width: 30, want: 2},
30 {name: "wraps to three rows", text: strings.Repeat("x", 61), width: 30, want: 3},
31 }
32 for _, tc := range cases {
33 t.Run(tc.name, func(t *testing.T) {
34 if got := workingLineRows(tc.text, tc.width); got != tc.want {
35 t.Fatalf("workingLineRows(%q, %d) = %d, want %d", tc.text, tc.width, got, tc.want)
36 }
37 })
38 }
39 }
40
41 // TestWorkingLineWrapKeepsComposerCursorInBox is the #7537 regression test: on
42 // a narrow terminal the working (spinner) line above the composer wraps to
43 // multiple terminal rows, and View() must still anchor the real terminal
44 // cursor inside the composer box instead of drifting above it.
45 func TestWorkingLineWrapKeepsComposerCursorInBox(t *testing.T) {
46 ctrl := control.New(control.Options{})
47 m := newChatTUI(ctrl, "", make(chan event.Event, 1), 24)
48
49 m0, _ := m.Update(tea.WindowSizeMsg{Width: 24, Height: 12})
50 m = m0.(chatTUI)
51 m.state = tuiRunning
52 m.elapsed = 1000000
53 m.turnTokens = 9999999
54
55 // Precondition: the working line really wraps at this width, otherwise this
56 // test would not exercise the bug.
57 working := m.runningWorkingLine(false, false)
58 wrappedRows := workingLineRows(working, m.width)
59 if wrappedRows <= 1 {
60 t.Fatalf("test setup: working line %q at width %d must wrap, got %d row(s)", working, m.width, wrappedRows)
61 }
62
63 v := m.View()
64 if v.Cursor == nil {
65 t.Fatal("composer cursor should be set while the composer is visible")
66 }
67 // The composer box starts below the viewport and the wrapped working line;
68 // its top border sits one row above the textarea rows and the bottom border
69 // one row below them.
70 boxTop := m.viewport.Height() + wrappedRows
71 boxBottom := boxTop + m.input.Height() + 2
72 if v.Cursor.Y < boxTop+1 || v.Cursor.Y >= boxBottom {
73 t.Fatalf("composer cursor Y=%d outside composer box rows [%d,%d): viewport=%d wrappedWorkingRows=%d inputHeight=%d",
74 v.Cursor.Y, boxTop+1, boxBottom, m.viewport.Height(), wrappedRows, m.input.Height())
75 }
76 }
77
77 lines GO