1package modes
2
3import (
4 "context"
5 "errors"
6 "strings"
7 "testing"
8
9 "github.com/bnema/zut/packages/agent/subagents"
10 "github.com/bnema/zut/packages/core"
11 "github.com/bnema/zut/packages/provider"
12)
13
14type recordingFastModeSettingsStore struct {
15 SettingsStore
16 setCalls int
17 last bool
18}
19
20func (s *recordingFastModeSettingsStore) SetFastMode(enabled bool) error {
21 s.setCalls++
22 s.last = enabled
23 return nil
24}
25
26type failingFastModeSettingsStore struct {
27 SettingsStore
28 err error
29}
30
31func (s *failingFastModeSettingsStore) SetFastMode(bool) error { return s.err }
32
33type fastModeProviderClient struct{}
34
35func (fastModeProviderClient) Name() string { return "openai" }
36func (fastModeProviderClient) Stream(_ context.Context, _ provider.Request) (<-chan provider.Event, error) {
37 out := make(chan provider.Event)
38 close(out)
39 return out, nil
40}
41
42func TestSettingsDialogOffersFastModeOffByDefault(t *testing.T) {
43 interactive := NewInteractive(InteractiveConfig{Provider: "openai", Model: "gpt-5"})
44 interactive.openSettingsDialog()
45
46 item := findSettingsItem(interactive.settingsDialog.items, "fast_mode")
47 if item == nil {
48 t.Fatal("settings dialog is missing fast mode")
49 }
50 if item.value {
51 t.Fatal("fast mode is enabled by default")
52 }
53}
54
55func TestApplyFastModeUpdatesAgentAndStore(t *testing.T) {
56 store := &recordingFastModeSettingsStore{}
57 agent := core.NewAgent(fastModeProviderClient{}, "gpt-5", "", nil)
58 interactive := NewInteractive(InteractiveConfig{
59 Provider: "openai",
60 Model: "gpt-5",
61 SettingsStore: store,
62 Agent: agent,
63 })
64 interactive.openSettingsDialog()
65 interactive.rend = nil
66
67 interactive.applySettingToggle("fast_mode", true)
68
69 if interactive.cfg.FastMode == nil || !*interactive.cfg.FastMode {
70 t.Fatal("interactive FastMode was not enabled")
71 }
72 if !agent.FastMode {
73 t.Fatal("agent FastMode was not enabled")
74 }
75 if store.setCalls != 1 || !store.last {
76 t.Fatalf("store calls = %d/%v, want one enabled write", store.setCalls, store.last)
77 }
78}
79
80func TestFastSlashTogglesFastMode(t *testing.T) {
81 store := &recordingFastModeSettingsStore{}
82 agent := core.NewAgent(fastModeProviderClient{}, "gpt-5", "", nil)
83 supervisor := subagents.New(subagents.Config{Root: t.TempDir(), RepoRoot: t.TempDir()})
84 interactive := NewInteractive(InteractiveConfig{
85 Provider: "openai",
86 Model: "gpt-5",
87 SettingsStore: store,
88 Agent: agent,
89 Supervisor: supervisor,
90 })
91 interactive.rend = nil
92
93 if interactive.runSlash(context.Background(), "/FAST") {
94 t.Fatal("/fast requested exit")
95 }
96 if interactive.cfg.FastMode == nil || !*interactive.cfg.FastMode || !agent.FastMode || !supervisor.FastMode() {
97 t.Fatalf("/fast did not enable every live fast-mode target: cfg=%v agent=%v supervisor=%v", interactive.cfg.FastMode, agent.FastMode, supervisor.FastMode())
98 }
99 if store.setCalls != 1 || !store.last || interactive.statusOK != "fast mode enabled" {
100 t.Fatalf("enable persistence/status = calls=%d last=%v status=%q", store.setCalls, store.last, interactive.statusOK)
101 }
102
103 interactive.runSlash(context.Background(), "/fast")
104 if interactive.cfg.FastMode == nil || *interactive.cfg.FastMode || agent.FastMode || supervisor.FastMode() {
105 t.Fatalf("/fast did not disable every live fast-mode target: cfg=%v agent=%v supervisor=%v", interactive.cfg.FastMode, agent.FastMode, supervisor.FastMode())
106 }
107 if store.setCalls != 2 || store.last || interactive.statusOK != "fast mode disabled" {
108 t.Fatalf("disable persistence/status = calls=%d last=%v status=%q", store.setCalls, store.last, interactive.statusOK)
109 }
110}
111
112func TestApplyFastModeRejectsUnsupportedProvider(t *testing.T) {
113 store := &recordingFastModeSettingsStore{}
114 interactive := NewInteractive(InteractiveConfig{
115 Provider: "anthropic",
116 Model: "claude-sonnet-4-5",
117 SettingsStore: store,
118 })
119 interactive.openSettingsDialog()
120 interactive.rend = nil
121
122 interactive.applySettingToggle("fast_mode", true)
123
124 if interactive.cfg.FastMode != nil && *interactive.cfg.FastMode {
125 t.Fatal("unsupported fast mode was enabled")
126 }
127 if store.setCalls != 0 {
128 t.Fatalf("store calls = %d, want none", store.setCalls)
129 }
130 if !strings.Contains(interactive.statusErr, "only supported for OpenAI providers") {
131 t.Fatalf("status error = %q, want unsupported-provider error", interactive.statusErr)
132 }
133}
134
135func TestApplyFastModeCanDisableUnsupportedProvider(t *testing.T) {
136 enabled := true
137 store := &recordingFastModeSettingsStore{}
138 agent := core.NewAgent(fastModeProviderClient{}, "claude-sonnet-4-5", "", nil)
139 agent.FastMode = true
140 interactive := NewInteractive(InteractiveConfig{
141 Provider: "anthropic",
142 Model: "claude-sonnet-4-5",
143 FastMode: &enabled,
144 SettingsStore: store,
145 Agent: agent,
146 })
147 interactive.openSettingsDialog()
148 interactive.rend = nil
149
150 interactive.applySettingToggle("fast_mode", false)
151
152 if interactive.cfg.FastMode == nil || *interactive.cfg.FastMode {
153 t.Fatal("fast mode was not disabled")
154 }
155 if agent.FastMode {
156 t.Fatal("agent fast mode was not disabled")
157 }
158 if store.setCalls != 1 || store.last {
159 t.Fatalf("store calls = %d/%v, want one disabled write", store.setCalls, store.last)
160 }
161}
162
163func TestApplyFastModeLeavesLiveStateWhenPersistenceFails(t *testing.T) {
164 store := &failingFastModeSettingsStore{err: errors.New("disk full")}
165 agent := core.NewAgent(fastModeProviderClient{}, "gpt-5", "", nil)
166 interactive := NewInteractive(InteractiveConfig{
167 Provider: "openai",
168 Model: "gpt-5",
169 SettingsStore: store,
170 Agent: agent,
171 })
172 interactive.openSettingsDialog()
173 interactive.rend = nil
174
175 interactive.applySettingToggle("fast_mode", true)
176
177 if interactive.cfg.FastMode != nil && *interactive.cfg.FastMode {
178 t.Fatal("fast mode changed despite persistence failure")
179 }
180 if agent.FastMode {
181 t.Fatal("agent fast mode changed despite persistence failure")
182 }
183 if !strings.Contains(interactive.statusErr, "disk full") {
184 t.Fatalf("status error = %q, want persistence error", interactive.statusErr)
185 }
186}
187
188func findSettingsItem(items []settingsItem, key string) *settingsItem {
189 for idx := range items {
190 if items[idx].key == key {
191 return &items[idx]
192 }
193 }
194 return nil
195}