v0.29.0 21b3150

browser_launch_relay_test.go RawHistoryFolder

1package desktop
2
3import (
4	"context"
5	"encoding/json"
6	"net"
7	"os"
8	"path/filepath"
9	"testing"
10	"time"
11
12	"github.com/bnema/dumber/internal/infrastructure/runtimeprofile"
13	"github.com/stretchr/testify/assert"
14	"github.com/stretchr/testify/require"
15)
16
17type browserWindowOpenerFunc func(context.Context, string) error
18
19func (f browserWindowOpenerFunc) OpenFreshWindow(ctx context.Context, url string) error {
20	return f(ctx, url)
21}
22
23func waitForSocket(t *testing.T, path string) {
24	t.Helper()
25
26	require.Eventually(t, func() bool {
27		_, err := os.Stat(path)
28		return err == nil
29	}, time.Second, 10*time.Millisecond)
30}
31
32func waitForSocketGone(t *testing.T, path string) {
33	t.Helper()
34
35	require.Eventually(t, func() bool {
36		_, err := os.Stat(path)
37		return os.IsNotExist(err)
38	}, time.Second, 10*time.Millisecond)
39}
40
41func shortTempDir(t *testing.T) string {
42	t.Helper()
43
44	dir, err := os.MkdirTemp("", "dbr-")
45	require.NoError(t, err)
46	t.Cleanup(func() {
47		_ = os.RemoveAll(dir)
48	})
49
50	return dir
51}
52
53func testIPC(root string) runtimeprofile.IPCPaths {
54	runtimeDir := filepath.Join(root, "runtime")
55	return runtimeprofile.IPCPaths{
56		RuntimeDir:          runtimeDir,
57		BrowserLaunchSocket: filepath.Join(runtimeDir, browserLaunchSocketName),
58	}
59}
60
61func testNamespacedIPC(root, engine string) runtimeprofile.IPCPaths {
62	runtimeDir := filepath.Join(root, "runtime", engine)
63	return runtimeprofile.IPCPaths{
64		RuntimeDir:          runtimeDir,
65		BrowserLaunchSocket: filepath.Join(runtimeDir, browserLaunchSocketName),
66	}
67}
68
69func TestBrowserLaunchRelay_DeliverOpenFreshWindow_MissingListenerReturnsFalseNil(t *testing.T) {
70	relay := NewBrowserLaunchRelay(testIPC(shortTempDir(t)))
71
72	delivered, err := relay.DeliverOpenFreshWindow(context.Background(), "https://example.com")
73
74	require.NoError(t, err)
75	assert.False(t, delivered)
76}
77
78func TestBrowserLaunchRelay_SameProfileAndEngine_UsesSameSocket(t *testing.T) {
79	ipc := testNamespacedIPC(shortTempDir(t), "cef")
80	listenerRelay := NewBrowserLaunchRelay(ipc)
81	clientRelay := NewBrowserLaunchRelay(ipc)
82
83	received := make(chan string, 1)
84	ctx, cancel := context.WithCancel(context.Background())
85	defer cancel()
86
87	closer, err := listenerRelay.Listen(ctx, browserWindowOpenerFunc(func(_ context.Context, url string) error {
88		received <- url
89		return nil
90	}))
91	require.NoError(t, err)
92	defer closer.Close()
93
94	waitForSocket(t, ipc.BrowserLaunchSocket)
95
96	delivered, err := clientRelay.DeliverOpenFreshWindow(context.Background(), "https://example.com/same-namespace")
97	require.NoError(t, err)
98	require.True(t, delivered)
99	require.Equal(t, "https://example.com/same-namespace", <-received)
100}
101
102func TestBrowserLaunchRelay_DevCEFAndDevWebKit_UseDifferentSockets(t *testing.T) {
103	root := t.TempDir()
104	cefIPC := testNamespacedIPC(root, "cef")
105	wkIPC := testNamespacedIPC(root, "webkit")
106	require.NotEqual(t, cefIPC.BrowserLaunchSocket, wkIPC.BrowserLaunchSocket)
107}
108
109func TestBrowserLaunchRelay_DeliverOpenFreshWindow_RoundTrip(t *testing.T) {
110	ipc := testIPC(shortTempDir(t))
111	relay := NewBrowserLaunchRelay(ipc)
112
113	received := make(chan string, 1)
114	ctx, cancel := context.WithCancel(context.Background())
115	defer cancel()
116
117	closer, err := relay.Listen(ctx, browserWindowOpenerFunc(func(_ context.Context, url string) error {
118		received <- url
119		return nil
120	}))
121	require.NoError(t, err)
122	defer closer.Close()
123
124	waitForSocket(t, ipc.BrowserLaunchSocket)
125
126	delivered, err := relay.DeliverOpenFreshWindow(context.Background(), "https://example.com/new")
127
128	require.NoError(t, err)
129	assert.True(t, delivered)
130
131	select {
132	case got := <-received:
133		assert.Equal(t, "https://example.com/new", got)
134	case <-time.After(time.Second):
135		t.Fatal("expected opener to receive the URL")
136	}
137}
138
139func TestBrowserLaunchRelay_RequiresBrowserLaunchSocketPath(t *testing.T) {
140	relay := NewBrowserLaunchRelay(runtimeprofile.IPCPaths{})
141
142	_, err := relay.Listen(context.Background(), browserWindowOpenerFunc(func(context.Context, string) error { return nil }))
143	require.Error(t, err)
144	require.Contains(t, err.Error(), "browser launch socket path")
145}
146
147func TestBrowserLaunchRelay_SecondListenWhileLiveFailsWithoutReplacingListener(t *testing.T) {
148	ipc := testIPC(shortTempDir(t))
149	relay := NewBrowserLaunchRelay(ipc)
150
151	firstReceived := make(chan string, 1)
152	ctx, cancel := context.WithCancel(context.Background())
153	defer cancel()
154
155	closer, err := relay.Listen(ctx, browserWindowOpenerFunc(func(_ context.Context, url string) error {
156		firstReceived <- url
157		return nil
158	}))
159	require.NoError(t, err)
160	defer closer.Close()
161
162	waitForSocket(t, ipc.BrowserLaunchSocket)
163
164	_, err = relay.Listen(ctx, browserWindowOpenerFunc(func(context.Context, string) error { return nil }))
165	require.Error(t, err)
166
167	delivered, err := relay.DeliverOpenFreshWindow(context.Background(), "https://example.com/live")
168	require.NoError(t, err)
169	require.True(t, delivered)
170
171	select {
172	case got := <-firstReceived:
173		assert.Equal(t, "https://example.com/live", got)
174	case <-time.After(time.Second):
175		t.Fatal("expected the original listener to remain active")
176	}
177}
178
179func TestBrowserLaunchRelay_RebindsStaleSocketPath(t *testing.T) {
180	ipc := testIPC(shortTempDir(t))
181	relay := NewBrowserLaunchRelay(ipc)
182
183	require.NoError(t, os.MkdirAll(filepath.Dir(ipc.BrowserLaunchSocket), 0o700))
184	require.NoError(t, os.WriteFile(ipc.BrowserLaunchSocket, []byte("stale"), 0o600))
185
186	received := make(chan string, 1)
187	ctx, cancel := context.WithCancel(context.Background())
188	defer cancel()
189
190	closer, err := relay.Listen(ctx, browserWindowOpenerFunc(func(_ context.Context, url string) error {
191		received <- url
192		return nil
193	}))
194	require.NoError(t, err)
195	defer closer.Close()
196
197	waitForSocket(t, ipc.BrowserLaunchSocket)
198
199	delivered, err := relay.DeliverOpenFreshWindow(context.Background(), "https://example.com/stale")
200	require.NoError(t, err)
201	require.True(t, delivered)
202
203	select {
204	case got := <-received:
205		assert.Equal(t, "https://example.com/stale", got)
206	case <-time.After(time.Second):
207		t.Fatal("expected stale socket path to be rebound")
208	}
209}
210
211func TestBrowserLaunchRelay_RejectsMalformedPayloads(t *testing.T) {
212	ipc := testIPC(shortTempDir(t))
213	relay := NewBrowserLaunchRelay(ipc)
214
215	received := make(chan string, 1)
216	ctx, cancel := context.WithCancel(context.Background())
217	defer cancel()
218
219	closer, err := relay.Listen(ctx, browserWindowOpenerFunc(func(_ context.Context, url string) error {
220		received <- url
221		return nil
222	}))
223	require.NoError(t, err)
224	defer closer.Close()
225
226	waitForSocket(t, ipc.BrowserLaunchSocket)
227
228	conn, err := net.Dial("unix", ipc.BrowserLaunchSocket)
229	require.NoError(t, err)
230
231	_, err = conn.Write([]byte("not-json"))
232	require.NoError(t, err)
233	require.NoError(t, conn.Close())
234
235	delivered, err := relay.DeliverOpenFreshWindow(context.Background(), "https://example.com/still-works")
236
237	require.NoError(t, err)
238	assert.True(t, delivered)
239
240	select {
241	case got := <-received:
242		assert.Equal(t, "https://example.com/still-works", got)
243	case <-time.After(time.Second):
244		t.Fatal("expected opener to receive the URL after malformed payload")
245	}
246}
247
248func TestBrowserLaunchRelay_CloseRemovesSocketPath(t *testing.T) {
249	ipc := testIPC(shortTempDir(t))
250	relay := NewBrowserLaunchRelay(ipc)
251
252	ctx, cancel := context.WithCancel(context.Background())
253	defer cancel()
254
255	closer, err := relay.Listen(ctx, browserWindowOpenerFunc(func(context.Context, string) error { return nil }))
256	require.NoError(t, err)
257
258	waitForSocket(t, ipc.BrowserLaunchSocket)
259
260	require.NoError(t, closer.Close())
261	waitForSocketGone(t, ipc.BrowserLaunchSocket)
262}
263
264func TestBrowserLaunchRelay_ContextCancelStopsServing(t *testing.T) {
265	ipc := testIPC(shortTempDir(t))
266	relay := NewBrowserLaunchRelay(ipc)
267
268	ctx, cancel := context.WithCancel(context.Background())
269	closer, err := relay.Listen(ctx, browserWindowOpenerFunc(func(context.Context, string) error { return nil }))
270	require.NoError(t, err)
271
272	waitForSocket(t, ipc.BrowserLaunchSocket)
273
274	cancel()
275	waitForSocketGone(t, ipc.BrowserLaunchSocket)
276	require.NoError(t, closer.Close())
277
278	delivered, err := relay.DeliverOpenFreshWindow(context.Background(), "https://example.com/after-cancel")
279	require.NoError(t, err)
280	assert.False(t, delivered)
281}
282
283func TestBrowserLaunchRelay_DeliverOpenFreshWindow_RespectsResponseReadTimeout(t *testing.T) {
284	ipc := testIPC(shortTempDir(t))
285	require.NoError(t, os.MkdirAll(ipc.RuntimeDir, 0o700))
286
287	listener, err := net.ListenUnix("unix", &net.UnixAddr{Name: ipc.BrowserLaunchSocket, Net: "unix"})
288	require.NoError(t, err)
289	defer func() {
290		_ = listener.Close()
291		_ = os.Remove(ipc.BrowserLaunchSocket)
292	}()
293
294	ready := make(chan struct{})
295	go func() {
296		close(ready)
297		conn, acceptErr := listener.AcceptUnix()
298		if acceptErr != nil {
299			return
300		}
301		defer conn.Close()
302		<-time.After(time.Second)
303	}()
304	<-ready
305
306	relay := NewBrowserLaunchRelay(ipc)
307	ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
308	defer cancel()
309
310	delivered, err := relay.DeliverOpenFreshWindow(ctx, "https://example.com/slow")
311	require.Error(t, err)
312	require.ErrorIs(t, err, context.DeadlineExceeded)
313	assert.False(t, delivered)
314}
315
316func TestBrowserLaunchRelay_DeliverOpenFreshWindow_ReturnsUnconfirmedErrorOnNoDeadlineTimeout(t *testing.T) {
317	ipc := testIPC(shortTempDir(t))
318	require.NoError(t, os.MkdirAll(ipc.RuntimeDir, 0o700))
319
320	listener, err := net.ListenUnix("unix", &net.UnixAddr{Name: ipc.BrowserLaunchSocket, Net: "unix"})
321	require.NoError(t, err)
322	defer func() {
323		_ = listener.Close()
324		_ = os.Remove(ipc.BrowserLaunchSocket)
325	}()
326
327	ready := make(chan struct{})
328	go func() {
329		close(ready)
330		conn, acceptErr := listener.AcceptUnix()
331		if acceptErr != nil {
332			return
333		}
334		defer conn.Close()
335		<-time.After(time.Second)
336	}()
337	<-ready
338
339	relay := NewBrowserLaunchRelay(ipc)
340
341	started := make(chan struct{})
342
343	type deliverResult struct {
344		delivered bool
345		err       error
346	}
347	result := make(chan deliverResult, 1)
348	go func() {
349		close(started)
350		delivered, err := relay.DeliverOpenFreshWindow(context.Background(), "https://example.com/slow")
351		result <- deliverResult{delivered: delivered, err: err}
352	}()
353	<-started
354
355	select {
356	case got := <-result:
357		assert.True(t, got.delivered)
358		require.Error(t, got.err)
359		require.ErrorIs(t, got.err, ErrBrowserLaunchRelayUnconfirmed)
360	case <-time.After(browserLaunchIOTimeout * 3):
361		t.Fatal("DeliverOpenFreshWindow blocked without a caller deadline")
362	}
363}
364
365func TestBrowserLaunchRelay_AcknowledgesBeforeOpenerReturns(t *testing.T) {
366	ipc := testIPC(shortTempDir(t))
367	relay := NewBrowserLaunchRelay(ipc)
368
369	started := make(chan struct{})
370	release := make(chan struct{})
371	ctx, cancel := context.WithCancel(context.Background())
372	defer cancel()
373
374	closer, err := relay.Listen(ctx, browserWindowOpenerFunc(func(_ context.Context, _ string) error {
375		close(started)
376		<-release
377		return nil
378	}))
379	require.NoError(t, err)
380	defer closer.Close()
381
382	waitForSocket(t, ipc.BrowserLaunchSocket)
383
384	conn, err := net.Dial("unix", ipc.BrowserLaunchSocket)
385	require.NoError(t, err)
386	defer conn.Close()
387	require.NoError(t, conn.SetDeadline(time.Now().Add(300*time.Millisecond)))
388	require.NoError(t, json.NewEncoder(conn).Encode(browserLaunchRequest{URL: "https://example.com/slow-but-healthy"}))
389
390	var response browserLaunchResponse
391	require.NoError(t, json.NewDecoder(conn).Decode(&response))
392	assert.Empty(t, response.Error)
393
394	select {
395	case <-started:
396	case <-time.After(time.Second):
397		t.Fatal("expected opener to be invoked after acknowledgement")
398	}
399	close(release)
400}
401
402func TestBrowserLaunchRelay_SilentClientDoesNotStallListener(t *testing.T) {
403	ipc := testIPC(shortTempDir(t))
404	relay := NewBrowserLaunchRelay(ipc)
405
406	received := make(chan string, 1)
407	ctx, cancel := context.WithCancel(context.Background())
408	defer cancel()
409
410	closer, err := relay.Listen(ctx, browserWindowOpenerFunc(func(_ context.Context, url string) error {
411		received <- url
412		return nil
413	}))
414	require.NoError(t, err)
415	defer closer.Close()
416
417	waitForSocket(t, ipc.BrowserLaunchSocket)
418
419	conn, err := net.Dial("unix", ipc.BrowserLaunchSocket)
420	require.NoError(t, err)
421	defer conn.Close()
422	time.Sleep(20 * time.Millisecond)
423
424	type deliverResult struct {
425		delivered bool
426		err       error
427	}
428	result := make(chan deliverResult, 1)
429	go func() {
430		delivered, err := relay.DeliverOpenFreshWindow(context.Background(), "https://example.com/recovered")
431		result <- deliverResult{delivered: delivered, err: err}
432	}()
433
434	select {
435	case got := <-result:
436		require.NoError(t, got.err)
437		require.True(t, got.delivered)
438	case <-time.After(300 * time.Millisecond):
439		t.Fatal("silent client stalled the relay listener")
440	}
441
442	select {
443	case got := <-received:
444		assert.Equal(t, "https://example.com/recovered", got)
445	case <-time.After(time.Second):
446		t.Fatal("expected opener to receive the URL")
447	}
448}

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