1package modes
2
3import (
4 "testing"
5
6 "github.com/bnema/zut/packages/provider"
7)
8
9func TestNormalizeAutoCompactThreshold(t *testing.T) {
10 tests := []struct {
11 name string
12 value *int
13 want int
14 }{
15 {name: "missing", value: nil, want: 85},
16 {name: "off", value: autoCompactIntPtr(0), want: 0},
17 {name: "seventy", value: autoCompactIntPtr(70), want: 70},
18 {name: "eighty", value: autoCompactIntPtr(80), want: 80},
19 {name: "eighty five", value: autoCompactIntPtr(85), want: 85},
20 {name: "ninety", value: autoCompactIntPtr(90), want: 90},
21 {name: "invalid low", value: autoCompactIntPtr(42), want: 85},
22 {name: "invalid high", value: autoCompactIntPtr(100), want: 85},
23 }
24
25 for _, tt := range tests {
26 t.Run(tt.name, func(t *testing.T) {
27 if got := normalizeAutoCompactThreshold(tt.value); got != tt.want {
28 t.Fatalf("normalizeAutoCompactThreshold(%v) = %d, want %d", tt.value, got, tt.want)
29 }
30 })
31 }
32}
33
34func TestShouldAutoContinueAfterCompaction(t *testing.T) {
35 tests := []struct {
36 name string
37 msgs []provider.Message
38 want bool
39 }{
40 {
41 name: "completed text answer",
42 msgs: []provider.Message{{
43 Role: provider.RoleAssistant,
44 Content: []provider.Content{provider.TextBlock{Text: "done"}},
45 }},
46 want: false,
47 },
48 {
49 name: "reasoning only",
50 msgs: []provider.Message{{
51 Role: provider.RoleAssistant,
52 Content: []provider.Content{provider.ReasoningBlock{Summary: "still working"}},
53 }},
54 want: true,
55 },
56 {
57 name: "tool call",
58 msgs: []provider.Message{{
59 Role: provider.RoleAssistant,
60 Content: []provider.Content{provider.ToolCallBlock{Name: "read"}},
61 }},
62 want: true,
63 },
64 {
65 name: "user tail",
66 msgs: []provider.Message{{
67 Role: provider.RoleUser,
68 Content: []provider.Content{provider.TextBlock{Text: "continue"}},
69 }},
70 want: true,
71 },
72 {name: "empty", want: false},
73 }
74
75 for _, tt := range tests {
76 t.Run(tt.name, func(t *testing.T) {
77 if got := shouldAutoContinueAfterCompaction(tt.msgs); got != tt.want {
78 t.Fatalf("shouldAutoContinueAfterCompaction() = %t, want %t", got, tt.want)
79 }
80 })
81 }
82}
83
84func TestShouldAutoCompactUsesConfiguredThreshold(t *testing.T) {
85 tests := []struct {
86 name string
87 used int
88 window int
89 threshold int
90 want bool
91 }{
92 {name: "below threshold", used: 84, window: 100, threshold: 85, want: false},
93 {name: "at threshold", used: 85, window: 100, threshold: 85, want: true},
94 {name: "above threshold", used: 86, window: 100, threshold: 85, want: true},
95 {name: "seventy percent preset", used: 70, window: 100, threshold: 70, want: true},
96 {name: "eighty percent preset", used: 80, window: 100, threshold: 80, want: true},
97 {name: "ninety percent preset", used: 90, window: 100, threshold: 90, want: true},
98 {name: "off", used: 100, window: 100, threshold: 0, want: false},
99 {name: "missing usage", used: 0, window: 100, threshold: 85, want: false},
100 {name: "missing window", used: 85, window: 0, threshold: 85, want: false},
101 }
102
103 for _, tt := range tests {
104 t.Run(tt.name, func(t *testing.T) {
105 if got := shouldAutoCompact(tt.used, tt.window, tt.threshold); got != tt.want {
106 t.Fatalf("shouldAutoCompact(%d, %d, %d) = %t, want %t", tt.used, tt.window, tt.threshold, got, tt.want)
107 }
108 })
109 }
110}
111
112func TestSettingsDialogOffersAutoCompactThresholdPresets(t *testing.T) {
113 interactive := NewInteractive(InteractiveConfig{})
114 interactive.openSettingsDialog()
115
116 var thresholdItem *settingsItem
117 for idx := range interactive.settingsDialog.items {
118 item := &interactive.settingsDialog.items[idx]
119 if item.key == "auto_compact_threshold" {
120 thresholdItem = item
121 break
122 }
123 }
124 if thresholdItem == nil {
125 t.Fatal("settings dialog is missing auto-compact threshold")
126 }
127
128 wantValues := []string{"0", "70", "80", "85", "90"}
129 if len(thresholdItem.options) != len(wantValues) {
130 t.Fatalf("auto-compact options = %d, want %d", len(thresholdItem.options), len(wantValues))
131 }
132 for idx, want := range wantValues {
133 if got := thresholdItem.options[idx].value; got != want {
134 t.Fatalf("auto-compact option %d = %q, want %q", idx, got, want)
135 }
136 }
137 if got := thresholdItem.options[thresholdItem.choice].value; got != "85" {
138 t.Fatalf("default auto-compact choice = %q, want 85", got)
139 }
140}
141
142func TestApplyAutoCompactThresholdSynchronizesLiveUpdate(t *testing.T) {
143 initialThreshold := 85
144 releaseStore := make(chan struct{})
145 store := &blockingAutoCompactSettingsStore{
146 entered: make(chan int, 1),
147 release: releaseStore,
148 }
149 interactive := NewInteractive(InteractiveConfig{
150 AutoCompactThreshold: &initialThreshold,
151 SettingsStore: store,
152 })
153
154 interactive.mu.Lock()
155 locked := true
156 released := false
157 done := make(chan struct{})
158 go func() {
159 defer close(done)
160 interactive.applyAutoCompactThresholdSetting("70")
161 }()
162 defer func() {
163 if !released {
164 close(releaseStore)
165 }
166 if locked {
167 interactive.mu.Unlock()
168 }
169 <-done
170 }()
171
172 if got := <-store.entered; got != 70 {
173 t.Fatalf("persisted threshold = %d, want 70", got)
174 }
175 if got := *interactive.cfg.AutoCompactThreshold; got != 85 {
176 t.Fatalf("threshold changed without holding Interactive.mu: got %d, want 85", got)
177 }
178
179 close(releaseStore)
180 released = true
181 interactive.mu.Unlock()
182 locked = false
183 <-done
184 if got := *interactive.cfg.AutoCompactThreshold; got != 70 {
185 t.Fatalf("threshold after setting completes = %d, want 70", got)
186 }
187}
188
189type blockingAutoCompactSettingsStore struct {
190 SettingsStore
191 entered chan int
192 release <-chan struct{}
193}
194
195func (s *blockingAutoCompactSettingsStore) SetAutoCompactThreshold(percent int) error {
196 s.entered <- percent
197 <-s.release
198 return nil
199}
200
201func autoCompactIntPtr(value int) *int {
202 return &value
203}