package tui import ( "strings" "testing" tea "github.com/charmbracelet/bubbletea" "github.com/johannSo/codey/internal/llm" ) // levelAt is where a level sits on the track, so tests can talk about // positions without hard-coding the order. func levelAt(name string) int { for i, l := range llm.EffortLevels { if l == name { return i } } return -1 } // runeIndex is strings.Index counted in cells rather than in bytes. func runeIndex(haystack, needle string) int { i := strings.Index(haystack, needle) if i < 0 { return -1 } return len([]rune(haystack[:i])) } // Bare /effort opens the slider on the level in force. Unset is not "nothing // selected": the API's default is high, and that is where the marker belongs. func TestEffortPickerOpensOnTheLevelInForce(t *testing.T) { for _, tc := range []struct{ effort, want string }{ {"", "high"}, {"low", "low"}, {"max", "max"}, } { m := newTestModel(t) m.opts.Effort = tc.effort m.submit("/effort") if m.phase != phaseEffort { t.Fatalf("effort %q: phase = %v, want the picker open", tc.effort, m.phase) } if got := llm.EffortLevels[m.effortIdx]; got != tc.want { t.Errorf("effort %q: marker on %q, want %q", tc.effort, got, tc.want) } } } // It is a slider, so the ends are ends: moving past one stays put rather than // wrapping around to the opposite extreme. func TestEffortPickerMovesAndClampsAtTheEnds(t *testing.T) { m := newTestModel(t) m.opts.Effort = "high" m.submit("/effort") m.Update(key("right")) if got := m.effortIdx; got != levelAt("xhigh") { t.Errorf("right put the marker on %q", llm.EffortLevels[got]) } m.Update(key("left")) m.Update(key("left")) if got := m.effortIdx; got != levelAt("medium") { t.Errorf("two lefts put the marker on %q", llm.EffortLevels[got]) } for i := 0; i < 5; i++ { m.Update(key("left")) } if got := m.effortIdx; got != 0 { t.Errorf("marker ran off the cheap end to %d", got) } for i := 0; i < 9; i++ { m.Update(key("right")) } if got, want := m.effortIdx, len(llm.EffortLevels)-1; got != want { t.Errorf("marker = %d, want it held at %d", got, want) } } func TestEffortPickerNumbersJumpToALevel(t *testing.T) { m := newTestModel(t) m.submit("/effort") m.Update(key("1")) if got := llm.EffortLevels[m.effortIdx]; got != "low" { t.Errorf("1 selected %q, want low", got) } m.Update(key("5")) if got := llm.EffortLevels[m.effortIdx]; got != "max" { t.Errorf("5 selected %q, want max", got) } // There is no sixth level; the key is not a level, so nothing moves. m.Update(key("6")) if got := llm.EffortLevels[m.effortIdx]; got != "max" { t.Errorf("6 moved the marker to %q", got) } } // Moving the marker changes nothing until enter; enter closes the picker and // the level takes. func TestEffortPickerAppliesOnEnter(t *testing.T) { m := newTestModel(t) m.opts.Effort = "high" m.submit("/effort") m.Update(key("right")) if m.opts.Effort != "high" { t.Errorf("effort = %q — moving the marker must not change anything", m.opts.Effort) } m.Update(key("enter")) if m.opts.Effort != "xhigh" { t.Errorf("effort = %q, want xhigh", m.opts.Effort) } if m.phase != phaseIdle { t.Errorf("phase = %v after applying, want the prompt back", m.phase) } if !strings.Contains(m.tr.render(), "xhigh") { t.Error("the change was not reported in the transcript") } if !strings.Contains(m.activity(), "xhigh") { t.Error("the effort pill did not follow") } } func TestEffortPickerCancelsOnEscape(t *testing.T) { m := newTestModel(t) m.opts.Effort = "low" m.submit("/effort") m.Update(key("right")) m.Update(key("esc")) if m.opts.Effort != "low" { t.Errorf("effort = %q, want the level left alone", m.opts.Effort) } if m.phase != phaseIdle { t.Errorf("phase = %v after cancelling, want the prompt back", m.phase) } } // Typing the level is still the fast path: it applies without a picker to // dismiss. func TestTypedLevelSkipsThePicker(t *testing.T) { m := newTestModel(t) m.submit("/effort max") if m.phase != phaseIdle { t.Errorf("phase = %v, want no picker for a level that was typed out", m.phase) } if m.opts.Effort != "max" { t.Errorf("effort = %q, want max", m.opts.Effort) } } // The marker has to sit over the label it selects — that alignment is the // whole illusion of a slider. func TestSliderMarkerSitsOverTheSelectedLabel(t *testing.T) { for _, width := range []int{60, 80, 120} { for i, level := range llm.EffortLevels { m := newTestModel(t) m.Update(tea.WindowSizeMsg{Width: width, Height: 30}) m.submit("/effort") m.effortIdx = i lines := strings.Split(m.effortView(), "\n") if len(lines) < 4 { t.Fatalf("width %d: the slider is %d lines: %q", width, len(lines), lines) } // Columns are cells, not bytes: the track is drawn in box // characters that are three bytes each. track, labels := lines[1], lines[2] marker := runeIndex(track, "▲") if marker < 0 { t.Fatalf("width %d: no marker on the track %q", width, track) } start := runeIndex(labels, level) if start < 0 { t.Fatalf("width %d: %q missing from the labels %q", width, level, labels) } if marker < start || marker >= start+len(level) { t.Errorf("width %d, %s: marker at column %d, label spans %d-%d\n%s\n%s", width, level, marker, start, start+len(level)-1, track, labels) } } } } func TestSliderShowsEveryLevelAndItsPoles(t *testing.T) { m := newTestModel(t) m.Update(tea.WindowSizeMsg{Width: 80, Height: 30}) m.submit("/effort") view := m.effortView() for _, want := range append([]string{"Faster", "Smarter", "enter", "esc"}, llm.EffortLevels...) { if !strings.Contains(view, want) { t.Errorf("%q missing from the slider:\n%s", want, view) } } } // A terminal too narrow for five labels gets the level and the keys rather // than a track with the words sliced in half. func TestSliderFallsBackOnANarrowTerminal(t *testing.T) { m := newTestModel(t) m.Update(tea.WindowSizeMsg{Width: 30, Height: 20}) m.opts.Effort = "xhigh" m.submit("/effort") view := m.effortView() if !strings.Contains(view, "xhigh") { t.Errorf("the selected level is missing:\n%s", view) } if strings.Contains(view, "medium") { t.Errorf("a label that cannot fit was rendered anyway:\n%s", view) } for _, line := range strings.Split(view, "\n") { if len(line) > 30 { t.Errorf("line is %d cells wide on a 30-cell terminal: %q", len(line), line) } } if got := strings.Count(m.View(), "\n") + 1; got > 20 { t.Errorf("the screen is %d lines on a 20-line terminal", got) } } // While the picker owns the screen there is no prompt to type into, so keys // that are not its own must not leak into the textarea. func TestPickerSwallowsTyping(t *testing.T) { m := newTestModel(t) m.submit("/effort") typeIn(m, "hello") if got := m.input.Value(); got != "" { t.Errorf("input = %q, want the picker to have eaten those keys", got) } if m.phase != phaseEffort { t.Errorf("phase = %v, want the picker still open", m.phase) } }