1package modes
2
3import (
4 "context"
5 "slices"
6 "strings"
7 "testing"
8
9 "github.com/patriceckhart/zot/packages/core"
10 "github.com/patriceckhart/zot/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 TestReasoningSettingNotifiesRuntime(t *testing.T) {
86 i := &Interactive{agent: &core.Agent{Reasoning: "high"}}
87 var levels []string
88 i.cfg.OnReasoningChanged = func(level string) {
89 if i.agent.Reasoning != level {
90 t.Fatalf("callback ran before agent update: got %q, want %q", i.agent.Reasoning, level)
91 }
92 levels = append(levels, level)
93 }
94 i.applyReasoningSetting("medium")
95 i.applyReasoningSetting("off")
96 if !slices.Equal(levels, []string{"medium", ""}) {
97 t.Fatalf("reasoning notifications = %q", levels)
98 }
99}
100
101func TestNormalizeModelQueryKeepsPresetAtSign(t *testing.T) {
102 if got := normalizeModelQuery("@preset/flash"); got != "@presetflash" {
103 t.Fatalf("id = %q; want @presetflash", got)
104 }
105 if got := normalizeModelQuery("preset"); got != "preset" {
106 t.Fatalf("query = %q; want preset", got)
107 }
108 haystack := normalizeModelQuery("openrouter @preset/flash Flash (preset)")
109 if !strings.Contains(haystack, normalizeModelQuery("preset")) {
110 t.Fatalf("%q does not contain preset", haystack)
111 }
112}
113
114func TestModelDialogAdvertisesReasoningSelector(t *testing.T) {
115 d := newModelDialog()
116 d.Open("", nil, "high")
117
118 text := strings.Join(d.Render(tui.Dark, 100), "\n")
119 if !strings.Contains(text, "current reasoning: high") || !strings.Contains(text, "/reasoning") {
120 t.Fatalf("model dialog missing reasoning guidance: %q", text)
121 }
122}
123
124func TestModelPickerSkipsLlamaRefreshWhenRouterIsNotConfigured(t *testing.T) {
125 i := &Interactive{
126 cfg: InteractiveConfig{
127 RefreshLlamaCPPModels: func(context.Context) error { return nil },
128 },
129 modelRefresh: make(chan modelRefreshResult, 1),
130 modelDialog: newModelDialog(),
131 }
132
133 i.runSlash(context.Background(), "/model")
134
135 if !i.modelDialog.Active() {
136 t.Fatal("model picker did not open immediately")
137 }
138 if i.statusOK == "refreshing models" {
139 t.Fatal("refresh status shown without llama.cpp router configuration")
140 }
141}