1package modes
2
3import (
4 "context"
5 "slices"
6 "strings"
7 "testing"
8
9 "github.com/bnema/zut/packages/provider"
10 "github.com/bnema/zut/packages/tui"
11)
12
13func TestReasoningSelectorUsesCurrentModelLevels(t *testing.T) {
14 i := &Interactive{cfg: InteractiveConfig{
15 Provider: "openai",
16 Model: "gpt-5.6-sol",
17 Reasoning: "minimum",
18 }}
19
20 item := i.reasoningSettingItem()
21 var levels []string
22 for _, option := range item.options {
23 levels = append(levels, option.value)
24 }
25 want := []string{"", "low", "medium", "high", "xhigh", "max"}
26 if !slices.Equal(levels, want) {
27 t.Fatalf("reasoning levels = %q, want %q", levels, want)
28 }
29 if item.options[item.choice].value != "low" {
30 t.Fatalf("clamped current level = %q, want low", item.options[item.choice].value)
31 }
32}
33
34func TestReasoningSelectorOnlyOffersOffForUnsupportedModel(t *testing.T) {
35 i := &Interactive{cfg: InteractiveConfig{
36 Provider: "google",
37 Model: "gemini-2.0-flash",
38 Reasoning: "high",
39 }}
40
41 item := i.reasoningSettingItem()
42 if len(item.options) != 1 || item.options[0].value != "" {
43 t.Fatalf("reasoning levels = %#v, want off only", item.options)
44 }
45 if item.hint != "current model does not support reasoning" {
46 t.Fatalf("hint = %q", item.hint)
47 }
48}
49
50func TestReasoningCommandOpensDirectSelector(t *testing.T) {
51 i := &Interactive{
52 cfg: InteractiveConfig{
53 Reasoning: "high",
54 },
55 settingsDialog: newSettingsDialog(),
56 }
57
58 i.runSlash(context.Background(), "/reasoning")
59
60 if !i.settingsDialog.Active() || !i.settingsDialog.selecting || !i.settingsDialog.direct {
61 t.Fatal("reasoning command did not open the direct option selector")
62 }
63 lines := i.settingsDialog.Render(tui.Dark, 100)
64 text := strings.Join(lines, "\n")
65 if !strings.Contains(text, "reasoning level") || !strings.Contains(text, "high") {
66 t.Fatalf("reasoning selector missing current level: %q", text)
67 }
68 i.settingsDialog.HandleKey(tui.Key{Kind: tui.KeyDown})
69 act := i.settingsDialog.HandleKey(tui.Key{Kind: tui.KeyEnter})
70 i.applySettingChange(act)
71 if i.cfg.Reasoning != "xhigh" || i.settingsDialog.Active() {
72 t.Fatalf("reasoning selection was not applied and closed: level=%q active=%v", i.cfg.Reasoning, i.settingsDialog.Active())
73 }
74 if i.statusOK != "reasoning level xhigh" {
75 t.Fatalf("reasoning status = %q", i.statusOK)
76 }
77
78 i.runSlash(context.Background(), "/reasoning")
79 act = i.settingsDialog.HandleKey(tui.Key{Kind: tui.KeyEsc})
80 if !act.Close || i.settingsDialog.Active() {
81 t.Fatal("escape did not close the direct reasoning selector")
82 }
83}
84
85func TestModelDialogAdvertisesReasoningNavigation(t *testing.T) {
86 d := newModelDialog()
87 d.Open("", nil, "high")
88 d.view = []provider.Model{{Provider: "openai-codex", ID: "gpt-5.6-luna", Reasoning: true}}
89
90 text := strings.Join(d.Render(tui.Dark, 100), "\n")
91 plain := stripANSIBytes(text)
92 if !strings.Contains(plain, "reasoning: high") || !strings.Contains(plain, "←/→ or H/L to change") {
93 t.Fatalf("model dialog missing reasoning guidance: %q", plain)
94 }
95}
96
97func TestModelPickerSkipsLlamaRefreshWhenRouterIsNotConfigured(t *testing.T) {
98 i := &Interactive{
99 cfg: InteractiveConfig{
100 RefreshLlamaCPPModels: func(context.Context) error { return nil },
101 },
102 modelRefresh: make(chan modelRefreshResult, 1),
103 modelDialog: newModelDialog(),
104 }
105
106 i.runSlash(context.Background(), "/model")
107
108 if !i.modelDialog.Active() {
109 t.Fatal("model picker did not open immediately")
110 }
111 if i.statusOK == "Refreshing models" {
112 t.Fatal("refresh status shown without llama.cpp router configuration")
113 }
114}