v0.4.2 83c2c60

auto_compact_test.go RawHistoryFolder

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}

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 477

↑↓ moveenter openesc close