v0.32.0 0617278

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_ContextCancelStopsServing(t *testing.T) {
434	ipc := testIPC(shortTempDir(t))
435	relay := NewBrowserLaunchRelay(ipc)
436
437	ctx, cancel := context.WithCancel(context.Background())
438	closer, err := relay.Listen(ctx, browserWindowOpenerFunc(func(context.Context, string) error { return nil }))
439	require.NoError(t, err)
440
441	waitForSocket(t, ipc.BrowserLaunchSocket)
442
443	cancel()
444	waitForSocketGone(t, ipc.BrowserLaunchSocket)
445	require.NoError(t, closer.Close())
446
447	delivered, err := relay.DeliverOpenExternalURL(context.Background(), "https://example.com/after-cancel")
448	require.NoError(t, err)
449	assert.False(t, delivered)
450}
451
452func TestBrowserLaunchRelay_DeliverOpenExternalURL_RespectsResponseReadTimeout(t *testing.T) {
453	ipc := testIPC(shortTempDir(t))
454	require.NoError(t, os.MkdirAll(ipc.RuntimeDir, 0o700))
455
456	listener, err := net.ListenUnix("unix", &net.UnixAddr{Name: ipc.BrowserLaunchSocket, Net: "unix"})
457	require.NoError(t, err)
458	defer func() {
459		_ = listener.Close()
460		_ = os.Remove(ipc.BrowserLaunchSocket)
461	}()
462
463	ready := make(chan struct{})
464	go func() {
465		close(ready)
466		conn, acceptErr := listener.AcceptUnix()
467		if acceptErr != nil {
468			return
469		}
470		defer conn.Close()
471		<-time.After(time.Second)
472	}()
473	<-ready
474
475	relay := NewBrowserLaunchRelay(ipc)
476	ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
477	defer cancel()
478
479	delivered, err := relay.DeliverOpenExternalURL(ctx, "https://example.com/slow")
480	require.Error(t, err)
481	require.ErrorIs(t, err, context.DeadlineExceeded)
482	assert.False(t, delivered)
483}
484
485func TestBrowserLaunchRelay_DeliverOpenExternalURL_ReturnsUnconfirmedErrorOnNoDeadlineTimeout(t *testing.T) {
486	ipc := testIPC(shortTempDir(t))
487	require.NoError(t, os.MkdirAll(ipc.RuntimeDir, 0o700))
488
489	listener, err := net.ListenUnix("unix", &net.UnixAddr{Name: ipc.BrowserLaunchSocket, Net: "unix"})
490	require.NoError(t, err)
491	defer func() {
492		_ = listener.Close()
493		_ = os.Remove(ipc.BrowserLaunchSocket)
494	}()
495
496	ready := make(chan struct{})
497	go func() {
498		close(ready)
499		conn, acceptErr := listener.AcceptUnix()
500		if acceptErr != nil {
501			return
502		}
503		defer conn.Close()
504		<-time.After(time.Second)
505	}()
506	<-ready
507
508	relay := NewBrowserLaunchRelay(ipc)
509
510	started := make(chan struct{})
511
512	type deliverResult struct {
513		delivered bool
514		err       error
515	}
516	result := make(chan deliverResult, 1)
517	go func() {
518		close(started)
519		delivered, err := relay.DeliverOpenExternalURL(context.Background(), "https://example.com/slow")
520		result <- deliverResult{delivered: delivered, err: err}
521	}()
522	<-started
523
524	select {
525	case got := <-result:
526		assert.True(t, got.delivered)
527		require.Error(t, got.err)
528		require.ErrorIs(t, got.err, ErrBrowserLaunchRelayUnconfirmed)
529	case <-time.After(browserLaunchIOTimeout * 3):
530		t.Fatal("DeliverOpenExternalURL blocked without a caller deadline")
531	}
532}
533
534func TestBrowserLaunchRelay_AcknowledgesBeforeOpenerReturns(t *testing.T) {
535	ipc := testIPC(shortTempDir(t))
536	relay := NewBrowserLaunchRelay(ipc)
537
538	started := make(chan struct{})
539	release := make(chan struct{})
540	ctx := t.Context()
541
542	closer, err := relay.Listen(ctx, browserWindowOpenerFunc(func(_ context.Context, _ string) error {
543		close(started)
544		<-release
545		return nil
546	}))
547	require.NoError(t, err)
548	defer closer.Close()
549
550	waitForSocket(t, ipc.BrowserLaunchSocket)
551
552	conn, err := net.Dial("unix", ipc.BrowserLaunchSocket)
553	require.NoError(t, err)
554	defer conn.Close()
555	require.NoError(t, conn.SetDeadline(time.Now().Add(300*time.Millisecond)))
556	require.NoError(t, json.NewEncoder(conn).Encode(browserLaunchRequest{RequestID: "raw-request-1", URL: "https://example.com/slow-but-healthy"}))
557
558	var response browserLaunchResponse
559	require.NoError(t, json.NewDecoder(conn).Decode(&response))
560	assert.Empty(t, response.Error)
561	assert.True(t, response.Accepted)
562	assert.Equal(t, "raw-request-1", response.RequestID)
563
564	select {
565	case <-started:
566	case <-time.After(time.Second):
567		t.Fatal("expected opener to be invoked after acknowledgement")
568	}
569	close(release)
570}
571
572func TestBrowserLaunchRelay_AcknowledgesAcceptedEvenWhenOpenerReturnsLateError(t *testing.T) {
573	ipc := testIPC(shortTempDir(t))
574	relay := NewBrowserLaunchRelay(ipc)
575
576	started := make(chan struct{})
577	ctx := t.Context()
578
579	closer, err := relay.Listen(ctx, browserWindowOpenerFunc(func(_ context.Context, _ string) error {
580		close(started)
581		return errors.New("late open failure")
582	}))
583	require.NoError(t, err)
584	defer closer.Close()
585
586	waitForSocket(t, ipc.BrowserLaunchSocket)
587
588	conn, err := net.Dial("unix", ipc.BrowserLaunchSocket)
589	require.NoError(t, err)
590	defer conn.Close()
591	require.NoError(t, conn.SetDeadline(time.Now().Add(300*time.Millisecond)))
592	require.NoError(t, json.NewEncoder(conn).Encode(browserLaunchRequest{RequestID: "raw-request-2", URL: "https://example.com/fails-later"}))
593
594	var response browserLaunchResponse
595	require.NoError(t, json.NewDecoder(conn).Decode(&response))
596	assert.Empty(t, response.Error)
597	assert.True(t, response.Accepted)
598	assert.Equal(t, "raw-request-2", response.RequestID)
599
600	select {
601	case <-started:
602	case <-time.After(time.Second):
603		t.Fatal("expected opener to be invoked after accepted acknowledgement")
604	}
605}
606
607func TestBrowserLaunchRelay_SilentClientDoesNotStallListener(t *testing.T) {
608	ipc := testIPC(shortTempDir(t))
609	relay := NewBrowserLaunchRelay(ipc)
610
611	received := make(chan string, 1)
612	ctx := t.Context()
613
614	closer, err := relay.Listen(ctx, browserWindowOpenerFunc(func(_ context.Context, url string) error {
615		received <- url
616		return nil
617	}))
618	require.NoError(t, err)
619	defer closer.Close()
620
621	waitForSocket(t, ipc.BrowserLaunchSocket)
622
623	conn, err := net.Dial("unix", ipc.BrowserLaunchSocket)
624	require.NoError(t, err)
625	defer conn.Close()
626	time.Sleep(20 * time.Millisecond)
627
628	type deliverResult struct {
629		delivered bool
630		err       error
631	}
632	result := make(chan deliverResult, 1)
633	go func() {
634		delivered, err := relay.DeliverOpenExternalURL(context.Background(), "https://example.com/recovered")
635		result <- deliverResult{delivered: delivered, err: err}
636	}()
637
638	select {
639	case got := <-result:
640		require.NoError(t, got.err)
641		require.True(t, got.delivered)
642	case <-time.After(300 * time.Millisecond):
643		t.Fatal("silent client stalled the relay listener")
644	}
645
646	select {
647	case got := <-received:
648		assert.Equal(t, "https://example.com/recovered", got)
649	case <-time.After(time.Second):
650		t.Fatal("expected opener to receive the URL")
651	}
652}
653
654type blockingRelayOpener struct {
655	release  chan struct{}
656	received chan string
657}
658
659func (o *blockingRelayOpener) OpenExternalURL(_ context.Context, url string) error {
660	o.received <- url
661	<-o.release
662	return nil
663}
664
665func (o *blockingRelayOpener) OpenFreshWindow(_ context.Context, url string) error {
666	return o.OpenExternalURL(context.Background(), url)
667}
668
669func startLeaseTestListener(t *testing.T, opener port.BrowserWindowOpener) *browserLaunchRelay {
670	t.Helper()
671	ipc := testIPC(shortTempDir(t))
672	require.NoError(t, os.MkdirAll(ipc.RuntimeDir, 0o700))
673	relay := NewBrowserLaunchRelay(ipc).(*browserLaunchRelay)
674	ctx, cancel := context.WithCancel(context.Background())
675	t.Cleanup(cancel)
676	closer, err := relay.Listen(ctx, opener)
677	require.NoError(t, err)
678	t.Cleanup(func() { _ = closer.Close() })
679	return relay
680}
681
682func waitForLeaseCount(t *testing.T, gate *process.AdmissionGate, want int) {
683	t.Helper()
684	deadline := time.Now().Add(3 * time.Second)
685	for time.Now().Before(deadline) {
686		if gate.Active() == want {
687			return
688		}
689		time.Sleep(5 * time.Millisecond)
690	}
691	t.Fatalf("lease count = %d, want %d", gate.Active(), want)
692}
693
694func TestBrowserLaunchRelay_HoldsLeaseThroughDispatch(t *testing.T) {
695	opener := &blockingRelayOpener{release: make(chan struct{}), received: make(chan string, 2)}
696	relay := startLeaseTestListener(t, opener)
697	gate := relay.AdmissionGate()
698
699	delivered := make(chan error, 1)
700	go func() {
701		_, err := relay.DeliverOpenExternalURL(context.Background(), "https://example.com/lease")
702		delivered <- err
703	}()
704	select {
705	case url := <-opener.received:
706		require.Equal(t, "https://example.com/lease", url)
707	case <-time.After(3 * time.Second):
708		t.Fatal("opener never received the request")
709	}
710	// Acknowledged and dispatched: the lease is held until dispatch settles.
711	waitForLeaseCount(t, gate, 1)
712	select {
713	case err := <-delivered:
714		require.NoError(t, err)
715	case <-time.After(3 * time.Second):
716		t.Fatal("client never received acknowledgement")
717	}
718	close(opener.release)
719	waitForLeaseCount(t, gate, 0)
720}
721
722func TestBrowserLaunchRelay_SimultaneousOpensHoldLeases(t *testing.T) {
723	opener := &blockingRelayOpener{release: make(chan struct{}), received: make(chan string, 4)}
724	relay := startLeaseTestListener(t, opener)
725	gate := relay.AdmissionGate()
726
727	for range 2 {
728		go func() {
729			_, _ = relay.DeliverOpenExternalURL(context.Background(), "https://example.com/parallel")
730		}()
731	}
732	for range 2 {
733		select {
734		case <-opener.received:
735		case <-time.After(3 * time.Second):
736			t.Fatal("opener never received a parallel request")
737		}
738	}
739	waitForLeaseCount(t, gate, 2)
740	close(opener.release)
741	waitForLeaseCount(t, gate, 0)
742}
743
744func TestBrowserLaunchRelay_ClosedAdmissionSendsErrorResponse(t *testing.T) {
745	opener := &blockingRelayOpener{release: make(chan struct{}), received: make(chan string, 1)}
746	relay := startLeaseTestListener(t, opener)
747	gate := relay.AdmissionGate()
748	close(opener.release)
749
750	gate.Close()
751	delivered, err := relay.DeliverOpenExternalURL(context.Background(), "https://example.com/late")
752	require.Error(t, err, "losing request must receive the error response")
753	require.Contains(t, err.Error(), "shutting down")
754	require.Equal(t, 0, gate.Active(), "refused requests hold no lease")
755	select {
756	case url := <-opener.received:
757		t.Fatalf("refused request reached the opener: %s", url)
758	case <-time.After(300 * time.Millisecond):
759	}
760	_ = delivered
761}
762
763type closeTrackingOpener struct {
764	*blockingRelayOpener
765	closed chan struct{}
766}
767
768func (o *closeTrackingOpener) CloseAllWindows(_ context.Context) error {
769	close(o.closed)
770	return nil
771}
772
773func sendRelayAction(t *testing.T, socketPath, action string) browserLaunchResponse {
774	t.Helper()
775	conn, err := net.DialUnix("unix", nil, &net.UnixAddr{Name: socketPath, Net: "unix"})
776	require.NoError(t, err)
777	defer func() { _ = conn.Close() }()
778	require.NoError(t, conn.SetDeadline(time.Now().Add(3*time.Second)))
779	require.NoError(t, json.NewEncoder(conn).Encode(browserLaunchRequest{RequestID: "diag-1", Action: browserLaunchAction(action)}))
780	var response browserLaunchResponse
781	require.NoError(t, json.NewDecoder(conn).Decode(&response))
782	return response
783}
784
785func TestBrowserLaunchRelay_DiagnosticCloseRejectedWithoutOptIn(t *testing.T) {
786	opener := &blockingRelayOpener{release: make(chan struct{}), received: make(chan string, 1)}
787	close(opener.release)
788	relay := startLeaseTestListener(t, opener)
789	response := sendRelayAction(t, relay.ipc.BrowserLaunchSocket, "close-all-windows")
790	require.False(t, response.Accepted, "rejected close must not acknowledge")
791	require.Contains(t, response.Error, "diagnostic close not enabled")
792}
793
794func TestBrowserLaunchRelay_DiagnosticCloseDispatchedWithOptIn(t *testing.T) {
795	t.Setenv(diagnosticWindowCloseEnvVar, "1")
796	opener := &blockingRelayOpener{release: make(chan struct{}), received: make(chan string, 1)}
797	close(opener.release)
798	tracker := &closeTrackingOpener{blockingRelayOpener: opener, closed: make(chan struct{})}
799	relay := startLeaseTestListener(t, tracker)
800	response := sendRelayAction(t, relay.ipc.BrowserLaunchSocket, "close-all-windows")
801	require.True(t, response.Accepted)
802	select {
803	case <-tracker.closed:
804	case <-time.After(3 * time.Second):
805		t.Fatal("diagnostic close never reached the opener")
806	}
807}

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