1package modes
2
3import (
4 "errors"
5 "strings"
6 "testing"
7
8 "github.com/bnema/zut/packages/core"
9)
10
11type recordingPonytailSettingsStore struct {
12 SettingsStore
13 values []bool
14 calls int
15 err error
16 failures map[int]error
17}
18
19func (s *recordingPonytailSettingsStore) SetPonytailEnabled(enabled bool) error {
20 s.calls++
21 if s.err != nil {
22 return s.err
23 }
24 if err := s.failures[s.calls]; err != nil {
25 return err
26 }
27 s.values = append(s.values, enabled)
28 return nil
29}
30
31func TestSettingsDialogOffersPonytailEnabledByDefault(t *testing.T) {
32 store := &recordingPonytailSettingsStore{}
33 interactive := NewInteractive(InteractiveConfig{
34 SettingsStore: store,
35 RefreshPrompt: func() error { return nil },
36 })
37 interactive.openSettingsDialog()
38
39 item := findSettingsItem(interactive.settingsDialog.items, "ponytail_enabled")
40 if item == nil {
41 t.Fatal("settings dialog is missing Ponytail mode")
42 }
43 if !item.value {
44 t.Fatal("Ponytail mode should be enabled when the config value is absent")
45 }
46 if item.disabled {
47 t.Fatal("Ponytail mode should be available with persistence and refresh capabilities")
48 }
49}
50
51func TestSettingsDialogDisablesPonytailWithoutCapabilities(t *testing.T) {
52 cases := []struct {
53 name string
54 cfg InteractiveConfig
55 }{
56 {name: "neither capability", cfg: InteractiveConfig{}},
57 {name: "persistence only", cfg: InteractiveConfig{SettingsStore: &recordingPonytailSettingsStore{}}},
58 {name: "refresh only", cfg: InteractiveConfig{RefreshPrompt: func() error { return nil }}},
59 }
60 for _, tc := range cases {
61 t.Run(tc.name, func(t *testing.T) {
62 interactive := NewInteractive(tc.cfg)
63 interactive.openSettingsDialog()
64
65 item := findSettingsItem(interactive.settingsDialog.items, "ponytail_enabled")
66 if item == nil {
67 t.Fatal("settings dialog is missing Ponytail mode")
68 }
69 if !item.disabled {
70 t.Fatal("Ponytail mode should be disabled without both capabilities")
71 }
72 if item.hint == "" {
73 t.Fatal("disabled Ponytail mode should explain why it is unavailable")
74 }
75 })
76 }
77}
78
79func TestApplyPonytailToggleRefreshesAndPersists(t *testing.T) {
80 store := &recordingPonytailSettingsStore{}
81 refreshCalls := 0
82 interactive := NewInteractive(InteractiveConfig{
83 SettingsStore: store,
84 RefreshPrompt: func() error {
85 refreshCalls++
86 return nil
87 },
88 })
89 interactive.rend = nil
90
91 interactive.applySettingToggle("ponytail_enabled", false)
92 if interactive.ponytailEnabled() {
93 t.Fatal("Ponytail mode remained enabled after disabling it")
94 }
95 interactive.applySettingToggle("ponytail_enabled", true)
96 if !interactive.ponytailEnabled() {
97 t.Fatal("Ponytail mode remained disabled after enabling it")
98 }
99 if refreshCalls != 2 {
100 t.Fatalf("refresh calls = %d, want 2", refreshCalls)
101 }
102 if len(store.values) != 2 || store.values[0] || !store.values[1] {
103 t.Fatalf("persisted values = %v, want [false true]", store.values)
104 }
105 if interactive.statusErr != "" {
106 t.Fatalf("unexpected status error: %q", interactive.statusErr)
107 }
108}
109
110func TestApplyPonytailToggleRollsBackWhenRefreshFails(t *testing.T) {
111 store := &recordingPonytailSettingsStore{}
112 refreshCalls := 0
113 interactive := NewInteractive(InteractiveConfig{
114 SettingsStore: store,
115 RefreshPrompt: func() error {
116 refreshCalls++
117 if refreshCalls == 1 {
118 return errors.New("refresh failed")
119 }
120 return nil
121 },
122 })
123 interactive.rend = nil
124
125 interactive.applySettingToggle("ponytail_enabled", false)
126 if !interactive.ponytailEnabled() {
127 t.Fatal("Ponytail mode stayed disabled after refresh rollback")
128 }
129 if refreshCalls != 2 {
130 t.Fatalf("refresh calls = %d, want initial refresh and rollback refresh", refreshCalls)
131 }
132 if len(store.values) != 2 || store.values[0] || !store.values[1] {
133 t.Fatalf("persisted rollback values = %v, want [false true]", store.values)
134 }
135 if !strings.Contains(interactive.statusErr, "refresh failed") {
136 t.Fatalf("status error = %q, want refresh failure", interactive.statusErr)
137 }
138}
139
140func TestApplyPonytailToggleKeepsDurableStateWhenRollbackPersistenceFails(t *testing.T) {
141 store := &recordingPonytailSettingsStore{
142 failures: map[int]error{2: errors.New("rollback disk full")},
143 }
144 refreshCalls := 0
145 interactive := NewInteractive(InteractiveConfig{
146 SettingsStore: store,
147 RefreshPrompt: func() error {
148 refreshCalls++
149 if refreshCalls == 1 {
150 return errors.New("refresh failed")
151 }
152 return nil
153 },
154 })
155 interactive.rend = nil
156
157 interactive.applySettingToggle("ponytail_enabled", false)
158 if interactive.ponytailEnabled() {
159 t.Fatal("in-memory setting reverted despite rollback persistence failure")
160 }
161 if len(store.values) != 1 || store.values[0] {
162 t.Fatalf("durable writes = %v, want only the successful disable", store.values)
163 }
164 if refreshCalls != 2 {
165 t.Fatalf("refresh calls = %d, want failed refresh plus durable-state reconciliation", refreshCalls)
166 }
167 if !strings.Contains(interactive.statusErr, "rollback disk full") {
168 t.Fatalf("status error = %q, want rollback failure", interactive.statusErr)
169 }
170}
171
172func TestApplyPonytailToggleKeepsStateWhenPersistenceFails(t *testing.T) {
173 store := &recordingPonytailSettingsStore{err: errors.New("disk full")}
174 refreshCalls := 0
175 interactive := NewInteractive(InteractiveConfig{
176 SettingsStore: store,
177 RefreshPrompt: func() error {
178 refreshCalls++
179 return nil
180 },
181 })
182 interactive.rend = nil
183
184 interactive.applySettingToggle("ponytail_enabled", false)
185 if !interactive.ponytailEnabled() {
186 t.Fatal("Ponytail mode changed despite persistence failure")
187 }
188 if refreshCalls != 0 {
189 t.Fatalf("refresh calls = %d, want none", refreshCalls)
190 }
191 if !strings.Contains(interactive.statusErr, "disk full") {
192 t.Fatalf("status error = %q, want persistence failure", interactive.statusErr)
193 }
194}
195
196func TestApplyPonytailToggleUpdatesLiveAgentPrompt(t *testing.T) {
197 const ponytailBlock = "[ponytail test block]"
198 store := &recordingPonytailSettingsStore{}
199 ag := core.NewAgent(nil, "model", "base\n\n"+ponytailBlock, nil)
200 var interactive *Interactive
201 refreshCalls := 0
202 interactive = NewInteractive(InteractiveConfig{
203 Agent: ag,
204 SettingsStore: store,
205 RefreshPrompt: func() error {
206 refreshCalls++
207 system := "base"
208 if interactive.ponytailEnabled() {
209 system += "\n\n" + ponytailBlock
210 }
211 ag.SetSystemPrompt(system)
212 return nil
213 },
214 })
215 interactive.rend = nil
216
217 interactive.applySettingToggle("ponytail_enabled", false)
218 system, _ := ag.PromptConfig()
219 if strings.Contains(system, ponytailBlock) {
220 t.Fatalf("live prompt still contains disabled Ponytail block: %q", system)
221 }
222 interactive.applySettingToggle("ponytail_enabled", true)
223 system, _ = ag.PromptConfig()
224 if strings.Count(system, ponytailBlock) != 1 {
225 t.Fatalf("live prompt block count = %d, want 1: %q", strings.Count(system, ponytailBlock), system)
226 }
227 if refreshCalls != 2 {
228 t.Fatalf("refresh calls = %d, want 2", refreshCalls)
229 }
230}