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}