v0.12.0 d035193

focus_state_machine_test.go RawHistoryFolder

1package browser
2
3import (
4	"container/heap"
5	"sync"
6	"testing"
7	"time"
8)
9
10// TestRingBuffer_AddAndRetrieve tests basic add and retrieve operations
11func TestRingBuffer_AddAndRetrieve(t *testing.T) {
12	rb := NewRingBuffer[int](5)
13
14	// Add items
15	rb.Add(1)
16	rb.Add(2)
17	rb.Add(3)
18
19	// Retrieve all
20	items := rb.GetAll()
21
22	if len(items) != 3 {
23		t.Errorf("expected 3 items, got %d", len(items))
24	}
25
26	expected := []int{1, 2, 3}
27	for i, item := range items {
28		if item != expected[i] {
29			t.Errorf("at index %d: expected %d, got %d", i, expected[i], item)
30		}
31	}
32}
33
34// TestRingBuffer_CircularOverwrite tests circular buffer overflow behavior
35func TestRingBuffer_CircularOverwrite(t *testing.T) {
36	rb := NewRingBuffer[int](3)
37
38	// Fill buffer
39	rb.Add(1)
40	rb.Add(2)
41	rb.Add(3)
42
43	// Overflow - should overwrite oldest
44	rb.Add(4)
45	rb.Add(5)
46
47	items := rb.GetAll()
48
49	if len(items) != 3 {
50		t.Errorf("expected 3 items, got %d", len(items))
51	}
52
53	// Should have 3, 4, 5 (oldest items 1, 2 overwritten)
54	expected := []int{3, 4, 5}
55	for i, item := range items {
56		if item != expected[i] {
57			t.Errorf("at index %d: expected %d, got %d", i, expected[i], item)
58		}
59	}
60}
61
62// TestRingBuffer_EmptyBuffer tests empty buffer behavior
63func TestRingBuffer_EmptyBuffer(t *testing.T) {
64	rb := NewRingBuffer[string](5)
65
66	items := rb.GetAll()
67
68	if items != nil {
69		t.Errorf("expected nil for empty buffer, got %v", items)
70	}
71}
72
73// TestRingBuffer_ConcurrentAccess tests thread-safety
74func TestRingBuffer_ConcurrentAccess(t *testing.T) {
75	rb := NewRingBuffer[int](100)
76	var wg sync.WaitGroup
77
78	// Concurrent writes
79	for i := 0; i < 10; i++ {
80		wg.Add(1)
81		go func(val int) {
82			defer wg.Done()
83			for j := 0; j < 10; j++ {
84				rb.Add(val*10 + j)
85			}
86		}(i)
87	}
88
89	// Concurrent reads
90	for i := 0; i < 5; i++ {
91		wg.Add(1)
92		go func() {
93			defer wg.Done()
94			for j := 0; j < 20; j++ {
95				_ = rb.GetAll()
96				time.Sleep(time.Microsecond)
97			}
98		}()
99	}
100
101	wg.Wait()
102
103	// Should have 100 items without panicking
104	items := rb.GetAll()
105	if len(items) != 100 {
106		t.Errorf("expected 100 items, got %d", len(items))
107	}
108}
109
110// TestPriorityQueue_OrdersByPriority tests priority ordering
111func TestPriorityQueue_OrdersByPriority(t *testing.T) {
112	tests := []struct {
113		name     string
114		requests []*FocusRequest
115		expected []string // IDs in expected pop order
116	}{
117		{
118			name: "urgent before normal",
119			requests: []*FocusRequest{
120				{ID: "normal", Priority: PriorityNormal, Timestamp: time.Now()},
121				{ID: "urgent", Priority: PriorityUrgent, Timestamp: time.Now()},
122			},
123			expected: []string{"urgent", "normal"},
124		},
125		{
126			name: "high before low",
127			requests: []*FocusRequest{
128				{ID: "low", Priority: PriorityLow, Timestamp: time.Now()},
129				{ID: "high", Priority: PriorityHigh, Timestamp: time.Now()},
130				{ID: "normal", Priority: PriorityNormal, Timestamp: time.Now()},
131			},
132			expected: []string{"high", "normal", "low"},
133		},
134		{
135			name: "multiple same priority",
136			requests: []*FocusRequest{
137				{ID: "first", Priority: PriorityNormal, Timestamp: time.Now()},
138				{ID: "second", Priority: PriorityNormal, Timestamp: time.Now()},
139				{ID: "urgent", Priority: PriorityUrgent, Timestamp: time.Now()},
140			},
141			expected: []string{"urgent", "first", "second"},
142		},
143	}
144
145	for _, tt := range tests {
146		t.Run(tt.name, func(t *testing.T) {
147			pq := make(FocusPriorityQueue, 0)
148			heap.Init(&pq)
149
150			// Push all requests
151			for _, req := range tt.requests {
152				heap.Push(&pq, req)
153			}
154
155			// Pop and verify order
156			for i, expectedID := range tt.expected {
157				if pq.Len() == 0 {
158					t.Fatalf("queue empty at position %d, expected %s", i, expectedID)
159				}
160				req := heap.Pop(&pq).(*FocusRequest)
161				if req.ID != expectedID {
162					t.Errorf("at position %d: expected ID %s, got %s", i, expectedID, req.ID)
163				}
164			}
165
166			if pq.Len() != 0 {
167				t.Errorf("queue should be empty, has %d items", pq.Len())
168			}
169		})
170	}
171}
172
173// TestPriorityQueue_PushPop tests basic push/pop operations
174func TestPriorityQueue_PushPop(t *testing.T) {
175	pq := make(FocusPriorityQueue, 0)
176	heap.Init(&pq)
177
178	// Push items
179	heap.Push(&pq, &FocusRequest{ID: "1", Priority: 50})
180	heap.Push(&pq, &FocusRequest{ID: "2", Priority: 90})
181	heap.Push(&pq, &FocusRequest{ID: "3", Priority: 10})
182
183	if pq.Len() != 3 {
184		t.Errorf("expected length 3, got %d", pq.Len())
185	}
186
187	// Pop should return highest priority (90)
188	first := heap.Pop(&pq).(*FocusRequest)
189	if first.ID != "2" {
190		t.Errorf("expected ID '2', got '%s'", first.ID)
191	}
192
193	// Next should be 50
194	second := heap.Pop(&pq).(*FocusRequest)
195	if second.ID != "1" {
196		t.Errorf("expected ID '1', got '%s'", second.ID)
197	}
198
199	// Last should be 10
200	third := heap.Pop(&pq).(*FocusRequest)
201	if third.ID != "3" {
202		t.Errorf("expected ID '3', got '%s'", third.ID)
203	}
204
205	if pq.Len() != 0 {
206		t.Errorf("queue should be empty")
207	}
208}
209
210// TestRequestDeduplicator_DetectsDuplicates tests duplicate detection
211func TestRequestDeduplicator_DetectsDuplicates(t *testing.T) {
212	dedup := NewRequestDeduplicator(100 * time.Millisecond)
213
214	// Create a dummy paneNode (nil is ok for signature generation)
215	node := &paneNode{}
216
217	req1 := FocusRequest{
218		TargetNode: node,
219		Source:     SourceKeyboard,
220		Timestamp:  time.Now(),
221	}
222
223	// First request should not be duplicate
224	if dedup.IsDuplicate(req1, false) {
225		t.Error("first request should not be duplicate")
226	}
227
228	// Immediate second request with same params should be duplicate
229	req2 := FocusRequest{
230		TargetNode: node,
231		Source:     SourceKeyboard,
232		Timestamp:  time.Now(),
233	}
234
235	if !dedup.IsDuplicate(req2, true) {
236		t.Error("second identical request should be duplicate")
237	}
238}
239
240// TestRequestDeduplicator_DifferentSourcesNotDuplicate tests different sources
241func TestRequestDeduplicator_DifferentSourcesNotDuplicate(t *testing.T) {
242	dedup := NewRequestDeduplicator(100 * time.Millisecond)
243
244	node := &paneNode{}
245
246	req1 := FocusRequest{
247		TargetNode: node,
248		Source:     SourceKeyboard,
249	}
250
251	req2 := FocusRequest{
252		TargetNode: node,
253		Source:     SourceMouse,
254	}
255
256	dedup.IsDuplicate(req1, false)
257
258	// Different source should not be duplicate
259	if dedup.IsDuplicate(req2, false) {
260		t.Error("requests with different sources should not be duplicates")
261	}
262}
263
264// TestRequestDeduplicator_ExpiresOldSignatures tests TTL expiration
265func TestRequestDeduplicator_ExpiresOldSignatures(t *testing.T) {
266	ttl := 50 * time.Millisecond
267	dedup := NewRequestDeduplicator(ttl)
268
269	node := &paneNode{}
270
271	req := FocusRequest{
272		TargetNode: node,
273		Source:     SourceKeyboard,
274	}
275
276	// First request
277	if dedup.IsDuplicate(req, false) {
278		t.Error("first request should not be duplicate")
279	}
280
281	// Immediate duplicate
282	if !dedup.IsDuplicate(req, true) {
283		t.Error("immediate duplicate should be detected")
284	}
285
286	// Wait for TTL to expire
287	time.Sleep(ttl + 10*time.Millisecond)
288
289	// Should not be duplicate after expiration
290	if dedup.IsDuplicate(req, false) {
291		t.Error("request after TTL should not be duplicate")
292	}
293}
294
295// TestRequestDeduplicator_ConcurrentAccess tests thread-safety
296func TestRequestDeduplicator_ConcurrentAccess(t *testing.T) {
297	dedup := NewRequestDeduplicator(100 * time.Millisecond)
298	var wg sync.WaitGroup
299
300	nodes := make([]*paneNode, 10)
301	for i := range nodes {
302		nodes[i] = &paneNode{}
303	}
304
305	// Concurrent duplicate checks
306	for i := 0; i < 10; i++ {
307		wg.Add(1)
308		go func(idx int) {
309			defer wg.Done()
310			for j := 0; j < 100; j++ {
311				req := FocusRequest{
312					TargetNode: nodes[idx%len(nodes)],
313					Source:     SourceKeyboard,
314				}
315				_ = dedup.IsDuplicate(req, false)
316				time.Sleep(time.Microsecond)
317			}
318		}(i)
319	}
320
321	wg.Wait()
322	// Should complete without race conditions or panics
323}
324
325// TestFocusRequest_Validation tests request validation logic
326func TestFocusRequest_Validation(t *testing.T) {
327	tests := []struct {
328		name      string
329		request   *FocusRequest
330		wantValid bool
331	}{
332		{
333			name: "valid request",
334			request: &FocusRequest{
335				ID:         "test-1",
336				TargetNode: &paneNode{},
337				Source:     SourceKeyboard,
338				Priority:   PriorityNormal,
339				Timestamp:  time.Now(),
340			},
341			wantValid: true,
342		},
343		{
344			name: "nil target node",
345			request: &FocusRequest{
346				ID:        "test-2",
347				Source:    SourceKeyboard,
348				Priority:  PriorityNormal,
349				Timestamp: time.Now(),
350			},
351			wantValid: false,
352		},
353		{
354			name: "empty ID",
355			request: &FocusRequest{
356				ID:         "",
357				TargetNode: &paneNode{},
358				Source:     SourceKeyboard,
359				Priority:   PriorityNormal,
360				Timestamp:  time.Now(),
361			},
362			wantValid: false,
363		},
364	}
365
366	for _, tt := range tests {
367		t.Run(tt.name, func(t *testing.T) {
368			valid := tt.request != nil &&
369				tt.request.TargetNode != nil &&
370				tt.request.ID != ""
371
372			if valid != tt.wantValid {
373				t.Errorf("expected valid=%v, got valid=%v", tt.wantValid, valid)
374			}
375		})
376	}
377}
378
379// TestPriorityCalculation_BySource tests priority assignment by source
380func TestPriorityCalculation_BySource(t *testing.T) {
381	tests := []struct {
382		source   FocusSource
383		expected int
384	}{
385		{SourceSystem, PriorityUrgent},
386		{SourceKeyboard, PriorityHigh},
387		{SourceProgrammatic, PriorityNormal},
388		{SourceMouse, PriorityNormal},
389		{SourceStackNav, PriorityHigh},
390		{SourceSplit, PriorityHigh},
391		{SourceClose, PriorityUrgent},
392	}
393
394	for _, tt := range tests {
395		t.Run(string(tt.source), func(t *testing.T) {
396			// This would be the actual priority calculation logic
397			// For now, we just verify expected mappings
398			var priority int
399			switch tt.source {
400			case SourceSystem, SourceClose:
401				priority = PriorityUrgent
402			case SourceKeyboard, SourceStackNav, SourceSplit:
403				priority = PriorityHigh
404			case SourceProgrammatic, SourceMouse:
405				priority = PriorityNormal
406			default:
407				priority = PriorityLow
408			}
409
410			if priority != tt.expected {
411				t.Errorf("source %s: expected priority %d, got %d", tt.source, tt.expected, priority)
412			}
413		})
414	}
415}

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 8

Tags 42

↑↓ moveenter openesc close