detached d11d103

test(focus): add unit tests for focus state machine data structures

commit
d11d1030312ea2b787a9197e900316f4496d5651
author
Brice · · 2025-10-05 15:51 UTC
parent
4575e03
signature
unsigned
1 file · +415 -0
A internal/app/browser/focus_state_machine_test.go +415 -0
@@ -0,0 +1,415 @@
1+package browser
2+
3+import (
4+ "container/heap"
5+ "sync"
6+ "testing"
7+ "time"
8+)
9+
10+// TestRingBuffer_AddAndRetrieve tests basic add and retrieve operations
11+func 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
35+func 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
63+func 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
74+func 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
111+func 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
174+func 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
211+func 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) {
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) {
236+ t.Error("second identical request should be duplicate")
237+ }
238+}
239+
240+// TestRequestDeduplicator_DifferentSourcesNotDuplicate tests different sources
241+func 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)
257+
258+ // Different source should not be duplicate
259+ if dedup.IsDuplicate(req2) {
260+ t.Error("requests with different sources should not be duplicates")
261+ }
262+}
263+
264+// TestRequestDeduplicator_ExpiresOldSignatures tests TTL expiration
265+func 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) {
278+ t.Error("first request should not be duplicate")
279+ }
280+
281+ // Immediate duplicate
282+ if !dedup.IsDuplicate(req) {
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) {
291+ t.Error("request after TTL should not be duplicate")
292+ }
293+}
294+
295+// TestRequestDeduplicator_ConcurrentAccess tests thread-safety
296+func 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)
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
326+func 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
380+func 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+}
@@ -0,0 +1,415 @@
1+package browser
2+
3+import (
4+ "container/heap"
5+ "sync"
6+ "testing"
7+ "time"
8+)
9+
10+// TestRingBuffer_AddAndRetrieve tests basic add and retrieve operations
11+func 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
35+func 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
63+func 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
74+func 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
111+func 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
174+func 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
211+func 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) {
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) {
236+ t.Error("second identical request should be duplicate")
237+ }
238+}
239+
240+// TestRequestDeduplicator_DifferentSourcesNotDuplicate tests different sources
241+func 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)
257+
258+ // Different source should not be duplicate
259+ if dedup.IsDuplicate(req2) {
260+ t.Error("requests with different sources should not be duplicates")
261+ }
262+}
263+
264+// TestRequestDeduplicator_ExpiresOldSignatures tests TTL expiration
265+func 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) {
278+ t.Error("first request should not be duplicate")
279+ }
280+
281+ // Immediate duplicate
282+ if !dedup.IsDuplicate(req) {
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) {
291+ t.Error("request after TTL should not be duplicate")
292+ }
293+}
294+
295+// TestRequestDeduplicator_ConcurrentAccess tests thread-safety
296+func 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)
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
326+func 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
380+func 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