Vectrum
Sign In
245 lines · 6.9 KB · Go
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)
	}
}