detached a5232dd

fix: report relay errors in browser launches

commit
a5232ddf2b44a0cbf7aac6942d62ebc9fb6e5303
author
brice · · 2026-04-14 15:20 UTC
parent
2c39d19
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +37 -3
M internal/infrastructure/desktop/browser_launcher.go +3 -3
@@ -33,12 +33,12 @@ func (l *BrowserLauncher) LaunchURL(ctx context.Context, url string) {
3333
3434 if l.relay != nil {
3535 delivered, err := l.relay.DeliverOpenFreshWindow(ctx, url)
36- if delivered {
37- return
38- }
3936 if err != nil {
4037 fmt.Fprintf(os.Stderr, "failed to forward dumber browser URL %q: %v\n", url, err)
4138 }
39+ if delivered {
40+ return
41+ }
4242 }
4343
4444 resolveExecutablePath := l.resolveExecutablePath
@@ -33,12 +33,12 @@ func (l *BrowserLauncher) LaunchURL(ctx context.Context, url string) {
33 33
34 if l.relay != nil {34 if l.relay != nil {
35 delivered, err := l.relay.DeliverOpenFreshWindow(ctx, url)35 delivered, err := l.relay.DeliverOpenFreshWindow(ctx, url)
36- if delivered {
37- return
38- }
39 if err != nil {36 if err != nil {
40 fmt.Fprintf(os.Stderr, "failed to forward dumber browser URL %q: %v\n", url, err)37 fmt.Fprintf(os.Stderr, "failed to forward dumber browser URL %q: %v\n", url, err)
41 }38 }
39+ if delivered {
40+ return
41+ }
42 }42 }
43 43
44 resolveExecutablePath := l.resolveExecutablePath44 resolveExecutablePath := l.resolveExecutablePath
M internal/infrastructure/desktop/browser_launcher_test.go +34 -0
@@ -2,6 +2,9 @@ package desktop
22
33 import (
44 "context"
5+ "errors"
6+ "io"
7+ "os"
58 "os/exec"
69 "testing"
710
@@ -51,3 +54,34 @@ func TestBrowserLauncher_LaunchURL_FallsBackToSpawnWhenRelayMisses(t *testing.T)
5154 assert.Equal(t, "/usr/bin/dumber", gotCmd.Path)
5255 assert.Equal(t, []string{"/usr/bin/dumber", "browse", "https://example.com"}, gotCmd.Args)
5356 }
57+
58+func TestBrowserLauncher_LaunchURL_ReportsRelayErrorWhenDelivered(t *testing.T) {
59+ relay := mocks.NewMockBrowserLaunchRelay(t)
60+ wantErr := errors.New("relay exploded")
61+ relay.EXPECT().DeliverOpenFreshWindow(context.Background(), "https://example.com").Return(true, wantErr)
62+
63+ launcher := NewBrowserLauncher(relay)
64+ launcher.resolveExecutablePath = func() (string, error) {
65+ t.Fatal("expected executable path resolution to be skipped")
66+ return "", nil
67+ }
68+ launcher.startDetachedProcess = func(cmd *exec.Cmd) error {
69+ t.Fatal("expected launch to stop after relay delivery")
70+ return nil
71+ }
72+
73+ oldStderr := os.Stderr
74+ r, w, err := os.Pipe()
75+ require.NoError(t, err)
76+ os.Stderr = w
77+ t.Cleanup(func() {
78+ os.Stderr = oldStderr
79+ })
80+
81+ launcher.LaunchURL(context.Background(), "https://example.com")
82+
83+ require.NoError(t, w.Close())
84+ output, err := io.ReadAll(r)
85+ require.NoError(t, err)
86+ assert.Contains(t, string(output), wantErr.Error())
87+}
@@ -2,6 +2,9 @@ package desktop
2 2
3 import (3 import (
4 "context"4 "context"
5+ "errors"
6+ "io"
7+ "os"
5 "os/exec"8 "os/exec"
6 "testing"9 "testing"
7 10
@@ -51,3 +54,34 @@ func TestBrowserLauncher_LaunchURL_FallsBackToSpawnWhenRelayMisses(t *testing.T)
51 assert.Equal(t, "/usr/bin/dumber", gotCmd.Path)54 assert.Equal(t, "/usr/bin/dumber", gotCmd.Path)
52 assert.Equal(t, []string{"/usr/bin/dumber", "browse", "https://example.com"}, gotCmd.Args)55 assert.Equal(t, []string{"/usr/bin/dumber", "browse", "https://example.com"}, gotCmd.Args)
53 }56 }
57+
58+func TestBrowserLauncher_LaunchURL_ReportsRelayErrorWhenDelivered(t *testing.T) {
59+ relay := mocks.NewMockBrowserLaunchRelay(t)
60+ wantErr := errors.New("relay exploded")
61+ relay.EXPECT().DeliverOpenFreshWindow(context.Background(), "https://example.com").Return(true, wantErr)
62+
63+ launcher := NewBrowserLauncher(relay)
64+ launcher.resolveExecutablePath = func() (string, error) {
65+ t.Fatal("expected executable path resolution to be skipped")
66+ return "", nil
67+ }
68+ launcher.startDetachedProcess = func(cmd *exec.Cmd) error {
69+ t.Fatal("expected launch to stop after relay delivery")
70+ return nil
71+ }
72+
73+ oldStderr := os.Stderr
74+ r, w, err := os.Pipe()
75+ require.NoError(t, err)
76+ os.Stderr = w
77+ t.Cleanup(func() {
78+ os.Stderr = oldStderr
79+ })
80+
81+ launcher.LaunchURL(context.Background(), "https://example.com")
82+
83+ require.NoError(t, w.Close())
84+ output, err := io.ReadAll(r)
85+ require.NoError(t, err)
86+ assert.Contains(t, string(output), wantErr.Error())
87+}

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