v0.13.0 976176a

browser_fuzzy_test.go RawHistoryFolder

1package services
2
3import (
4	"context"
5	"database/sql"
6	"testing"
7	"time"
8
9	"github.com/bnema/dumber/internal/config"
10	"github.com/bnema/dumber/internal/db"
11	mock_db "github.com/bnema/dumber/internal/db/mocks"
12	"go.uber.org/mock/gomock"
13)
14
15// TestBrowserService_FuzzyCacheIntegration tests the fuzzy cache lifecycle in BrowserService
16func TestBrowserService_FuzzyCacheIntegration(t *testing.T) {
17	ctrl := gomock.NewController(t)
18	defer ctrl.Finish()
19
20	mockDB := mock_db.NewMockDatabaseQuerier(ctrl)
21	ctx := context.Background()
22
23	// Mock test history data
24	testHistory := []db.History{
25		{
26			ID:  1,
27			Url: "https://github.com",
28			Title: sql.NullString{
29				String: "GitHub",
30				Valid:  true,
31			},
32			VisitCount: sql.NullInt64{Int64: 5, Valid: true},
33		},
34		{
35			ID:  2,
36			Url: "https://google.com",
37			Title: sql.NullString{
38				String: "Google",
39				Valid:  true,
40			},
41			VisitCount: sql.NullInt64{Int64: 10, Valid: true},
42		},
43		{
44			ID:  3,
45			Url: "https://stackoverflow.com",
46			Title: sql.NullString{
47				String: "Stack Overflow",
48				Valid:  true,
49			},
50			VisitCount: sql.NullInt64{Int64: 3, Valid: true},
51		},
52	}
53
54	// Expect GetHistory to be called when loading fuzzy cache
55	mockDB.EXPECT().
56		GetHistory(gomock.Any(), gomock.Any()).
57		Return(testHistory, nil).
58		MinTimes(1)
59
60	// Create browser service (fuzzy cache initialized)
61	cfg := &config.Config{}
62	service := NewBrowserService(cfg, mockDB)
63
64	// Test 1: Verify fuzzy cache was initialized
65	if service.fuzzyCache == nil {
66		t.Fatal("Fuzzy cache should be initialized")
67	}
68
69	// Test 2: Load fuzzy cache from DB
70	err := service.LoadFuzzyCacheFromDB(ctx)
71	if err != nil {
72		t.Fatalf("Failed to load fuzzy cache: %v", err)
73	}
74
75	// Test 3: Verify cache has entries
76	stats := service.fuzzyCache.Stats()
77	if stats.EntryCount == 0 {
78		t.Error("Fuzzy cache should have entries after loading")
79	}
80
81	t.Logf("Fuzzy cache loaded: %d entries, %d trigrams", stats.EntryCount, stats.TrigramCount)
82
83	// Test 4: Verify refresh tracking is initialized
84	if service.historySinceRefresh != 0 {
85		t.Error("historySinceRefresh should start at 0")
86	}
87
88	if service.lastCacheRefresh.IsZero() {
89		t.Error("lastCacheRefresh should be initialized")
90	}
91
92	// Test 5: Flush all caches (should include fuzzy cache)
93	err = service.FlushAllCaches(ctx)
94	if err != nil {
95		t.Fatalf("Failed to flush caches: %v", err)
96	}
97
98	t.Log("Fuzzy cache integration test passed")
99}
100
101// TestBrowserService_FuzzyCacheSmartRefresh tests the smart refresh logic
102func TestBrowserService_FuzzyCacheSmartRefresh(t *testing.T) {
103	ctrl := gomock.NewController(t)
104	defer ctrl.Finish()
105
106	mockDB := mock_db.NewMockDatabaseQuerier(ctrl)
107	cfg := &config.Config{}
108
109	// Expect GetHistory to be called when loading fuzzy cache
110	mockDB.EXPECT().
111		GetHistory(gomock.Any(), gomock.Any()).
112		Return([]db.History{}, nil).
113		AnyTimes()
114
115	service := NewBrowserService(cfg, mockDB)
116
117	// Load initial cache
118	ctx := context.Background()
119	err := service.LoadFuzzyCacheFromDB(ctx)
120	if err != nil {
121		t.Fatalf("Failed to load fuzzy cache: %v", err)
122	}
123
124	// Test 1: Verify initial state
125	if service.historySinceRefresh != 0 {
126		t.Error("historySinceRefresh should start at 0")
127	}
128
129	initialRefreshTime := service.lastCacheRefresh
130
131	// Test 2: Simulate adding less than threshold entries (< 10)
132	service.historySinceRefresh = 5
133	service.lastCacheRefresh = time.Now().Add(-10 * time.Minute) // Old enough
134
135	// This should NOT trigger refresh (not enough entries)
136	// Manually check the condition
137	const minEntries = 10
138	const minInterval = 5 * time.Minute
139
140	shouldRefresh := service.historySinceRefresh >= minEntries &&
141		time.Since(service.lastCacheRefresh) >= minInterval
142
143	if shouldRefresh {
144		t.Error("Should not trigger refresh with only 5 entries")
145	}
146
147	// Test 3: Simulate enough entries but too recent
148	service.historySinceRefresh = 15
149	service.lastCacheRefresh = time.Now() // Just refreshed
150
151	shouldRefresh = service.historySinceRefresh >= minEntries &&
152		time.Since(service.lastCacheRefresh) >= minInterval
153
154	if shouldRefresh {
155		t.Error("Should not trigger refresh when last refresh was too recent")
156	}
157
158	// Test 4: Simulate conditions that SHOULD trigger refresh
159	service.historySinceRefresh = 15
160	service.lastCacheRefresh = time.Now().Add(-10 * time.Minute)
161
162	shouldRefresh = service.historySinceRefresh >= minEntries &&
163		time.Since(service.lastCacheRefresh) >= minInterval
164
165	if !shouldRefresh {
166		t.Error("Should trigger refresh with 15 entries and 10 minutes passed")
167	}
168
169	t.Logf("Smart refresh logic validated: %d entries, %v since refresh, should refresh: %v",
170		service.historySinceRefresh,
171		time.Since(service.lastCacheRefresh),
172		shouldRefresh)
173
174	// Test 5: Verify counter would reset after refresh
175	if shouldRefresh {
176		// Simulate the reset that happens in the actual code
177		service.historySinceRefresh = 0
178		service.lastCacheRefresh = time.Now()
179
180		if service.historySinceRefresh != 0 {
181			t.Error("Counter should reset after refresh")
182		}
183	}
184
185	// Test 6: Verify old refresh time was updated
186	if !service.lastCacheRefresh.After(initialRefreshTime) {
187		t.Error("Refresh time should be updated")
188	}
189
190	t.Log("Smart refresh logic test passed")
191}
192
193// TestBrowserService_FuzzyCacheThresholds tests the threshold constants
194func TestBrowserService_FuzzyCacheThresholds(t *testing.T) {
195	// This test documents and validates the threshold values
196
197	const minEntries = 10               // From the actual code
198	const minInterval = 5 * time.Minute // From the actual code
199
200	t.Logf("Fuzzy cache refresh thresholds:")
201	t.Logf("  - Minimum entries: %d", minEntries)
202	t.Logf("  - Minimum interval: %v", minInterval)
203
204	// Test reasonable threshold values
205	if minEntries < 5 {
206		t.Error("minEntries too low - will refresh too frequently")
207	}
208
209	if minEntries > 100 {
210		t.Error("minEntries too high - cache will be stale")
211	}
212
213	if minInterval < 1*time.Minute {
214		t.Error("minInterval too low - will refresh too frequently")
215	}
216
217	if minInterval > 30*time.Minute {
218		t.Error("minInterval too high - cache will be stale")
219	}
220
221	// Test that both conditions are required
222	// This prevents:
223	// - Refreshing after 1 entry but 10 minutes (would be wasteful)
224	// - Refreshing after 100 entries but 1 second (would be too frequent)
225
226	t.Log("Threshold validation passed - both conditions required for refresh")
227}
228
229// TestBrowserService_FuzzyCacheLoadFromDB tests loading cache at startup
230func TestBrowserService_FuzzyCacheLoadFromDB(t *testing.T) {
231	ctrl := gomock.NewController(t)
232	defer ctrl.Finish()
233
234	mockDB := mock_db.NewMockDatabaseQuerier(ctrl)
235	ctx := context.Background()
236
237	// Mock test history entries
238	testHistory := []db.History{
239		{
240			ID:         1,
241			Url:        "https://github.com/user/repo",
242			Title:      sql.NullString{String: "GitHub Repository", Valid: true},
243			VisitCount: sql.NullInt64{Int64: 10, Valid: true},
244		},
245		{
246			ID:         2,
247			Url:        "https://stackoverflow.com/questions/123",
248			Title:      sql.NullString{String: "Stack Overflow Question", Valid: true},
249			VisitCount: sql.NullInt64{Int64: 5, Valid: true},
250		},
251		{
252			ID:         3,
253			Url:        "https://golang.org/doc",
254			Title:      sql.NullString{String: "Go Documentation", Valid: true},
255			VisitCount: sql.NullInt64{Int64: 15, Valid: true},
256		},
257		{
258			ID:         4,
259			Url:        "https://reddit.com/r/golang",
260			Title:      sql.NullString{String: "Golang Subreddit", Valid: true},
261			VisitCount: sql.NullInt64{Int64: 3, Valid: true},
262		},
263		{
264			ID:         5,
265			Url:        "https://news.ycombinator.com",
266			Title:      sql.NullString{String: "Hacker News", Valid: true},
267			VisitCount: sql.NullInt64{Int64: 20, Valid: true},
268		},
269	}
270
271	// Expect GetHistory to be called when loading fuzzy cache
272	mockDB.EXPECT().
273		GetHistory(gomock.Any(), gomock.Any()).
274		Return(testHistory, nil).
275		MinTimes(1)
276
277	// Create service and load cache
278	cfg := &config.Config{}
279	service := NewBrowserService(cfg, mockDB)
280
281	// Load cache from DB
282	err := service.LoadFuzzyCacheFromDB(ctx)
283	if err != nil {
284		t.Fatalf("Failed to load fuzzy cache from DB: %v", err)
285	}
286
287	// Verify cache has loaded entries
288	stats := service.fuzzyCache.Stats()
289	if stats.EntryCount < len(testHistory) {
290		t.Errorf("Expected at least %d entries, got %d", len(testHistory), stats.EntryCount)
291	}
292
293	if stats.TrigramCount == 0 {
294		t.Error("Expected trigram index to be built")
295	}
296
297	t.Logf("Loaded fuzzy cache: %d entries, %d trigrams", stats.EntryCount, stats.TrigramCount)
298
299	// Test search functionality
300	result, err := service.fuzzyCache.Search(ctx, "github")
301	if err != nil {
302		t.Fatalf("Failed to search fuzzy cache: %v", err)
303	}
304
305	if len(result.Matches) == 0 {
306		t.Error("Expected to find matches for 'github'")
307	}
308
309	t.Logf("Search for 'github' found %d matches", len(result.Matches))
310
311	t.Log("Load from DB test passed")
312}

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