v0.44.0 efd47b3

interactive_model_test.go RawHistoryFolder

1package modes
2
3import (
4	"context"
5	"slices"
6	"strings"
7	"testing"
8
9	"github.com/bnema/zut/packages/core"
10	"github.com/bnema/zut/packages/provider"
11	"github.com/bnema/zut/packages/tui"
12)
13
14func TestReasoningSelectorUsesCurrentModelLevels(t *testing.T) {
15	i := &Interactive{cfg: InteractiveConfig{
16		Provider:  "openai",
17		Model:     "gpt-5.6-sol",
18		Reasoning: "minimum",
19	}}
20
21	item := i.reasoningSettingItem()
22	var levels []string
23	for _, option := range item.options {
24		levels = append(levels, option.value)
25	}
26	want := []string{"", "low", "medium", "high", "xhigh", "max"}
27	if !slices.Equal(levels, want) {
28		t.Fatalf("reasoning levels = %q, want %q", levels, want)
29	}
30	if item.options[item.choice].value != "low" {
31		t.Fatalf("clamped current level = %q, want low", item.options[item.choice].value)
32	}
33}
34
35func TestReasoningSelectorOnlyOffersOffForUnsupportedModel(t *testing.T) {
36	i := &Interactive{cfg: InteractiveConfig{
37		Provider:  "google",
38		Model:     "gemini-2.0-flash",
39		Reasoning: "high",
40	}}
41
42	item := i.reasoningSettingItem()
43	if len(item.options) != 1 || item.options[0].value != "" {
44		t.Fatalf("reasoning levels = %#v, want off only", item.options)
45	}
46	if item.hint != "current model does not support reasoning" {
47		t.Fatalf("hint = %q", item.hint)
48	}
49}
50
51func TestCtrlRCyclesCurrentModelReasoningLevels(t *testing.T) {
52	var changed []string
53	i := NewInteractive(InteractiveConfig{
54		Theme:     tui.Dark,
55		Provider:  "openai",
56		Model:     "gpt-5.6-sol",
57		Reasoning: "high",
58		OnReasoningChanged: func(level string) {
59			changed = append(changed, level)
60		},
61	})
62	i.agent = &core.Agent{Reasoning: "high"}
63	i.rend = nil
64
65	for _, want := range []string{"xhigh", "max", ""} {
66		if done := i.handleKey(context.Background(), tui.Key{Kind: tui.KeyCtrlR}); done {
67			t.Fatal("ctrl+r exited the interactive session")
68		}
69		if i.cfg.Reasoning != want || i.agent.Reasoning != want {
70			t.Fatalf("reasoning = cfg %q agent %q, want %q", i.cfg.Reasoning, i.agent.Reasoning, want)
71		}
72	}
73	if !slices.Equal(changed, []string{"xhigh", "max", ""}) {
74		t.Fatalf("reasoning callbacks = %q", changed)
75	}
76	if i.statusOK != "reasoning level off — profile 1 updated" {
77		t.Fatalf("reasoning status = %q", i.statusOK)
78	}
79}
80
81func TestCtrlRReportsUnsupportedCurrentModel(t *testing.T) {
82	i := NewInteractive(InteractiveConfig{
83		Theme:     tui.Dark,
84		Provider:  "google",
85		Model:     "gemini-2.0-flash",
86		Reasoning: "high",
87	})
88	i.rend = nil
89
90	i.handleKey(context.Background(), tui.Key{Kind: tui.KeyCtrlR})
91
92	if i.cfg.Reasoning != "high" {
93		t.Fatalf("reasoning changed to %q for unsupported model", i.cfg.Reasoning)
94	}
95	if i.statusOK != "current model does not support reasoning" {
96		t.Fatalf("reasoning status = %q", i.statusOK)
97	}
98}
99
100func TestReasoningCommandOpensDirectSelector(t *testing.T) {
101	i := &Interactive{
102		cfg: InteractiveConfig{
103			Reasoning: "high",
104		},
105		settingsDialog: newSettingsDialog(),
106	}
107
108	i.runSlash(context.Background(), "/reasoning")
109
110	if !i.settingsDialog.Active() || !i.settingsDialog.selecting || !i.settingsDialog.direct {
111		t.Fatal("reasoning command did not open the direct option selector")
112	}
113	lines := i.settingsDialog.Render(tui.Dark, 100)
114	text := strings.Join(lines, "\n")
115	if !strings.Contains(text, "reasoning level") || !strings.Contains(text, "high") {
116		t.Fatalf("reasoning selector missing current level: %q", text)
117	}
118	i.settingsDialog.HandleKey(tui.Key{Kind: tui.KeyDown})
119	act := i.settingsDialog.HandleKey(tui.Key{Kind: tui.KeyEnter})
120	i.applySettingChange(act)
121	if i.cfg.Reasoning != "xhigh" || i.settingsDialog.Active() {
122		t.Fatalf("reasoning selection was not applied and closed: level=%q active=%v", i.cfg.Reasoning, i.settingsDialog.Active())
123	}
124	if i.statusOK != "reasoning level xhigh" {
125		t.Fatalf("reasoning status = %q", i.statusOK)
126	}
127
128	i.runSlash(context.Background(), "/reasoning")
129	act = i.settingsDialog.HandleKey(tui.Key{Kind: tui.KeyEsc})
130	if !act.Close || i.settingsDialog.Active() {
131		t.Fatal("escape did not close the direct reasoning selector")
132	}
133}
134
135func TestModelDialogAdvertisesReasoningNavigation(t *testing.T) {
136	d := newModelDialog()
137	d.Open("", nil, "high")
138	d.view = []provider.Model{{Provider: "openai-codex", ID: "gpt-5.6-luna", Reasoning: true}}
139
140	text := strings.Join(d.Render(tui.Dark, 100), "\n")
141	plain := stripANSIBytes(text)
142	if !strings.Contains(plain, "reasoning: high") || !strings.Contains(plain, "←/→ or H/L to change") {
143		t.Fatalf("model dialog missing reasoning guidance: %q", plain)
144	}
145}
146
147func TestModelPickerSkipsLlamaRefreshWhenRouterIsNotConfigured(t *testing.T) {
148	i := &Interactive{
149		cfg: InteractiveConfig{
150			RefreshLlamaCPPModels: func(context.Context) error { return nil },
151		},
152		modelRefresh: make(chan modelRefreshResult, 1),
153		modelDialog:  newModelDialog(),
154	}
155
156	i.runSlash(context.Background(), "/model")
157
158	if !i.modelDialog.Active() {
159		t.Fatal("model picker did not open immediately")
160	}
161	if i.statusOK == "Refreshing models" {
162		t.Fatal("refresh status shown without llama.cpp router configuration")
163	}
164}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 476

↑↓ moveenter openesc close