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" {
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}