v0.48.1 deaab59

model_profile_restoration_test.go RawHistoryFolder

1package agent
2
3import (
4	"testing"
5
6	"github.com/bnema/zut/packages/agent/modes"
7	"github.com/bnema/zut/packages/core"
8	"github.com/bnema/zut/packages/tui"
9)
10
11func TestResolveRepairsActiveModelProfileFallback(t *testing.T) {
12	for _, tc := range []struct {
13		name    string
14		active  int
15		profile QuickModelShortcut
16	}{
17		{"missing model", 2, QuickModelShortcut{Provider: "openai", Model: "removed-model", Reasoning: "high"}},
18		{"missing provider default slot", 0, QuickModelShortcut{Provider: "removed-provider", Model: "removed-model", Reasoning: "high"}},
19	} {
20		t.Run(tc.name, func(t *testing.T) {
21			t.Setenv("ZUT_HOME", t.TempDir())
22			slot := tc.active
23			if slot == 0 {
24				slot = 1
25			}
26			profiles := make([]QuickModelShortcut, slot)
27			profiles[slot-1] = tc.profile
28			if err := SaveConfig(Config{ActiveModelProfile: tc.active, QuickModelShortcuts: profiles}); err != nil {
29				t.Fatal(err)
30			}
31			args := Args{APIKey: "synthetic-test-key"}
32			resolved, err := Resolve(args, false)
33			if err != nil {
34				t.Fatal(err)
35			}
36			saved, err := LoadConfig()
37			if err != nil {
38				t.Fatal(err)
39			}
40			p := saved.QuickModelShortcuts[slot-1]
41			if p.Provider != resolved.Provider || p.Model != resolved.Model || p.Reasoning != resolved.Reasoning {
42				t.Fatalf("fallback left stale profile: %+v; resolved %s/%s (%s)", p, resolved.Provider, resolved.Model, resolved.Reasoning)
43			}
44			again, err := Resolve(args, false)
45			if err != nil {
46				t.Fatal(err)
47			}
48			if again.Provider != resolved.Provider || again.Model != resolved.Model || again.Reasoning != resolved.Reasoning {
49				t.Fatal("repaired selection changed on next resolution")
50			}
51		})
52	}
53}
54
55func TestResolveExplicitFallbackDoesNotRepairFavorite(t *testing.T) {
56	for _, args := range []Args{{Model: "removed-model"}, {Provider: "removed-provider"}} {
57		t.Setenv("ZUT_HOME", t.TempDir())
58		original := QuickModelShortcut{Provider: "openai", Model: "gpt-5.6-sol", Reasoning: "high"}
59		if err := SaveConfig(Config{ActiveModelProfile: 1, QuickModelShortcuts: []QuickModelShortcut{original}}); err != nil {
60			t.Fatal(err)
61		}
62		args.APIKey = "synthetic-test-key"
63		if _, err := Resolve(args, false); err != nil {
64			t.Fatal(err)
65		}
66		saved, err := LoadConfig()
67		if err != nil {
68			t.Fatal(err)
69		}
70		if saved.QuickModelShortcuts[0] != original {
71			t.Fatalf("explicit fallback overwrote favorite: %+v", saved.QuickModelShortcuts[0])
72		}
73	}
74}
75
76func TestModelProfileStartupClampsReasoningAndPreservesAutosave(t *testing.T) {
77	for _, tc := range []struct {
78		profile QuickModelShortcut
79		want    string
80	}{
81		{QuickModelShortcut{Provider: "openai", Model: "gpt-5.6-sol", Reasoning: "minimum"}, "low"},
82		{QuickModelShortcut{Provider: "google", Model: "gemini-2.0-flash", Reasoning: "high"}, ""},
83	} {
84		t.Run(tc.profile.Model, func(t *testing.T) {
85			t.Setenv("ZUT_HOME", t.TempDir())
86			cfg := Config{ActiveModelProfile: 1, QuickModelShortcuts: []QuickModelShortcut{tc.profile}}
87			if err := SaveConfig(cfg); err != nil {
88				t.Fatal(err)
89			}
90			r, err := Resolve(Args{APIKey: "synthetic-test-key"}, false)
91			if err != nil {
92				t.Fatal(err)
93			}
94			if r.Reasoning != tc.want {
95				t.Fatalf("startup reasoning=%q, want %q", r.Reasoning, tc.want)
96			}
97			i := modes.NewInteractive(modes.InteractiveConfig{
98				Theme: tui.Dark, Provider: r.Provider, Model: r.Model, Reasoning: r.Reasoning,
99				Agent: core.NewAgent(nil, r.Model, "", nil), SettingsStore: configSettingsStore{},
100				QuickModelShortcuts: interactiveQuickModelShortcuts(cfg.QuickModelShortcuts), ActiveModelProfile: 1,
101			})
102			i.SubmitSlash("/model " + r.Model)
103			saved, err := LoadConfig()
104			if err != nil {
105				t.Fatal(err)
106			}
107			if saved.QuickModelShortcuts[0].Reasoning != tc.want {
108				t.Fatalf("clamped active profile failed to autosave: %+v", saved.QuickModelShortcuts[0])
109			}
110		})
111	}
112}

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