v0.27.2 5960dd8

history_repo_test.go RawHistoryFolder

1package sqlite_test
2
3import (
4	"context"
5	"path/filepath"
6	"testing"
7	"time"
8
9	"github.com/bnema/dumber/internal/domain/entity"
10	"github.com/bnema/dumber/internal/infrastructure/persistence/sqlite"
11	"github.com/bnema/dumber/internal/logging"
12	"github.com/stretchr/testify/assert"
13	"github.com/stretchr/testify/require"
14)
15
16func historyTestCtx() context.Context {
17	logger := logging.NewFromConfigValues("debug", "console")
18	return logging.WithContext(context.Background(), logger)
19}
20
21func TestHistoryRepository_GetRecentSince(t *testing.T) {
22	ctx := historyTestCtx()
23	dbPath := filepath.Join(t.TempDir(), "dumber.db")
24
25	db, err := sqlite.NewConnection(ctx, dbPath)
26	require.NoError(t, err)
27	t.Cleanup(func() { _ = db.Close() })
28
29	repo := sqlite.NewHistoryRepository(db)
30
31	// Save some history entries
32	entries := []*entity.HistoryEntry{
33		{URL: "https://example.com", Title: "Example"},
34		{URL: "https://github.com", Title: "GitHub"},
35		{URL: "https://google.com", Title: "Google"},
36	}
37
38	for _, e := range entries {
39		require.NoError(t, repo.Save(ctx, e))
40	}
41
42	// Get recent since 30 days (all should be included)
43	results, err := repo.GetRecentSince(ctx, 30)
44	require.NoError(t, err)
45	require.Len(t, results, 3)
46
47	// Results should be sorted by last_visited DESC
48	// Since they were all just inserted, order may vary
49	urls := make(map[string]bool)
50	for _, r := range results {
51		urls[r.URL] = true
52	}
53	assert.True(t, urls["https://example.com"])
54	assert.True(t, urls["https://github.com"])
55	assert.True(t, urls["https://google.com"])
56}
57
58func TestHistoryRepository_GetRecentSince_EmptyResult(t *testing.T) {
59	ctx := historyTestCtx()
60	dbPath := filepath.Join(t.TempDir(), "dumber.db")
61
62	db, err := sqlite.NewConnection(ctx, dbPath)
63	require.NoError(t, err)
64	t.Cleanup(func() { _ = db.Close() })
65
66	repo := sqlite.NewHistoryRepository(db)
67
68	// Get recent with no data
69	results, err := repo.GetRecentSince(ctx, 30)
70	require.NoError(t, err)
71	assert.Empty(t, results)
72}
73
74func TestHistoryRepository_GetMostVisited(t *testing.T) {
75	ctx := historyTestCtx()
76	dbPath := filepath.Join(t.TempDir(), "dumber.db")
77
78	db, err := sqlite.NewConnection(ctx, dbPath)
79	require.NoError(t, err)
80	t.Cleanup(func() { _ = db.Close() })
81
82	repo := sqlite.NewHistoryRepository(db)
83
84	// Save entries with different visit counts
85	entry1 := &entity.HistoryEntry{URL: "https://example.com", Title: "Example"}
86	entry2 := &entity.HistoryEntry{URL: "https://github.com", Title: "GitHub"}
87	entry3 := &entity.HistoryEntry{URL: "https://google.com", Title: "Google"}
88
89	require.NoError(t, repo.Save(ctx, entry1))
90	require.NoError(t, repo.Save(ctx, entry2))
91	require.NoError(t, repo.Save(ctx, entry3))
92
93	// Increment visit counts to create different popularities
94	// github: 5 visits, google: 3 visits, example: 1 visit
95	for i := 0; i < 4; i++ {
96		require.NoError(t, repo.IncrementVisitCount(ctx, "https://github.com"))
97	}
98	for i := 0; i < 2; i++ {
99		require.NoError(t, repo.IncrementVisitCount(ctx, "https://google.com"))
100	}
101
102	// Get most visited
103	results, err := repo.GetMostVisited(ctx, 30)
104	require.NoError(t, err)
105	require.Len(t, results, 3)
106
107	// First result should be github (5 visits)
108	assert.Equal(t, "https://github.com", results[0].URL)
109	assert.Equal(t, int64(5), results[0].VisitCount)
110
111	// Second should be google (3 visits)
112	assert.Equal(t, "https://google.com", results[1].URL)
113	assert.Equal(t, int64(3), results[1].VisitCount)
114
115	// Third should be example (1 visit)
116	assert.Equal(t, "https://example.com", results[2].URL)
117	assert.Equal(t, int64(1), results[2].VisitCount)
118}
119
120func TestHistoryRepository_GetMostVisited_EmptyResult(t *testing.T) {
121	ctx := historyTestCtx()
122	dbPath := filepath.Join(t.TempDir(), "dumber.db")
123
124	db, err := sqlite.NewConnection(ctx, dbPath)
125	require.NoError(t, err)
126	t.Cleanup(func() { _ = db.Close() })
127
128	repo := sqlite.NewHistoryRepository(db)
129
130	// Get most visited with no data
131	results, err := repo.GetMostVisited(ctx, 30)
132	require.NoError(t, err)
133	assert.Empty(t, results)
134}
135
136func TestHistoryRepository_GetRecentSince_RespectsTimeFilter(t *testing.T) {
137	ctx := historyTestCtx()
138	dbPath := filepath.Join(t.TempDir(), "dumber.db")
139
140	db, err := sqlite.NewConnection(ctx, dbPath)
141	require.NoError(t, err)
142	t.Cleanup(func() { _ = db.Close() })
143
144	repo := sqlite.NewHistoryRepository(db)
145
146	// Save an entry
147	entry := &entity.HistoryEntry{URL: "https://example.com", Title: "Example"}
148	require.NoError(t, repo.Save(ctx, entry))
149
150	// Query with 1 day filter - should include recently added entry
151	results, err := repo.GetRecentSince(ctx, 1)
152	require.NoError(t, err)
153	require.Len(t, results, 1)
154
155	// Query with very large days filter - should still work
156	results, err = repo.GetRecentSince(ctx, 365)
157	require.NoError(t, err)
158	require.Len(t, results, 1)
159}
160
161func TestHistoryRepository_CRUD(t *testing.T) {
162	ctx := historyTestCtx()
163	dbPath := filepath.Join(t.TempDir(), "dumber.db")
164
165	db, err := sqlite.NewConnection(ctx, dbPath)
166	require.NoError(t, err)
167	t.Cleanup(func() { _ = db.Close() })
168
169	repo := sqlite.NewHistoryRepository(db)
170
171	// Create
172	entry := &entity.HistoryEntry{URL: "https://example.com", Title: "Example"}
173	require.NoError(t, repo.Save(ctx, entry))
174
175	// Read
176	found, err := repo.FindByURL(ctx, "https://example.com")
177	require.NoError(t, err)
178	require.NotNil(t, found)
179	assert.Equal(t, "https://example.com", found.URL)
180	assert.Equal(t, "Example", found.Title)
181	assert.Equal(t, int64(1), found.VisitCount)
182
183	// Update (save again increments visit count)
184	require.NoError(t, repo.Save(ctx, entry))
185	found2, err := repo.FindByURL(ctx, "https://example.com")
186	require.NoError(t, err)
187	assert.Equal(t, int64(2), found2.VisitCount)
188
189	// Get recent
190	recent, err := repo.GetRecent(ctx, 10, 0)
191	require.NoError(t, err)
192	require.Len(t, recent, 1)
193
194	// Delete
195	require.NoError(t, repo.Delete(ctx, found.ID))
196
197	// Verify deleted
198	deleted, err := repo.FindByURL(ctx, "https://example.com")
199	require.NoError(t, err)
200	assert.Nil(t, deleted)
201}
202
203// Ensure the test uses an isolated timestamp for time-sensitive queries
204func TestHistoryRepository_TimeFilterIntegrity(t *testing.T) {
205	ctx := historyTestCtx()
206	dbPath := filepath.Join(t.TempDir(), "dumber.db")
207
208	db, err := sqlite.NewConnection(ctx, dbPath)
209	require.NoError(t, err)
210	t.Cleanup(func() { _ = db.Close() })
211
212	repo := sqlite.NewHistoryRepository(db)
213
214	// Save entry
215	entry := &entity.HistoryEntry{URL: "https://test.com", Title: "Test"}
216	require.NoError(t, repo.Save(ctx, entry))
217
218	// Verify entry was saved with recent timestamp
219	found, err := repo.FindByURL(ctx, "https://test.com")
220	require.NoError(t, err)
221	require.NotNil(t, found)
222
223	// LastVisited should be within the last minute
224	timeDiff := time.Since(found.LastVisited)
225	assert.Less(t, timeDiff, time.Minute, "LastVisited should be recent, got %v ago", timeDiff)
226}
227
228func TestHistoryRepository_GetRecentSince_RejectsNonPositiveDays(t *testing.T) {
229	ctx := historyTestCtx()
230	dbPath := filepath.Join(t.TempDir(), "dumber.db")
231
232	db, err := sqlite.NewConnection(ctx, dbPath)
233	require.NoError(t, err)
234	t.Cleanup(func() { _ = db.Close() })
235
236	repo := sqlite.NewHistoryRepository(db)
237
238	// days = 0 should return error
239	_, err = repo.GetRecentSince(ctx, 0)
240	require.Error(t, err)
241	assert.Contains(t, err.Error(), "days must be positive")
242
243	// days < 0 should return error
244	_, err = repo.GetRecentSince(ctx, -5)
245	require.Error(t, err)
246	assert.Contains(t, err.Error(), "days must be positive")
247}
248
249func TestHistoryRepository_GetMostVisited_RejectsNonPositiveDays(t *testing.T) {
250	ctx := historyTestCtx()
251	dbPath := filepath.Join(t.TempDir(), "dumber.db")
252
253	db, err := sqlite.NewConnection(ctx, dbPath)
254	require.NoError(t, err)
255	t.Cleanup(func() { _ = db.Close() })
256
257	repo := sqlite.NewHistoryRepository(db)
258
259	// days = 0 should return error
260	_, err = repo.GetMostVisited(ctx, 0)
261	require.Error(t, err)
262	assert.Contains(t, err.Error(), "days must be positive")
263
264	// days < 0 should return error
265	_, err = repo.GetMostVisited(ctx, -5)
266	require.Error(t, err)
267	assert.Contains(t, err.Error(), "days must be positive")
268}
269
270func TestHistoryRepository_GetAllRecentHistory(t *testing.T) {
271	ctx := historyTestCtx()
272	dbPath := filepath.Join(t.TempDir(), "dumber.db")
273
274	db, err := sqlite.NewConnection(ctx, dbPath)
275	require.NoError(t, err)
276	t.Cleanup(func() { _ = db.Close() })
277
278	repo := sqlite.NewHistoryRepository(db)
279
280	// Save some entries
281	entries := []*entity.HistoryEntry{
282		{URL: "https://example.com", Title: "Example"},
283		{URL: "https://github.com", Title: "GitHub"},
284	}
285	for _, e := range entries {
286		require.NoError(t, repo.Save(ctx, e))
287	}
288
289	// Get all recent history
290	results, err := repo.GetAllRecentHistory(ctx)
291	require.NoError(t, err)
292	require.Len(t, results, 2)
293}
294
295func TestHistoryRepository_GetAllMostVisited(t *testing.T) {
296	ctx := historyTestCtx()
297	dbPath := filepath.Join(t.TempDir(), "dumber.db")
298
299	db, err := sqlite.NewConnection(ctx, dbPath)
300	require.NoError(t, err)
301	t.Cleanup(func() { _ = db.Close() })
302
303	repo := sqlite.NewHistoryRepository(db)
304
305	// Save entries with different visit counts
306	entry1 := &entity.HistoryEntry{URL: "https://example.com", Title: "Example"}
307	entry2 := &entity.HistoryEntry{URL: "https://github.com", Title: "GitHub"}
308
309	require.NoError(t, repo.Save(ctx, entry1))
310	require.NoError(t, repo.Save(ctx, entry2))
311
312	// Increment github visits
313	for i := 0; i < 3; i++ {
314		require.NoError(t, repo.IncrementVisitCount(ctx, "https://github.com"))
315	}
316
317	// Get all most visited
318	results, err := repo.GetAllMostVisited(ctx)
319	require.NoError(t, err)
320	require.Len(t, results, 2)
321
322	// First should be github (4 visits)
323	assert.Equal(t, "https://github.com", results[0].URL)
324	assert.Equal(t, int64(4), results[0].VisitCount)
325}
326
327func TestHistoryRepository_Search_SingleWord(t *testing.T) {
328	ctx := historyTestCtx()
329	dbPath := filepath.Join(t.TempDir(), "dumber.db")
330
331	db, err := sqlite.NewConnection(ctx, dbPath)
332	require.NoError(t, err)
333	t.Cleanup(func() { _ = db.Close() })
334
335	repo := sqlite.NewHistoryRepository(db)
336
337	// Save entries
338	entries := []*entity.HistoryEntry{
339		{URL: "https://github.com/user/repo", Title: "My GitHub Repository"},
340		{URL: "https://gitlab.com/project", Title: "GitLab Project"},
341		{URL: "https://example.com", Title: "Example Site"},
342	}
343	for _, e := range entries {
344		require.NoError(t, repo.Save(ctx, e))
345	}
346
347	// Search for "github" - should find the github entry
348	results, err := repo.Search(ctx, "github", 10)
349	require.NoError(t, err)
350	require.Len(t, results, 1)
351	assert.Equal(t, "https://github.com/user/repo", results[0].Entry.URL)
352}
353
354func TestHistoryRepository_Search_MultiWord(t *testing.T) {
355	ctx := historyTestCtx()
356	dbPath := filepath.Join(t.TempDir(), "dumber.db")
357
358	db, err := sqlite.NewConnection(ctx, dbPath)
359	require.NoError(t, err)
360	t.Cleanup(func() { _ = db.Close() })
361
362	repo := sqlite.NewHistoryRepository(db)
363
364	// Save entries
365	entries := []*entity.HistoryEntry{
366		{URL: "https://github.com/issues", Title: "GitHub Issues"},
367		{URL: "https://github.com/pulls", Title: "GitHub Pull Requests"},
368		{URL: "https://jira.com/issues", Title: "Jira Issues"},
369	}
370	for _, e := range entries {
371		require.NoError(t, repo.Save(ctx, e))
372	}
373
374	// Search for "github issues" - should find only the github issues entry
375	results, err := repo.Search(ctx, "github issues", 10)
376	require.NoError(t, err)
377	require.Len(t, results, 1)
378	assert.Equal(t, "https://github.com/issues", results[0].Entry.URL)
379}
380
381func TestHistoryRepository_Search_MatchesTitle(t *testing.T) {
382	ctx := historyTestCtx()
383	dbPath := filepath.Join(t.TempDir(), "dumber.db")
384
385	db, err := sqlite.NewConnection(ctx, dbPath)
386	require.NoError(t, err)
387	t.Cleanup(func() { _ = db.Close() })
388
389	repo := sqlite.NewHistoryRepository(db)
390
391	// Save entry with specific title that doesn't appear in URL
392	entry := &entity.HistoryEntry{
393		URL:   "https://docs.example.com/guide",
394		Title: "Comprehensive Documentation Guide",
395	}
396	require.NoError(t, repo.Save(ctx, entry))
397
398	// Search for "Documentation" - should find by title
399	results, err := repo.Search(ctx, "Documentation", 10)
400	require.NoError(t, err)
401	require.Len(t, results, 1)
402	assert.Equal(t, "https://docs.example.com/guide", results[0].Entry.URL)
403}
404
405func TestHistoryRepository_Search_EmptyQuery(t *testing.T) {
406	ctx := historyTestCtx()
407	dbPath := filepath.Join(t.TempDir(), "dumber.db")
408
409	db, err := sqlite.NewConnection(ctx, dbPath)
410	require.NoError(t, err)
411	t.Cleanup(func() { _ = db.Close() })
412
413	repo := sqlite.NewHistoryRepository(db)
414
415	// Save an entry
416	require.NoError(t, repo.Save(ctx, &entity.HistoryEntry{
417		URL:   "https://example.com",
418		Title: "Example",
419	}))
420
421	// Empty query should return empty results, no error
422	results, err := repo.Search(ctx, "", 10)
423	require.NoError(t, err)
424	assert.Empty(t, results)
425
426	// Whitespace-only query should also return empty results
427	results, err = repo.Search(ctx, "   ", 10)
428	require.NoError(t, err)
429	assert.Empty(t, results)
430}
431
432func TestHistoryRepository_Search_NoValidTokens(t *testing.T) {
433	ctx := historyTestCtx()
434	dbPath := filepath.Join(t.TempDir(), "dumber.db")
435
436	db, err := sqlite.NewConnection(ctx, dbPath)
437	require.NoError(t, err)
438	t.Cleanup(func() { _ = db.Close() })
439
440	repo := sqlite.NewHistoryRepository(db)
441
442	require.NoError(t, repo.Save(ctx, &entity.HistoryEntry{
443		URL:   "https://example.com",
444		Title: "Example",
445	}))
446
447	invalidQueries := []string{
448		"!!!",
449		"___",
450		"AND OR NOT NEAR",
451		`"***"`,
452	}
453
454	for _, q := range invalidQueries {
455		results, err := repo.Search(ctx, q, 10)
456		require.NoError(t, err, "query %q should return early without SQL errors", q)
457		assert.Empty(t, results, "query %q should have no valid tokens", q)
458	}
459}
460
461func TestHistoryRepository_Search_NoResults(t *testing.T) {
462	ctx := historyTestCtx()
463	dbPath := filepath.Join(t.TempDir(), "dumber.db")
464
465	db, err := sqlite.NewConnection(ctx, dbPath)
466	require.NoError(t, err)
467	t.Cleanup(func() { _ = db.Close() })
468
469	repo := sqlite.NewHistoryRepository(db)
470
471	// Save an entry
472	require.NoError(t, repo.Save(ctx, &entity.HistoryEntry{
473		URL:   "https://example.com",
474		Title: "Example",
475	}))
476
477	// Search for something that doesn't exist
478	results, err := repo.Search(ctx, "nonexistent", 10)
479	require.NoError(t, err)
480	assert.Empty(t, results)
481}
482
483func TestHistoryRepository_Search_PrefixMatching(t *testing.T) {
484	ctx := historyTestCtx()
485	dbPath := filepath.Join(t.TempDir(), "dumber.db")
486
487	db, err := sqlite.NewConnection(ctx, dbPath)
488	require.NoError(t, err)
489	t.Cleanup(func() { _ = db.Close() })
490
491	repo := sqlite.NewHistoryRepository(db)
492
493	// Save entries
494	entries := []*entity.HistoryEntry{
495		{URL: "https://github.com", Title: "GitHub"},
496		{URL: "https://gitlab.com", Title: "GitLab"},
497		{URL: "https://example.com", Title: "Example"},
498	}
499	for _, e := range entries {
500		require.NoError(t, repo.Save(ctx, e))
501	}
502
503	// Search for "git" - should find both github and gitlab (prefix match)
504	results, err := repo.Search(ctx, "git", 10)
505	require.NoError(t, err)
506	require.Len(t, results, 2)
507
508	// Verify both git* entries are found
509	urls := make(map[string]bool)
510	for _, r := range results {
511		urls[r.Entry.URL] = true
512	}
513	assert.True(t, urls["https://github.com"])
514	assert.True(t, urls["https://gitlab.com"])
515}
516
517func TestHistoryRepository_Search_DomainLikeQuery(t *testing.T) {
518	ctx := historyTestCtx()
519	dbPath := filepath.Join(t.TempDir(), "dumber.db")
520
521	db, err := sqlite.NewConnection(ctx, dbPath)
522	require.NoError(t, err)
523	t.Cleanup(func() { _ = db.Close() })
524
525	repo := sqlite.NewHistoryRepository(db)
526
527	// Save entries with domain-like URLs
528	entries := []*entity.HistoryEntry{
529		{URL: "https://gordon.bnema.dev/", Title: "Gordon - Self-hosted Deployment Platform"},
530		{URL: "https://example.com", Title: "Example Site"},
531		{URL: "https://other.gordon.io", Title: "Another Gordon Site"},
532	}
533	for _, e := range entries {
534		require.NoError(t, repo.Save(ctx, e))
535	}
536
537	// Search with period in query (domain-like) - periods should be treated as separators
538	// "gordon.bnem" should match "gordon.bnema.dev" (tokens: gordon, bnem -> gordon, bnema)
539	results, err := repo.Search(ctx, "gordon.bnem", 10)
540	require.NoError(t, err)
541	require.NotEmpty(t, results, "domain-like query 'gordon.bnem' should match gordon.bnema.dev")
542
543	// Verify the gordon.bnema.dev entry is found
544	found := false
545	for _, r := range results {
546		if r.Entry.URL == "https://gordon.bnema.dev/" {
547			found = true
548			break
549		}
550	}
551	assert.True(t, found, "gordon.bnema.dev should be in results")
552}
553
554func TestHistoryRepository_Search_SpecialCharacters(t *testing.T) {
555	ctx := historyTestCtx()
556	dbPath := filepath.Join(t.TempDir(), "dumber.db")
557
558	db, err := sqlite.NewConnection(ctx, dbPath)
559	require.NoError(t, err)
560	t.Cleanup(func() { _ = db.Close() })
561
562	repo := sqlite.NewHistoryRepository(db)
563
564	// Save entry
565	require.NoError(t, repo.Save(ctx, &entity.HistoryEntry{
566		URL:   "https://example.com/path",
567		Title: "Example Page",
568	}))
569
570	// Search with FTS5 special characters - should not cause errors
571	specialQueries := []string{
572		"example:",     // colon
573		"example*",     // asterisk
574		"example()",    // parentheses
575		`example"test`, // quotes
576		"example^",     // caret
577		"example-test", // hyphen
578		"example/",     // slash
579	}
580
581	for _, q := range specialQueries {
582		results, err := repo.Search(ctx, q, 10)
583		require.NoError(t, err, "query %q should not cause error", q)
584		// May or may not find results, but should not error
585		_ = results
586	}
587}
588
589func TestHistoryRepository_Search_IgnoresFTSOperatorsAsTokens(t *testing.T) {
590	ctx := historyTestCtx()
591	dbPath := filepath.Join(t.TempDir(), "dumber.db")
592
593	db, err := sqlite.NewConnection(ctx, dbPath)
594	require.NoError(t, err)
595	t.Cleanup(func() { _ = db.Close() })
596
597	repo := sqlite.NewHistoryRepository(db)
598
599	require.NoError(t, repo.Save(ctx, &entity.HistoryEntry{
600		URL:   "https://github.com/bnema/dumber",
601		Title: "Dumber",
602	}))
603
604	results, err := repo.Search(ctx, "AND github OR", 10)
605	require.NoError(t, err)
606	require.Len(t, results, 1)
607	assert.Equal(t, "https://github.com/bnema/dumber", results[0].Entry.URL)
608}
609
610func TestHistoryRepository_Search_SlashSeparatedQuery(t *testing.T) {
611	ctx := historyTestCtx()
612	dbPath := filepath.Join(t.TempDir(), "dumber.db")
613
614	db, err := sqlite.NewConnection(ctx, dbPath)
615	require.NoError(t, err)
616	t.Cleanup(func() { _ = db.Close() })
617
618	repo := sqlite.NewHistoryRepository(db)
619
620	require.NoError(t, repo.Save(ctx, &entity.HistoryEntry{
621		URL:   "https://github.com/bnema/dumber",
622		Title: "Dumber",
623	}))
624
625	results, err := repo.Search(ctx, "github.com/bnema", 10)
626	require.NoError(t, err)
627	require.NotEmpty(t, results, "slash-separated query should match history")
628}
629
630func TestHistoryRepository_AboutBlank_CappedVisitCount(t *testing.T) {
631	ctx := historyTestCtx()
632	dbPath := filepath.Join(t.TempDir(), "dumber.db")
633
634	db, err := sqlite.NewConnection(ctx, dbPath)
635	require.NoError(t, err)
636	t.Cleanup(func() { _ = db.Close() })
637
638	repo := sqlite.NewHistoryRepository(db)
639
640	// Save about:blank multiple times - visit count should stay at 1
641	for i := 0; i < 5; i++ {
642		require.NoError(t, repo.Save(ctx, &entity.HistoryEntry{
643			URL:   "about:blank",
644			Title: "New Tab",
645		}))
646	}
647
648	// Verify about:blank has visit_count = 1
649	found, err := repo.FindByURL(ctx, "about:blank")
650	require.NoError(t, err)
651	require.NotNil(t, found)
652	assert.Equal(t, int64(1), found.VisitCount, "about:blank should always have visit_count = 1")
653
654	// Try incrementing visit count directly - should be ignored
655	require.NoError(t, repo.IncrementVisitCount(ctx, "about:blank"))
656
657	found2, err := repo.FindByURL(ctx, "about:blank")
658	require.NoError(t, err)
659	assert.Equal(t, int64(1), found2.VisitCount, "about:blank increment should be ignored")
660}
661
662func TestHistoryRepository_AboutBlank_NotDominant(t *testing.T) {
663	ctx := historyTestCtx()
664	dbPath := filepath.Join(t.TempDir(), "dumber.db")
665
666	db, err := sqlite.NewConnection(ctx, dbPath)
667	require.NoError(t, err)
668	t.Cleanup(func() { _ = db.Close() })
669
670	repo := sqlite.NewHistoryRepository(db)
671
672	// Save about:blank many times
673	for i := 0; i < 10; i++ {
674		require.NoError(t, repo.Save(ctx, &entity.HistoryEntry{
675			URL:   "about:blank",
676			Title: "New Tab",
677		}))
678	}
679
680	// Save a regular entry with multiple visits
681	require.NoError(t, repo.Save(ctx, &entity.HistoryEntry{
682		URL:   "https://example.com",
683		Title: "Example",
684	}))
685	require.NoError(t, repo.IncrementVisitCount(ctx, "https://example.com"))
686	require.NoError(t, repo.IncrementVisitCount(ctx, "https://example.com"))
687
688	// Get most visited - example.com should be first (3 visits vs 1)
689	results, err := repo.GetAllMostVisited(ctx)
690	require.NoError(t, err)
691	require.Len(t, results, 2)
692
693	assert.Equal(t, "https://example.com", results[0].URL, "example.com should be first with higher visit count")
694	assert.Equal(t, int64(3), results[0].VisitCount)
695
696	assert.Equal(t, "about:blank", results[1].URL, "about:blank should be second with capped visit count")
697	assert.Equal(t, int64(1), results[1].VisitCount)
698}

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