browser_launch_relay_test.go RawHistoryFolder

1package desktop
2
3import (
4	"context"
5	"encoding/json"
6	"errors"
7	"net"
8	"os"
9	"path/filepath"
10	"testing"
11	"time"
12
13	"github.com/bnema/dumber/internal/application/port"
14	"github.com/bnema/dumber/internal/infrastructure/process"
15	"github.com/bnema/dumber/internal/infrastructure/runtimeprofile"
16	"github.com/stretchr/testify/assert"
17	"github.com/stretchr/testify/require"
18)
19
20type browserWindowOpenerFunc func(context.Context, string) error
21
22func (f browserWindowOpenerFunc) OpenExternalURL(ctx context.Context, url string) error {
23	return f(ctx, url)
24}
25
26func (f browserWindowOpenerFunc) OpenFreshWindow(ctx context.Context, url string) error {
27	return f(ctx, url)
28}
29
30type browserWindowOpener struct {
31	openExternalURL func(context.Context, string) error
32	openFreshWindow func(context.Context, string) error
33}
34
35func (o browserWindowOpener) OpenExternalURL(ctx context.Context, url string) error {
36	return o.openExternalURL(ctx, url)
37}
38
39func (o browserWindowOpener) OpenFreshWindow(ctx context.Context, url string) error {
40	return o.openFreshWindow(ctx, url)
41}
42
43func waitForSocket(t *testing.T, path string) {
44	t.Helper()
45
46	require.Eventually(t, func() bool {
47		_, err := os.Stat(path)
48		return err == nil
49	}, time.Second, 10*time.Millisecond)
50}
51
52func waitForSocketGone(t *testing.T, path string) {
53	t.Helper()
54
55	require.Eventually(t, func() bool {
56		_, err := os.Stat(path)
57		return os.IsNotExist(err)
58	}, time.Second, 10*time.Millisecond)
59}
60
61func shortTempDir(t *testing.T) string {
62	t.Helper()
63
64	dir, err := os.MkdirTemp("", "dbr-")
65	require.NoError(t, err)
66	t.Cleanup(func() {
67		_ = os.RemoveAll(dir)
68	})
69
70	return dir
71}
72
73func TestValidateBrowserLaunchSocketDirOwnedAcceptsCurrentUser(t *testing.T) {
74	ipc := testIPC(shortTempDir(t))
75	require.NoError(t, os.MkdirAll(filepath.Dir(ipc.BrowserLaunchSocket), 0o700))
76
77	require.NoError(t, validateBrowserLaunchSocketDirOwned(ipc.BrowserLaunchSocket, uint32(os.Geteuid())))
78}
79
80func TestValidateBrowserLaunchSocketDirOwnedRejectsUnexpectedOwner(t *testing.T) {
81	ipc := testIPC(shortTempDir(t))
82	require.NoError(t, os.MkdirAll(filepath.Dir(ipc.BrowserLaunchSocket), 0o700))
83
84	unexpectedUID := uint32(os.Geteuid() + 1)
85	if unexpectedUID == uint32(os.Geteuid()) {
86		unexpectedUID++
87	}
88
89	err := validateBrowserLaunchSocketDirOwned(ipc.BrowserLaunchSocket, unexpectedUID)
90	require.Error(t, err)
91	require.Contains(t, err.Error(), "browser launch dir owner")
92}
93
94func TestValidateBrowserLaunchSocketDirOwnedRejectsWritableUserParent(t *testing.T) {
95	root := shortTempDir(t)
96	ipc := runtimeprofile.IPCPaths{
97		RuntimeDir:          filepath.Join(root, "dumber", "dev-abc123", "cef"),
98		BrowserLaunchSocket: filepath.Join(root, "dumber", "dev-abc123", "cef", browserLaunchSocketName),
99	}
100	require.NoError(t, os.MkdirAll(filepath.Dir(ipc.BrowserLaunchSocket), 0o700))
101	require.NoError(t, os.Chmod(filepath.Join(root, "dumber"), 0o777))
102	t.Cleanup(func() {
103		_ = os.Chmod(filepath.Join(root, "dumber"), 0o700)
104	})
105
106	err := validateBrowserLaunchSocketDirOwned(ipc.BrowserLaunchSocket, uint32(os.Geteuid()))
107	require.Error(t, err)
108	require.Contains(t, err.Error(), "browser launch dir permissions")
109}
110
111func testIPC(root string) runtimeprofile.IPCPaths {
112	runtimeDir := filepath.Join(root, "runtime")
113	return runtimeprofile.IPCPaths{
114		RuntimeDir:          runtimeDir,
115		BrowserLaunchSocket: filepath.Join(runtimeDir, browserLaunchSocketName),
116	}
117}
118
119func testNamespacedIPC(root, engine string) runtimeprofile.IPCPaths {
120	runtimeDir := filepath.Join(root, "runtime", engine)
121	return runtimeprofile.IPCPaths{
122		RuntimeDir:          runtimeDir,
123		BrowserLaunchSocket: filepath.Join(runtimeDir, browserLaunchSocketName),
124	}
125}
126
127func overrideBrowserLaunchRequestID(t *testing.T, id string) func() {
128	t.Helper()
129	previous := newBrowserLaunchRequestID
130	newBrowserLaunchRequestID = func() string { return id }
131	return func() { newBrowserLaunchRequestID = previous }
132}
133
134func TestBrowserLaunchRelay_DeliverOpenExternalURL_MissingListenerReturnsFalseNil(t *testing.T) {
135	relay := NewBrowserLaunchRelay(testIPC(shortTempDir(t)))
136
137	delivered, err := relay.DeliverOpenExternalURL(context.Background(), "https://example.com")
138
139	require.NoError(t, err)
140	assert.False(t, delivered)
141}
142
143func TestBrowserLaunchRelay_SameProfileAndEngine_UsesSameSocket(t *testing.T) {
144	ipc := testNamespacedIPC(shortTempDir(t), "cef")
145	listenerRelay := NewBrowserLaunchRelay(ipc)
146	clientRelay := NewBrowserLaunchRelay(ipc)
147
148	received := make(chan string, 1)
149	ctx := t.Context()
150
151	closer, err := listenerRelay.Listen(ctx, browserWindowOpenerFunc(func(_ context.Context, url string) error {
152		received <- url
153		return nil
154	}))
155	require.NoError(t, err)
156	defer closer.Close()
157
158	waitForSocket(t, ipc.BrowserLaunchSocket)
159
160	delivered, err := clientRelay.DeliverOpenExternalURL(context.Background(), "https://example.com/same-namespace")
161	require.NoError(t, err)
162	require.True(t, delivered)
163	require.Equal(t, "https://example.com/same-namespace", <-received)
164}
165
166func TestBrowserLaunchRelay_DevCEFAndDevWebKit_UseDifferentSockets(t *testing.T) {
167	root := t.TempDir()
168	cefIPC := testNamespacedIPC(root, "cef")
169	wkIPC := testNamespacedIPC(root, "webkit")
170	require.NotEqual(t, cefIPC.BrowserLaunchSocket, wkIPC.BrowserLaunchSocket)
171}
172
173func TestBrowserLaunchRelay_DeliverOpenExternalURL_SendsRequestIDAndAcceptsMatchingResponse(t *testing.T) {
174	ipc := testIPC(shortTempDir(t))
175	relay := NewBrowserLaunchRelay(ipc)
176	restore := overrideBrowserLaunchRequestID(t, "request-test-1")
177	defer restore()
178
179	receivedRequest := make(chan browserLaunchRequest, 1)
180	require.NoError(t, os.MkdirAll(ipc.RuntimeDir, 0o700))
181	listener, err := net.ListenUnix("unix", &net.UnixAddr{Name: ipc.BrowserLaunchSocket, Net: "unix"})
182	require.NoError(t, err)
183	defer func() {
184		_ = listener.Close()
185		_ = os.Remove(ipc.BrowserLaunchSocket)
186	}()
187
188	go func() {
189		conn, acceptErr := listener.AcceptUnix()
190		if acceptErr != nil {
191			return
192		}
193		defer conn.Close()
194		var request browserLaunchRequest
195		if decodeErr := json.NewDecoder(conn).Decode(&request); decodeErr != nil {
196			return
197		}
198		receivedRequest <- request
199		_ = json.NewEncoder(conn).Encode(browserLaunchResponse{RequestID: request.RequestID, Accepted: true})
200	}()
201
202	delivered, err := relay.DeliverOpenExternalURL(context.Background(), "https://example.com/request-id")
203
204	require.NoError(t, err)
205	require.True(t, delivered)
206	request := <-receivedRequest
207	require.Equal(t, "request-test-1", request.RequestID)
208	require.Equal(t, "https://example.com/request-id", request.URL)
209}
210
211func TestBrowserLaunchRelay_DeliverOpenExternalURL_RejectsMismatchedResponseRequestID(t *testing.T) {
212	ipc := testIPC(shortTempDir(t))
213	relay := NewBrowserLaunchRelay(ipc)
214	restore := overrideBrowserLaunchRequestID(t, "request-test-2")
215	defer restore()
216
217	require.NoError(t, os.MkdirAll(ipc.RuntimeDir, 0o700))
218	listener, err := net.ListenUnix("unix", &net.UnixAddr{Name: ipc.BrowserLaunchSocket, Net: "unix"})
219	require.NoError(t, err)
220	defer func() {
221		_ = listener.Close()
222		_ = os.Remove(ipc.BrowserLaunchSocket)
223	}()
224
225	go func() {
226		conn, acceptErr := listener.AcceptUnix()
227		if acceptErr != nil {
228			return
229		}
230		defer conn.Close()
231		var request browserLaunchRequest
232		if decodeErr := json.NewDecoder(conn).Decode(&request); decodeErr != nil {
233			return
234		}
235		_ = json.NewEncoder(conn).Encode(browserLaunchResponse{RequestID: "different-request", Accepted: true})
236	}()
237
238	delivered, err := relay.DeliverOpenExternalURL(context.Background(), "https://example.com/request-id-mismatch")
239
240	require.Error(t, err)
241	require.True(t, delivered)
242	require.Contains(t, err.Error(), "mismatched browser launch relay response")
243}
244
245func TestBrowserLaunchRelay_DeliverOpenExternalURL_RoundTrip(t *testing.T) {
246	ipc := testIPC(shortTempDir(t))
247	relay := NewBrowserLaunchRelay(ipc)
248
249	received := make(chan string, 1)
250	ctx := t.Context()
251
252	closer, err := relay.Listen(ctx, browserWindowOpener{
253		openExternalURL: func(_ context.Context, url string) error {
254			received <- url
255			return nil
256		},
257		openFreshWindow: func(context.Context, string) error {
258			t.Fatal("external URL relay request must preserve configured placement")
259			return nil
260		},
261	})
262	require.NoError(t, err)
263	defer closer.Close()
264
265	waitForSocket(t, ipc.BrowserLaunchSocket)
266
267	delivered, err := relay.DeliverOpenExternalURL(context.Background(), "https://example.com/new")
268
269	require.NoError(t, err)
270	assert.True(t, delivered)
271
272	select {
273	case got := <-received:
274		assert.Equal(t, "https://example.com/new", got)
275	case <-time.After(time.Second):
276		t.Fatal("expected opener to receive the URL")
277	}
278}
279
280func TestBrowserLaunchRelay_DeliverOpenFreshWindow_RoundTrip(t *testing.T) {
281	ipc := testIPC(shortTempDir(t))
282	relay := NewBrowserLaunchRelay(ipc)
283
284	freshWindows := make(chan string, 1)
285	ctx := t.Context()
286	closer, err := relay.Listen(ctx, browserWindowOpener{
287		openExternalURL: func(context.Context, string) error {
288			t.Fatal("external-link placement must not handle standalone omnibox navigation")
289			return nil
290		},
291		openFreshWindow: func(_ context.Context, url string) error {
292			freshWindows <- url
293			return nil
294		},
295	})
296	require.NoError(t, err)
297	defer closer.Close()
298
299	waitForSocket(t, ipc.BrowserLaunchSocket)
300	delivered, err := relay.DeliverOpenFreshWindow(context.Background(), "https://example.com/omnibox")
301
302	require.NoError(t, err)
303	require.True(t, delivered)
304	select {
305	case got := <-freshWindows:
306		assert.Equal(t, "https://example.com/omnibox", got)
307	case <-time.After(time.Second):
308		t.Fatal("expected fresh-window opener to receive the URL")
309	}
310}
311
312func TestBrowserLaunchRelay_RequiresBrowserLaunchSocketPath(t *testing.T) {
313	relay := NewBrowserLaunchRelay(runtimeprofile.IPCPaths{})
314
315	_, err := relay.Listen(context.Background(), browserWindowOpenerFunc(func(context.Context, string) error { return nil }))
316	require.Error(t, err)
317	require.Contains(t, err.Error(), "browser launch socket path")
318}
319
320func TestBrowserLaunchRelay_SecondListenWhileLiveFailsWithoutReplacingListener(t *testing.T) {
321	ipc := testIPC(shortTempDir(t))
322	relay := NewBrowserLaunchRelay(ipc)
323
324	firstReceived := make(chan string, 1)
325	ctx := t.Context()
326
327	closer, err := relay.Listen(ctx, browserWindowOpenerFunc(func(_ context.Context, url string) error {
328		firstReceived <- url
329		return nil
330	}))
331	require.NoError(t, err)
332	defer closer.Close()
333
334	waitForSocket(t, ipc.BrowserLaunchSocket)
335
336	_, err = relay.Listen(ctx, browserWindowOpenerFunc(func(context.Context, string) error { return nil }))
337	require.Error(t, err)
338
339	delivered, err := relay.DeliverOpenExternalURL(context.Background(), "https://example.com/live")
340	require.NoError(t, err)
341	require.True(t, delivered)
342
343	select {
344	case got := <-firstReceived:
345		assert.Equal(t, "https://example.com/live", got)
346	case <-time.After(time.Second):
347		t.Fatal("expected the original listener to remain active")
348	}
349}
350
351func TestBrowserLaunchRelay_RebindsStaleSocketPath(t *testing.T) {
352	ipc := testIPC(shortTempDir(t))
353	relay := NewBrowserLaunchRelay(ipc)
354
355	require.NoError(t, os.MkdirAll(filepath.Dir(ipc.BrowserLaunchSocket), 0o700))
356	require.NoError(t, os.WriteFile(ipc.BrowserLaunchSocket, []byte("stale"), 0o600))
357
358	received := make(chan string, 1)
359	ctx := t.Context()
360
361	closer, err := relay.Listen(ctx, browserWindowOpenerFunc(func(_ context.Context, url string) error {
362		received <- url
363		return nil
364	}))
365	require.NoError(t, err)
366	defer closer.Close()
367
368	waitForSocket(t, ipc.BrowserLaunchSocket)
369
370	delivered, err := relay.DeliverOpenExternalURL(context.Background(), "https://example.com/stale")
371	require.NoError(t, err)
372	require.True(t, delivered)
373
374	select {
375	case got := <-received:
376		assert.Equal(t, "https://example.com/stale", got)
377	case <-time.After(time.Second):
378		t.Fatal("expected stale socket path to be rebound")
379	}
380}
381
382func TestBrowserLaunchRelay_RejectsMalformedPayloads(t *testing.T) {
383	ipc := testIPC(shortTempDir(t))
384	relay := NewBrowserLaunchRelay(ipc)
385
386	received := make(chan string, 1)
387	ctx := t.Context()
388
389	closer, err := relay.Listen(ctx, browserWindowOpenerFunc(func(_ context.Context, url string) error {
390		received <- url
391		return nil
392	}))
393	require.NoError(t, err)
394	defer closer.Close()
395
396	waitForSocket(t, ipc.BrowserLaunchSocket)
397
398	conn, err := net.Dial("unix", ipc.BrowserLaunchSocket)
399	require.NoError(t, err)
400
401	_, err = conn.Write([]byte("not-json"))
402	require.NoError(t, err)
403	require.NoError(t, conn.Close())
404
405	delivered, err := relay.DeliverOpenExternalURL(context.Background(), "https://example.com/still-works")
406
407	require.NoError(t, err)
408	assert.True(t, delivered)
409
410	select {
411	case got := <-received:
412		assert.Equal(t, "https://example.com/still-works", got)
413	case <-time.After(time.Second):
414		t.Fatal("expected opener to receive the URL after malformed payload")
415	}
416}
417
418func TestBrowserLaunchRelay_CloseRemovesSocketPath(t *testing.T) {
419	ipc := testIPC(shortTempDir(t))
420	relay := NewBrowserLaunchRelay(ipc)
421
422	ctx := t.Context()
423
424	closer, err := relay.Listen(ctx, browserWindowOpenerFunc(func(context.Context, string) error { return nil }))
425	require.NoError(t, err)
426
427	waitForSocket(t, ipc.BrowserLaunchSocket)
428
429	require.NoError(t, closer.Close())
430	waitForSocketGone(t, ipc.BrowserLaunchSocket)
431}
432
433func TestBrowserLaunchRelay_CloseDoesNotRemoveSuccessorSocket(t *testing.T) {
434	ipc := testIPC(shortTempDir(t))
435	relay := NewBrowserLaunchRelay(ipc)
436	closer, err := relay.Listen(t.Context(), browserWindowOpenerFunc(func(context.Context, string) error { return nil }))
437	require.NoError(t, err)
438	waitForSocket(t, ipc.BrowserLaunchSocket)
439
440	require.NoError(t, os.Remove(ipc.BrowserLaunchSocket))
441	successor, err := net.ListenUnix("unix", &net.UnixAddr{Name: ipc.BrowserLaunchSocket, Net: "unix"})
442	require.NoError(t, err)
443	defer successor.Close()
444	require.NoError(t, closer.Close())
445	_, err = os.Lstat(ipc.BrowserLaunchSocket)
446	require.NoError(t, err, "closing predecessor must preserve successor pathname")
447}
448
449func TestBrowserLaunchRelay_ClientCloseBeforeAckDispatchesOnceAndDeduplicates(t *testing.T) {
450	ipc := testIPC(shortTempDir(t))
451	relay := NewBrowserLaunchRelay(ipc)
452	received := make(chan string, 2)
453	closer, err := relay.Listen(t.Context(), browserWindowOpenerFunc(func(_ context.Context, url string) error { received <- url; return nil }))
454	require.NoError(t, err)
455	defer closer.Close()
456	waitForSocket(t, ipc.BrowserLaunchSocket)
457
458	send := func() {
459		conn, dialErr := net.Dial("unix", ipc.BrowserLaunchSocket)
460		require.NoError(t, dialErr)
461		require.NoError(t, json.NewEncoder(conn).Encode(browserLaunchRequest{RequestID: "retained-1", URL: "https://example.com/retained"}))
462		require.NoError(t, conn.Close())
463	}
464	send()
465	require.Equal(t, "https://example.com/retained", <-received)
466	send()
467	select {
468	case got := <-received:
469		t.Fatalf("duplicate request dispatched: %s", got)
470	case <-time.After(100 * time.Millisecond):
471	}
472}
473
474func TestBrowserLaunchRelay_ContextCancelStopsServing(t *testing.T) {
475	ipc := testIPC(shortTempDir(t))
476	relay := NewBrowserLaunchRelay(ipc)
477
478	ctx, cancel := context.WithCancel(context.Background())
479	closer, err := relay.Listen(ctx, browserWindowOpenerFunc(func(context.Context, string) error { return nil }))
480	require.NoError(t, err)
481
482	waitForSocket(t, ipc.BrowserLaunchSocket)
483
484	cancel()
485	waitForSocketGone(t, ipc.BrowserLaunchSocket)
486	require.NoError(t, closer.Close())
487
488	delivered, err := relay.DeliverOpenExternalURL(context.Background(), "https://example.com/after-cancel")
489	require.NoError(t, err)
490	assert.False(t, delivered)
491}
492
493func TestBrowserLaunchRelay_DeliverOpenExternalURL_RespectsResponseReadTimeout(t *testing.T) {
494	ipc := testIPC(shortTempDir(t))
495	require.NoError(t, os.MkdirAll(ipc.RuntimeDir, 0o700))
496
497	listener, err := net.ListenUnix("unix", &net.UnixAddr{Name: ipc.BrowserLaunchSocket, Net: "unix"})
498	require.NoError(t, err)
499	defer func() {
500		_ = listener.Close()
501		_ = os.Remove(ipc.BrowserLaunchSocket)
502	}()
503
504	ready := make(chan struct{})
505	go func() {
506		close(ready)
507		conn, acceptErr := listener.AcceptUnix()
508		if acceptErr != nil {
509			return
510		}
511		defer conn.Close()
512		<-time.After(time.Second)
513	}()
514	<-ready
515
516	relay := NewBrowserLaunchRelay(ipc)
517	ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
518	defer cancel()
519
520	delivered, err := relay.DeliverOpenExternalURL(ctx, "https://example.com/slow")
521	require.Error(t, err)
522	require.ErrorIs(t, err, context.DeadlineExceeded)
523	assert.False(t, delivered)
524}
525
526func TestBrowserLaunchRelay_DeliverOpenExternalURL_ReturnsUnconfirmedErrorOnNoDeadlineTimeout(t *testing.T) {
527	ipc := testIPC(shortTempDir(t))
528	require.NoError(t, os.MkdirAll(ipc.RuntimeDir, 0o700))
529
530	listener, err := net.ListenUnix("unix", &net.UnixAddr{Name: ipc.BrowserLaunchSocket, Net: "unix"})
531	require.NoError(t, err)
532	defer func() {
533		_ = listener.Close()
534		_ = os.Remove(ipc.BrowserLaunchSocket)
535	}()
536
537	ready := make(chan struct{})
538	go func() {
539		close(ready)
540		conn, acceptErr := listener.AcceptUnix()
541		if acceptErr != nil {
542			return
543		}
544		defer conn.Close()
545		<-time.After(time.Second)
546	}()
547	<-ready
548
549	relay := NewBrowserLaunchRelay(ipc)
550
551	started := make(chan struct{})
552
553	type deliverResult struct {
554		delivered bool
555		err       error
556	}
557	result := make(chan deliverResult, 1)
558	go func() {
559		close(started)
560		delivered, err := relay.DeliverOpenExternalURL(context.Background(), "https://example.com/slow")
561		result <- deliverResult{delivered: delivered, err: err}
562	}()
563	<-started
564
565	select {
566	case got := <-result:
567		assert.True(t, got.delivered)
568		require.Error(t, got.err)
569		require.ErrorIs(t, got.err, ErrBrowserLaunchRelayUnconfirmed)
570	case <-time.After(browserLaunchIOTimeout * 3):
571		t.Fatal("DeliverOpenExternalURL blocked without a caller deadline")
572	}
573}
574
575func TestBrowserLaunchRelay_AcknowledgesBeforeOpenerReturns(t *testing.T) {
576	ipc := testIPC(shortTempDir(t))
577	relay := NewBrowserLaunchRelay(ipc)
578
579	started := make(chan struct{})
580	release := make(chan struct{})
581	ctx := t.Context()
582
583	closer, err := relay.Listen(ctx, browserWindowOpenerFunc(func(_ context.Context, _ string) error {
584		close(started)
585		<-release
586		return nil
587	}))
588	require.NoError(t, err)
589	defer closer.Close()
590
591	waitForSocket(t, ipc.BrowserLaunchSocket)
592
593	conn, err := net.Dial("unix", ipc.BrowserLaunchSocket)
594	require.NoError(t, err)
595	defer conn.Close()
596	require.NoError(t, conn.SetDeadline(time.Now().Add(300*time.Millisecond)))
597	require.NoError(t, json.NewEncoder(conn).Encode(browserLaunchRequest{RequestID: "raw-request-1", URL: "https://example.com/slow-but-healthy"}))
598
599	var response browserLaunchResponse
600	require.NoError(t, json.NewDecoder(conn).Decode(&response))
601	assert.Empty(t, response.Error)
602	assert.True(t, response.Accepted)
603	assert.Equal(t, "raw-request-1", response.RequestID)
604
605	select {
606	case <-started:
607	case <-time.After(time.Second):
608		t.Fatal("expected opener to be invoked after acknowledgement")
609	}
610	close(release)
611}
612
613func TestBrowserLaunchRelay_AcknowledgesAcceptedEvenWhenOpenerReturnsLateError(t *testing.T) {
614	ipc := testIPC(shortTempDir(t))
615	relay := NewBrowserLaunchRelay(ipc)
616
617	started := make(chan struct{})
618	ctx := t.Context()
619
620	closer, err := relay.Listen(ctx, browserWindowOpenerFunc(func(_ context.Context, _ string) error {
621		close(started)
622		return errors.New("late open failure")
623	}))
624	require.NoError(t, err)
625	defer closer.Close()
626
627	waitForSocket(t, ipc.BrowserLaunchSocket)
628
629	conn, err := net.Dial("unix", ipc.BrowserLaunchSocket)
630	require.NoError(t, err)
631	defer conn.Close()
632	require.NoError(t, conn.SetDeadline(time.Now().Add(300*time.Millisecond)))
633	require.NoError(t, json.NewEncoder(conn).Encode(browserLaunchRequest{RequestID: "raw-request-2", URL: "https://example.com/fails-later"}))
634
635	var response browserLaunchResponse
636	require.NoError(t, json.NewDecoder(conn).Decode(&response))
637	assert.Empty(t, response.Error)
638	assert.True(t, response.Accepted)
639	assert.Equal(t, "raw-request-2", response.RequestID)
640
641	select {
642	case <-started:
643	case <-time.After(time.Second):
644		t.Fatal("expected opener to be invoked after accepted acknowledgement")
645	}
646}
647
648func TestBrowserLaunchRelay_SilentClientDoesNotStallListener(t *testing.T) {
649	ipc := testIPC(shortTempDir(t))
650	relay := NewBrowserLaunchRelay(ipc)
651
652	received := make(chan string, 1)
653	ctx := t.Context()
654
655	closer, err := relay.Listen(ctx, browserWindowOpenerFunc(func(_ context.Context, url string) error {
656		received <- url
657		return nil
658	}))
659	require.NoError(t, err)
660	defer closer.Close()
661
662	waitForSocket(t, ipc.BrowserLaunchSocket)
663
664	conn, err := net.Dial("unix", ipc.BrowserLaunchSocket)
665	require.NoError(t, err)
666	defer conn.Close()
667	time.Sleep(20 * time.Millisecond)
668
669	type deliverResult struct {
670		delivered bool
671		err       error
672	}
673	result := make(chan deliverResult, 1)
674	go func() {
675		delivered, err := relay.DeliverOpenExternalURL(context.Background(), "https://example.com/recovered")
676		result <- deliverResult{delivered: delivered, err: err}
677	}()
678
679	select {
680	case got := <-result:
681		require.NoError(t, got.err)
682		require.True(t, got.delivered)
683	case <-time.After(300 * time.Millisecond):
684		t.Fatal("silent client stalled the relay listener")
685	}
686
687	select {
688	case got := <-received:
689		assert.Equal(t, "https://example.com/recovered", got)
690	case <-time.After(time.Second):
691		t.Fatal("expected opener to receive the URL")
692	}
693}
694
695type blockingRelayOpener struct {
696	release  chan struct{}
697	received chan string
698}
699
700func (o *blockingRelayOpener) OpenExternalURL(_ context.Context, url string) error {
701	o.received <- url
702	<-o.release
703	return nil
704}
705
706func (o *blockingRelayOpener) OpenFreshWindow(_ context.Context, url string) error {
707	return o.OpenExternalURL(context.Background(), url)
708}
709
710func startLeaseTestListener(t *testing.T, opener port.BrowserWindowOpener) *browserLaunchRelay {
711	t.Helper()
712	ipc := testIPC(shortTempDir(t))
713	require.NoError(t, os.MkdirAll(ipc.RuntimeDir, 0o700))
714	relay := NewBrowserLaunchRelay(ipc).(*browserLaunchRelay)
715	ctx, cancel := context.WithCancel(context.Background())
716	t.Cleanup(cancel)
717	closer, err := relay.Listen(ctx, opener)
718	require.NoError(t, err)
719	t.Cleanup(func() { _ = closer.Close() })
720	return relay
721}
722
723func waitForLeaseCount(t *testing.T, gate *process.AdmissionGate, want int) {
724	t.Helper()
725	deadline := time.Now().Add(3 * time.Second)
726	for time.Now().Before(deadline) {
727		if gate.Active() == want {
728			return
729		}
730		time.Sleep(5 * time.Millisecond)
731	}
732	t.Fatalf("lease count = %d, want %d", gate.Active(), want)
733}
734
735func TestBrowserLaunchRelay_HoldsLeaseThroughDispatch(t *testing.T) {
736	opener := &blockingRelayOpener{release: make(chan struct{}), received: make(chan string, 2)}
737	relay := startLeaseTestListener(t, opener)
738	gate := relay.AdmissionGate()
739
740	delivered := make(chan error, 1)
741	go func() {
742		_, err := relay.DeliverOpenExternalURL(context.Background(), "https://example.com/lease")
743		delivered <- err
744	}()
745	select {
746	case url := <-opener.received:
747		require.Equal(t, "https://example.com/lease", url)
748	case <-time.After(3 * time.Second):
749		t.Fatal("opener never received the request")
750	}
751	// Acknowledged and dispatched: the lease is held until dispatch settles.
752	waitForLeaseCount(t, gate, 1)
753	select {
754	case err := <-delivered:
755		require.NoError(t, err)
756	case <-time.After(3 * time.Second):
757		t.Fatal("client never received acknowledgement")
758	}
759	close(opener.release)
760	waitForLeaseCount(t, gate, 0)
761}
762
763func TestBrowserLaunchRelay_SimultaneousOpensHoldLeases(t *testing.T) {
764	opener := &blockingRelayOpener{release: make(chan struct{}), received: make(chan string, 4)}
765	relay := startLeaseTestListener(t, opener)
766	gate := relay.AdmissionGate()
767
768	for range 2 {
769		go func() {
770			_, _ = relay.DeliverOpenExternalURL(context.Background(), "https://example.com/parallel")
771		}()
772	}
773	for range 2 {
774		select {
775		case <-opener.received:
776		case <-time.After(3 * time.Second):
777			t.Fatal("opener never received a parallel request")
778		}
779	}
780	waitForLeaseCount(t, gate, 2)
781	close(opener.release)
782	waitForLeaseCount(t, gate, 0)
783}
784
785func TestBrowserLaunchRelay_ClosedAdmissionSendsErrorResponse(t *testing.T) {
786	opener := &blockingRelayOpener{release: make(chan struct{}), received: make(chan string, 1)}
787	relay := startLeaseTestListener(t, opener)
788	gate := relay.AdmissionGate()
789	close(opener.release)
790
791	gate.Close()
792	delivered, err := relay.DeliverOpenExternalURL(context.Background(), "https://example.com/late")
793	require.Error(t, err, "losing request must receive the error response")
794	require.Contains(t, err.Error(), "shutting down")
795	require.Equal(t, 0, gate.Active(), "refused requests hold no lease")
796	select {
797	case url := <-opener.received:
798		t.Fatalf("refused request reached the opener: %s", url)
799	case <-time.After(300 * time.Millisecond):
800	}
801	_ = delivered
802}
803
804type closeTrackingOpener struct {
805	*blockingRelayOpener
806	closed chan struct{}
807}
808
809func (o *closeTrackingOpener) CloseAllWindows(_ context.Context) error {
810	close(o.closed)
811	return nil
812}
813
814func sendRelayAction(t *testing.T, socketPath, action string) browserLaunchResponse {
815	t.Helper()
816	conn, err := net.DialUnix("unix", nil, &net.UnixAddr{Name: socketPath, Net: "unix"})
817	require.NoError(t, err)
818	defer func() { _ = conn.Close() }()
819	require.NoError(t, conn.SetDeadline(time.Now().Add(3*time.Second)))
820	require.NoError(t, json.NewEncoder(conn).Encode(browserLaunchRequest{RequestID: "diag-1", Action: browserLaunchAction(action)}))
821	var response browserLaunchResponse
822	require.NoError(t, json.NewDecoder(conn).Decode(&response))
823	return response
824}
825
826func TestBrowserLaunchRelay_DiagnosticCloseRejectedWithoutOptIn(t *testing.T) {
827	opener := &blockingRelayOpener{release: make(chan struct{}), received: make(chan string, 1)}
828	close(opener.release)
829	relay := startLeaseTestListener(t, opener)
830	response := sendRelayAction(t, relay.ipc.BrowserLaunchSocket, "close-all-windows")
831	require.False(t, response.Accepted, "rejected close must not acknowledge")
832	require.Contains(t, response.Error, "diagnostic close not enabled")
833}
834
835func TestBrowserLaunchRelay_DiagnosticCloseDispatchedWithOptIn(t *testing.T) {
836	t.Setenv(diagnosticWindowCloseEnvVar, "1")
837	opener := &blockingRelayOpener{release: make(chan struct{}), received: make(chan string, 1)}
838	close(opener.release)
839	tracker := &closeTrackingOpener{blockingRelayOpener: opener, closed: make(chan struct{})}
840	relay := startLeaseTestListener(t, tracker)
841	response := sendRelayAction(t, relay.ipc.BrowserLaunchSocket, "close-all-windows")
842	require.True(t, response.Accepted)
843	select {
844	case <-tracker.closed:
845	case <-time.After(3 * time.Second):
846		t.Fatal("diagnostic close never reached the opener")
847	}
848}

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