detached 42f0f48

test(ui): make headless gtk tests deterministic

Add testHasX11Auth() preflight check that verifies X11 authorization file exists and has a matching display entry before initializing GTK/Adwaita in requireGTKDisplayApp(). This prevents GTK from emitting 'Failed to open display' warnings to stderr in headless shells where DISPLAY is set but no X authority is configured.

- Add testHasX11Auth(): pure function inspecting XAUTHORITY env, ~/.Xauthority fallback, and parsing the binary .Xauthority format for display-number matching.
- Update requireGTKDisplayApp() to call testHasX11Auth() after testHasUsableGTKDisplay() and before EnsureAdwaitaInitialized(), so invalid/no-auth displays skip before any GTK initialization.
- Preserve existing gdk.DisplayGetDefault() == nil safety check for real-display envs.
- Add 20 pure-probe unit tests covering testHasUsableGTKDisplay (env/socket scenarios) and testHasX11Auth (auth file presence and content matching) using t.Setenv and temp dirs.

Verification: rtk go test ./internal/ui -count=1 → 198 passed, 0 failed. rtk make test → internal/ui PASS (pre-existing failure only in internal/infrastructure/webkit, unrelated).
commit
42f0f48626f76bdf6d72bccac64a233770c048d1
author
brice · · 2026-06-20 09:52 UTC
committer
brice · · 2026-06-20 13:00 UTC
parent
2be8535
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
1 file · +555 -26
M internal/ui/app_browser_launch_test.go +555 -26
@@ -1,7 +1,9 @@
11 package ui
22
33 import (
4+ "bytes"
45 "context"
6+ "encoding/binary"
57 "errors"
68 "io"
79 "math"
@@ -79,51 +81,160 @@ func (r *fakeSessionStateRepo) GetAllSnapshots(context.Context) ([]*entity.Sessi
7981
8082 func (r *fakeSessionStateRepo) GetTotalSnapshotsSize(context.Context) (int64, error) { return 0, nil }
8183
82-func testHasUsableGTKDisplay() bool {
83- if waylandDisplay := os.Getenv("WAYLAND_DISPLAY"); waylandDisplay != "" {
84- candidates := []string{waylandDisplay}
85- if runtimeDir := os.Getenv("XDG_RUNTIME_DIR"); runtimeDir != "" && !filepath.IsAbs(waylandDisplay) {
86- candidates = append([]string{filepath.Join(runtimeDir, waylandDisplay)}, candidates...)
84+func testHasUsableWaylandDisplay() bool {
85+ waylandDisplay := os.Getenv("WAYLAND_DISPLAY")
86+ if waylandDisplay == "" {
87+ return false
88+ }
89+ candidates := []string{waylandDisplay}
90+ if runtimeDir := os.Getenv("XDG_RUNTIME_DIR"); runtimeDir != "" && !filepath.IsAbs(waylandDisplay) {
91+ candidates = append([]string{filepath.Join(runtimeDir, waylandDisplay)}, candidates...)
92+ }
93+ for _, candidate := range candidates {
94+ if _, err := os.Stat(candidate); err == nil {
95+ return true
8796 }
88- for _, candidate := range candidates {
97+ }
98+ return false
99+}
100+
101+func testHasUsableX11Display() bool {
102+ display := os.Getenv("DISPLAY")
103+ if display == "" {
104+ return false
105+ }
106+ if strings.HasPrefix(display, ":") {
107+ displayNum := strings.TrimPrefix(display, ":")
108+ displayNum = strings.SplitN(displayNum, ".", 2)[0]
109+ if displayNum == "" {
110+ return false
111+ }
112+ x11SocketCandidates := []string{filepath.Join(os.TempDir(), ".X11-unix", "X"+displayNum)}
113+ if fallback := "/tmp/.X11-unix/X" + displayNum; fallback != x11SocketCandidates[0] {
114+ x11SocketCandidates = append(x11SocketCandidates, fallback)
115+ }
116+ for _, candidate := range x11SocketCandidates {
89117 if _, err := os.Stat(candidate); err == nil {
90118 return true
91119 }
92120 }
121+ return false
93122 }
123+ return true // TCP display
124+}
94125
95- if display := os.Getenv("DISPLAY"); display != "" {
96- if strings.HasPrefix(display, ":") {
97- displayNum := strings.TrimPrefix(display, ":")
98- displayNum = strings.SplitN(displayNum, ".", 2)[0]
99- if displayNum == "" {
100- return false
101- }
102- x11SocketCandidates := []string{filepath.Join(os.TempDir(), ".X11-unix", "X"+displayNum)}
103- if fallback := "/tmp/.X11-unix/X" + displayNum; fallback != x11SocketCandidates[0] {
104- x11SocketCandidates = append(x11SocketCandidates, fallback)
105- }
106- for _, candidate := range x11SocketCandidates {
107- if _, err := os.Stat(candidate); err == nil {
108- return true
109- }
110- }
126+func testHasUsableGTKDisplay() bool {
127+ return testHasUsableWaylandDisplay() || testHasUsableX11Display()
128+}
129+
130+// testHasX11Auth checks whether X11 authorization is available for the current
131+// DISPLAY before any GTK/Adwaita initialization. It is a pure function that
132+// inspects env vars and the .Xauthority file only; no GTK calls.
133+func testHasX11Auth() bool {
134+ display := os.Getenv("DISPLAY")
135+ if display == "" || !strings.HasPrefix(display, ":") {
136+ // No DISPLAY or TCP-style display; can't check locally.
137+ // Let GTK try and fall through to gdk.DisplayGetDefault().
138+ return true
139+ }
140+
141+ displayNum := strings.TrimPrefix(display, ":")
142+ displayNum = strings.SplitN(displayNum, ".", 2)[0]
143+ if displayNum == "" {
144+ return false
145+ }
146+
147+ // Find the X authority file.
148+ authFile := os.Getenv("XAUTHORITY")
149+ if authFile == "" {
150+ home, err := os.UserHomeDir()
151+ if err != nil {
111152 return false
112153 }
113- return true
154+ authFile = filepath.Join(home, ".Xauthority")
155+ }
156+
157+ if _, err := os.Stat(authFile); err != nil {
158+ return false // No authority file; X11 auth will fail.
114159 }
115160
161+ // Parse the .Xauthority file to look for a matching display entry.
162+ // Format: sequence of {family(2B), addrLen(2B), addr, numLen(2B), num, nameLen(2B), name, dataLen(2B), data}
163+ // All multi-byte values are big-endian (network byte order).
164+ data, err := os.ReadFile(authFile)
165+ if err != nil {
166+ return false
167+ }
168+
169+ return testXauthorityHasDisplay(data, displayNum)
170+}
171+
172+func testXauthorityHasDisplay(data []byte, displayNum string) bool {
173+ for len(data) > 0 {
174+ if len(data) < 2 {
175+ return false
176+ }
177+ data = data[2:] // family, unused
178+
179+ _, rest, ok := testReadXauthorityField(data) // address
180+ if !ok {
181+ return false
182+ }
183+ num, rest, ok := testReadXauthorityField(rest)
184+ if !ok {
185+ return false
186+ }
187+ name, rest, ok := testReadXauthorityField(rest)
188+ if !ok {
189+ return false
190+ }
191+ cookie, rest, ok := testReadXauthorityField(rest)
192+ if !ok {
193+ return false
194+ }
195+ if string(num) == displayNum {
196+ return len(name) > 0 && len(cookie) > 0
197+ }
198+ data = rest
199+ }
116200 return false
117201 }
118202
203+func testReadXauthorityField(data []byte) ([]byte, []byte, bool) {
204+ if len(data) < 2 {
205+ return nil, nil, false
206+ }
207+ fieldLen := int(binary.BigEndian.Uint16(data[:2]))
208+ data = data[2:]
209+ if len(data) < fieldLen {
210+ return nil, nil, false
211+ }
212+ return data[:fieldLen], data[fieldLen:], true
213+}
214+
215+// testNeedsX11Auth reports whether GTK is expected to use the X11 backend,
216+// meaning X11 authorization must be available. When a usable Wayland display
217+// is present and GDK_BACKEND is not forced to x11, X11 auth is not required
218+// because GTK will use the Wayland backend.
219+func testNeedsX11Auth() bool {
220+ return !testHasUsableWaylandDisplay() || os.Getenv("GDK_BACKEND") == "x11"
221+}
222+
119223 func requireGTKDisplayApp(t *testing.T) *gtk.Application {
120224 t.Helper()
121- if !testHasUsableGTKDisplay() {
122- t.Skip("GTK display not available")
225+ hasWayland := testHasUsableWaylandDisplay()
226+ hasX11 := testHasUsableX11Display()
227+ if !hasWayland && !hasX11 {
228+ t.Skip("GTK display not available (no DISPLAY/WAYLAND_DISPLAY socket)")
229+ }
230+ // Only require X11 auth when the X11 backend will be used:
231+ // no Wayland (fallback to X11), or GDK_BACKEND explicitly set to x11.
232+ if testNeedsX11Auth() && !testHasX11Auth() {
233+ t.Skip("GTK display not available (X11 authorization unavailable)")
123234 }
124235 EnsureAdwaitaInitialized()
125236 if gdk.DisplayGetDefault() == nil {
126- t.Skip("GTK display not available")
237+ t.Skip("GTK display not available (gdk.DisplayGetDefault returned nil)")
127238 }
128239 appID := AppID
129240 gtkApp := gtk.NewApplication(&appID, gio.GApplicationNonUniqueValue)
@@ -133,6 +244,424 @@ func requireGTKDisplayApp(t *testing.T) *gtk.Application {
133244 return gtkApp
134245 }
135246
247+func Test_testHasUsableGTKDisplay_PureProbe(t *testing.T) {
248+ t.Run("no display env vars", func(t *testing.T) {
249+ t.Setenv("DISPLAY", "")
250+ t.Setenv("WAYLAND_DISPLAY", "")
251+ if got := testHasUsableGTKDisplay(); got {
252+ t.Errorf("testHasUsableGTKDisplay() = %v, want false", got)
253+ }
254+ })
255+
256+ t.Run("DISPLAY numeric without socket", func(t *testing.T) {
257+ t.Setenv("DISPLAY", ":99")
258+ t.Setenv("WAYLAND_DISPLAY", "")
259+ // No /tmp/.X11-unix/X99 socket exists
260+ if got := testHasUsableGTKDisplay(); got {
261+ t.Errorf("testHasUsableGTKDisplay() = %v, want false", got)
262+ }
263+ })
264+
265+ t.Run("DISPLAY with screen without socket", func(t *testing.T) {
266+ t.Setenv("DISPLAY", ":99.0")
267+ t.Setenv("WAYLAND_DISPLAY", "")
268+ if got := testHasUsableGTKDisplay(); got {
269+ t.Errorf("testHasUsableGTKDisplay() = %v, want false", got)
270+ }
271+ })
272+
273+ t.Run("DISPLAY colon only no number", func(t *testing.T) {
274+ t.Setenv("DISPLAY", ":")
275+ t.Setenv("WAYLAND_DISPLAY", "")
276+ if got := testHasUsableGTKDisplay(); got {
277+ t.Errorf("testHasUsableGTKDisplay() = %v, want false for empty display number", got)
278+ }
279+ })
280+
281+ t.Run("DISPLAY TCP always passes socket check", func(t *testing.T) {
282+ t.Setenv("DISPLAY", "remote-host:0")
283+ t.Setenv("WAYLAND_DISPLAY", "")
284+ if got := testHasUsableGTKDisplay(); !got {
285+ t.Errorf("testHasUsableGTKDisplay() = %v, want true for TCP display", got)
286+ }
287+ })
288+
289+ t.Run("DISPLAY with existing socket", func(t *testing.T) {
290+ t.Setenv("DISPLAY", ":0")
291+ t.Setenv("WAYLAND_DISPLAY", "")
292+ // /tmp/.X11-unix/X0 exists on this machine
293+ got := testHasUsableGTKDisplay()
294+ // We can't guarantee the socket exists in all environments,
295+ // but this verifies it doesn't crash.
296+ t.Logf("testHasUsableGTKDisplay() = %v (depends on /tmp/.X11-unix/X0)", got)
297+ })
298+
299+ t.Run("WAYLAND_DISPLAY with existing socket", func(t *testing.T) {
300+ t.Setenv("DISPLAY", "")
301+ t.Setenv("WAYLAND_DISPLAY", "wayland-1")
302+ tmpDir := t.TempDir()
303+ socketPath := filepath.Join(tmpDir, "wayland-1")
304+ if err := os.WriteFile(socketPath, nil, 0644); err != nil {
305+ t.Fatal(err)
306+ }
307+ // XDG_RUNTIME_DIR controls where Wayland sockets are looked up
308+ t.Setenv("XDG_RUNTIME_DIR", tmpDir)
309+ if got := testHasUsableGTKDisplay(); !got {
310+ t.Errorf("testHasUsableGTKDisplay() = %v, want true with existing wayland socket", got)
311+ }
312+ })
313+
314+ t.Run("WAYLAND_DISPLAY without socket", func(t *testing.T) {
315+ t.Setenv("DISPLAY", "")
316+ t.Setenv("WAYLAND_DISPLAY", "wayland-nonexistent")
317+ t.Setenv("XDG_RUNTIME_DIR", t.TempDir())
318+ if got := testHasUsableGTKDisplay(); got {
319+ t.Errorf("testHasUsableGTKDisplay() = %v, want false without wayland socket", got)
320+ }
321+ })
322+
323+ t.Run("WAYLAND_DISPLAY absolute path with existing socket", func(t *testing.T) {
324+ t.Setenv("DISPLAY", "")
325+ socketPath := filepath.Join(t.TempDir(), "wayland.sock")
326+ if err := os.WriteFile(socketPath, nil, 0644); err != nil {
327+ t.Fatal(err)
328+ }
329+ t.Setenv("WAYLAND_DISPLAY", socketPath)
330+ t.Setenv("XDG_RUNTIME_DIR", "/nonexistent") // Should be ignored for absolute paths
331+ if got := testHasUsableGTKDisplay(); !got {
332+ t.Errorf("testHasUsableGTKDisplay() = %v, want true with absolute wayland socket path", got)
333+ }
334+ })
335+}
336+
337+func Test_testHasX11Auth_PureProbe(t *testing.T) {
338+ t.Run("DISPLAY unset", func(t *testing.T) {
339+ t.Setenv("DISPLAY", "")
340+ t.Setenv("XAUTHORITY", "")
341+ if got := testHasX11Auth(); !got {
342+ t.Errorf("testHasX11Auth() = %v, want true when DISPLAY unset", got)
343+ }
344+ })
345+
346+ t.Run("DISPLAY TCP remote", func(t *testing.T) {
347+ t.Setenv("DISPLAY", "remote-host:0")
348+ t.Setenv("XAUTHORITY", "")
349+ if got := testHasX11Auth(); !got {
350+ t.Errorf("testHasX11Auth() = %v, want true for TCP display", got)
351+ }
352+ })
353+
354+ t.Run("DISPLAY local no auth file", func(t *testing.T) {
355+ t.Setenv("DISPLAY", ":0")
356+ t.Setenv("XAUTHORITY", "")
357+ // Use a temp home dir with no .Xauthority
358+ t.Setenv("HOME", t.TempDir())
359+ if got := testHasX11Auth(); got {
360+ t.Errorf("testHasX11Auth() = %v, want false without auth file", got)
361+ }
362+ })
363+
364+ t.Run("DISPLAY local XAUTHORITY env missing file", func(t *testing.T) {
365+ t.Setenv("DISPLAY", ":0")
366+ t.Setenv("XAUTHORITY", "/nonexistent/.Xauthority")
367+ if got := testHasX11Auth(); got {
368+ t.Errorf("testHasX11Auth() = %v, want false when XAUTHORITY file missing", got)
369+ }
370+ })
371+
372+ t.Run("DISPLAY local XAUTHORITY with matching entry", func(t *testing.T) {
373+ t.Setenv("DISPLAY", ":1")
374+ authFile := filepath.Join(t.TempDir(), ".Xauthority")
375+ createXauthFile(t, authFile, "1", "MIT-MAGIC-COOKIE-1", []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10})
376+ t.Setenv("XAUTHORITY", authFile)
377+ if got := testHasX11Auth(); !got {
378+ t.Errorf("testHasX11Auth() = %v, want true with matching auth entry", got)
379+ }
380+ })
381+
382+ t.Run("DISPLAY local XAUTHORITY with non-matching entry", func(t *testing.T) {
383+ t.Setenv("DISPLAY", ":2")
384+ authFile := filepath.Join(t.TempDir(), ".Xauthority")
385+ createXauthFile(t, authFile, "0", "MIT-MAGIC-COOKIE-1", []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10})
386+ t.Setenv("XAUTHORITY", authFile)
387+ if got := testHasX11Auth(); got {
388+ t.Errorf("testHasX11Auth() = %v, want false with non-matching auth entry", got)
389+ }
390+ })
391+
392+ t.Run("DISPLAY local with screen number", func(t *testing.T) {
393+ t.Setenv("DISPLAY", ":0.0")
394+ authFile := filepath.Join(t.TempDir(), ".Xauthority")
395+ createXauthFile(t, authFile, "0", "MIT-MAGIC-COOKIE-1", []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10})
396+ t.Setenv("XAUTHORITY", authFile)
397+ if got := testHasX11Auth(); !got {
398+ t.Errorf("testHasX11Auth() = %v, want true with screen suffix stripped", got)
399+ }
400+ })
401+
402+ t.Run("DISPLAY colon only no number defaults to false", func(t *testing.T) {
403+ t.Setenv("DISPLAY", ":")
404+ t.Setenv("XAUTHORITY", "")
405+ if got := testHasX11Auth(); got {
406+ t.Errorf("testHasX11Auth() = %v, want false for empty display number", got)
407+ }
408+ })
409+}
410+
411+// writeXauthRecord appends one local-family .Xauthority record to buf with the given fields.
412+// If name or data is empty, the length byte is written as zero but the field
413+// data is omitted, producing a truncated record suitable for malformed tests.
414+func writeXauthRecord(buf *bytes.Buffer, num, name string, data []byte) {
415+ // family (2 bytes, big-endian)
416+ familyBytes := make([]byte, 2)
417+ binary.BigEndian.PutUint16(familyBytes, 256)
418+ buf.Write(familyBytes)
419+
420+ // address
421+ addrLen := make([]byte, 2)
422+ addr := "host"
423+ binary.BigEndian.PutUint16(addrLen, uint16(len(addr)))
424+ buf.Write(addrLen)
425+ buf.WriteString(addr)
426+
427+ // number
428+ numLen := make([]byte, 2)
429+ binary.BigEndian.PutUint16(numLen, uint16(len(num)))
430+ buf.Write(numLen)
431+ buf.WriteString(num)
432+
433+ // name
434+ nameLen := make([]byte, 2)
435+ binary.BigEndian.PutUint16(nameLen, uint16(len(name)))
436+ buf.Write(nameLen)
437+ if name != "" {
438+ buf.WriteString(name)
439+ }
440+
441+ // data
442+ dLen := make([]byte, 2)
443+ binary.BigEndian.PutUint16(dLen, uint16(len(data)))
444+ buf.Write(dLen)
445+ if len(data) > 0 {
446+ buf.Write(data)
447+ }
448+}
449+
450+func Test_testHasX11Auth_MalformedRecords(t *testing.T) {
451+ t.Run("matching display with empty name field", func(t *testing.T) {
452+ t.Setenv("DISPLAY", ":1")
453+ authFile := filepath.Join(t.TempDir(), ".Xauthority")
454+ var buf bytes.Buffer
455+ writeXauthRecord(&buf, "1", "", []byte{0x01, 0x02, 0x03, 0x04})
456+ if err := os.WriteFile(authFile, buf.Bytes(), 0644); err != nil {
457+ t.Fatal(err)
458+ }
459+ t.Setenv("XAUTHORITY", authFile)
460+ if got := testHasX11Auth(); got {
461+ t.Errorf("testHasX11Auth() = %v, want false for matching display with empty name", got)
462+ }
463+ })
464+
465+ t.Run("matching display with empty data field", func(t *testing.T) {
466+ t.Setenv("DISPLAY", ":1")
467+ authFile := filepath.Join(t.TempDir(), ".Xauthority")
468+ var buf bytes.Buffer
469+ writeXauthRecord(&buf, "1", "MIT-MAGIC-COOKIE-1", nil)
470+ if err := os.WriteFile(authFile, buf.Bytes(), 0644); err != nil {
471+ t.Fatal(err)
472+ }
473+ t.Setenv("XAUTHORITY", authFile)
474+ if got := testHasX11Auth(); got {
475+ t.Errorf("testHasX11Auth() = %v, want false for matching display with empty data", got)
476+ }
477+ })
478+
479+ t.Run("truncated record after display number", func(t *testing.T) {
480+ t.Setenv("DISPLAY", ":1")
481+ authFile := filepath.Join(t.TempDir(), ".Xauthority")
482+ var buf bytes.Buffer
483+ family := make([]byte, 2)
484+ binary.BigEndian.PutUint16(family, 256)
485+ buf.Write(family)
486+ addr := "host"
487+ addrLen := make([]byte, 2)
488+ binary.BigEndian.PutUint16(addrLen, uint16(len(addr)))
489+ buf.Write(addrLen)
490+ buf.WriteString(addr)
491+ num := "1"
492+ numLen := make([]byte, 2)
493+ binary.BigEndian.PutUint16(numLen, uint16(len(num)))
494+ buf.Write(numLen)
495+ buf.WriteString(num)
496+ // Record ends here — no name or data fields follow.
497+ if err := os.WriteFile(authFile, buf.Bytes(), 0644); err != nil {
498+ t.Fatal(err)
499+ }
500+ t.Setenv("XAUTHORITY", authFile)
501+ if got := testHasX11Auth(); got {
502+ t.Errorf("testHasX11Auth() = %v, want false for truncated record", got)
503+ }
504+ })
505+
506+ t.Run("multiple entries with one matching and valid", func(t *testing.T) {
507+ t.Setenv("DISPLAY", ":3")
508+ authFile := filepath.Join(t.TempDir(), ".Xauthority")
509+ var buf bytes.Buffer
510+ writeXauthRecord(&buf, "0", "MIT-MAGIC-COOKIE-1", []byte{0x01, 0x02, 0x03, 0x04})
511+ writeXauthRecord(&buf, "1", "MIT-MAGIC-COOKIE-1", []byte{0x05, 0x06, 0x07, 0x08})
512+ writeXauthRecord(&buf, "3", "MIT-MAGIC-COOKIE-1", []byte{0x09, 0x0a, 0x0b, 0x0c})
513+ if err := os.WriteFile(authFile, buf.Bytes(), 0644); err != nil {
514+ t.Fatal(err)
515+ }
516+ t.Setenv("XAUTHORITY", authFile)
517+ if got := testHasX11Auth(); !got {
518+ t.Errorf("testHasX11Auth() = %v, want true for third entry matching display :3", got)
519+ }
520+ })
521+}
522+
523+func Test_testHasUsableWaylandDisplay_PureProbe(t *testing.T) {
524+ t.Run("no WAYLAND_DISPLAY", func(t *testing.T) {
525+ t.Setenv("WAYLAND_DISPLAY", "")
526+ if got := testHasUsableWaylandDisplay(); got {
527+ t.Errorf("testHasUsableWaylandDisplay() = %v, want false", got)
528+ }
529+ })
530+
531+ t.Run("WAYLAND_DISPLAY with existing socket", func(t *testing.T) {
532+ t.Setenv("WAYLAND_DISPLAY", "wayland-1")
533+ tmpDir := t.TempDir()
534+ socketPath := filepath.Join(tmpDir, "wayland-1")
535+ if err := os.WriteFile(socketPath, nil, 0644); err != nil {
536+ t.Fatal(err)
537+ }
538+ t.Setenv("XDG_RUNTIME_DIR", tmpDir)
539+ if got := testHasUsableWaylandDisplay(); !got {
540+ t.Errorf("testHasUsableWaylandDisplay() = %v, want true with existing wayland socket", got)
541+ }
542+ })
543+
544+ t.Run("WAYLAND_DISPLAY absolute path", func(t *testing.T) {
545+ socketPath := filepath.Join(t.TempDir(), "wayland.sock")
546+ if err := os.WriteFile(socketPath, nil, 0644); err != nil {
547+ t.Fatal(err)
548+ }
549+ t.Setenv("WAYLAND_DISPLAY", socketPath)
550+ t.Setenv("XDG_RUNTIME_DIR", "/nonexistent")
551+ if got := testHasUsableWaylandDisplay(); !got {
552+ t.Errorf("testHasUsableWaylandDisplay() = %v, want true with absolute path", got)
553+ }
554+ })
555+}
556+
557+func Test_testHasUsableX11Display_PureProbe(t *testing.T) {
558+ t.Run("no DISPLAY", func(t *testing.T) {
559+ t.Setenv("DISPLAY", "")
560+ if got := testHasUsableX11Display(); got {
561+ t.Errorf("testHasUsableX11Display() = %v, want false", got)
562+ }
563+ })
564+
565+ t.Run("DISPLAY numeric without socket", func(t *testing.T) {
566+ t.Setenv("DISPLAY", ":99")
567+ if got := testHasUsableX11Display(); got {
568+ t.Errorf("testHasUsableX11Display() = %v, want false", got)
569+ }
570+ })
571+
572+ t.Run("DISPLAY TCP always passes socket check", func(t *testing.T) {
573+ t.Setenv("DISPLAY", "remote-host:0")
574+ if got := testHasUsableX11Display(); !got {
575+ t.Errorf("testHasUsableX11Display() = %v, want true for TCP display", got)
576+ }
577+ })
578+}
579+
580+func Test_testNeedsX11Auth_PureProbe(t *testing.T) {
581+ t.Run("Wayland available forces no X11 auth", func(t *testing.T) {
582+ t.Setenv("GDK_BACKEND", "")
583+ t.Setenv("WAYLAND_DISPLAY", "wayland-1")
584+ tmpDir := t.TempDir()
585+ socketPath := filepath.Join(tmpDir, "wayland-1")
586+ if err := os.WriteFile(socketPath, nil, 0644); err != nil {
587+ t.Fatal(err)
588+ }
589+ t.Setenv("XDG_RUNTIME_DIR", tmpDir)
590+ t.Setenv("DISPLAY", ":0")
591+ if got := testNeedsX11Auth(); got {
592+ t.Errorf("testNeedsX11Auth() = %v, want false when Wayland is available and GDK_BACKEND is unset", got)
593+ }
594+ })
595+
596+ t.Run("GDK_BACKEND=x11 forces X11 auth even with Wayland", func(t *testing.T) {
597+ t.Setenv("GDK_BACKEND", "x11")
598+ t.Setenv("WAYLAND_DISPLAY", "wayland-1")
599+ tmpDir := t.TempDir()
600+ socketPath := filepath.Join(tmpDir, "wayland-1")
601+ if err := os.WriteFile(socketPath, nil, 0644); err != nil {
602+ t.Fatal(err)
603+ }
604+ t.Setenv("XDG_RUNTIME_DIR", tmpDir)
605+ if got := testNeedsX11Auth(); !got {
606+ t.Errorf("testNeedsX11Auth() = %v, want true when GDK_BACKEND=x11", got)
607+ }
608+ })
609+
610+ t.Run("No Wayland fallback needs X11 auth", func(t *testing.T) {
611+ t.Setenv("GDK_BACKEND", "")
612+ t.Setenv("WAYLAND_DISPLAY", "")
613+ t.Setenv("DISPLAY", ":0")
614+ if got := testNeedsX11Auth(); !got {
615+ t.Errorf("testNeedsX11Auth() = %v, want true when only X11 is available", got)
616+ }
617+ })
618+}
619+
620+// createXauthFile writes a minimal .Xauthority file with one entry.
621+// family=256 (FamilyLocal), address=hostname, number=displayNum, name=proto, data=cookie.
622+func createXauthFile(t *testing.T, path, displayNum, proto string, cookie []byte) {
623+ t.Helper()
624+
625+ hostname, err := os.Hostname()
626+ if err != nil {
627+ hostname = "localhost"
628+ }
629+
630+ var buf bytes.Buffer
631+ // family (2 bytes, big-endian) - 256 = FamilyLocal
632+ family := make([]byte, 2)
633+ binary.BigEndian.PutUint16(family, 256)
634+ buf.Write(family)
635+
636+ // address length + address (hostname)
637+ addrLen := make([]byte, 2)
638+ binary.BigEndian.PutUint16(addrLen, uint16(len(hostname)))
639+ buf.Write(addrLen)
640+ buf.WriteString(hostname)
641+
642+ // number length + number (display number)
643+ numLen := make([]byte, 2)
644+ binary.BigEndian.PutUint16(numLen, uint16(len(displayNum)))
645+ buf.Write(numLen)
646+ buf.WriteString(displayNum)
647+
648+ // name length + name (auth protocol)
649+ nameLen := make([]byte, 2)
650+ binary.BigEndian.PutUint16(nameLen, uint16(len(proto)))
651+ buf.Write(nameLen)
652+ buf.WriteString(proto)
653+
654+ // data length + data (auth cookie)
655+ dataLen := make([]byte, 2)
656+ binary.BigEndian.PutUint16(dataLen, uint16(len(cookie)))
657+ buf.Write(dataLen)
658+ buf.Write(cookie)
659+
660+ if err := os.WriteFile(path, buf.Bytes(), 0644); err != nil {
661+ t.Fatal(err)
662+ }
663+}
664+
136665 // setDependencyField uses reflection to reach unexported dependency fields for tests.
137666 // Keep this test-only so production visibility stays narrow.
138667 func setDependencyField(t *testing.T, deps *Dependencies, field string, value any) {
@@ -1,7 +1,9 @@
1 package ui1 package ui
2 2
3 import (3 import (
4+ "bytes"
4 "context"5 "context"
6+ "encoding/binary"
5 "errors"7 "errors"
6 "io"8 "io"
7 "math"9 "math"
@@ -79,51 +81,160 @@ func (r *fakeSessionStateRepo) GetAllSnapshots(context.Context) ([]*entity.Sessi
79 81
80 func (r *fakeSessionStateRepo) GetTotalSnapshotsSize(context.Context) (int64, error) { return 0, nil }82 func (r *fakeSessionStateRepo) GetTotalSnapshotsSize(context.Context) (int64, error) { return 0, nil }
81 83
82-func testHasUsableGTKDisplay() bool {84+func testHasUsableWaylandDisplay() bool {
83- if waylandDisplay := os.Getenv("WAYLAND_DISPLAY"); waylandDisplay != "" {85+ waylandDisplay := os.Getenv("WAYLAND_DISPLAY")
84- candidates := []string{waylandDisplay}86+ if waylandDisplay == "" {
85- if runtimeDir := os.Getenv("XDG_RUNTIME_DIR"); runtimeDir != "" && !filepath.IsAbs(waylandDisplay) {87+ return false
86- candidates = append([]string{filepath.Join(runtimeDir, waylandDisplay)}, candidates...)88+ }
89+ candidates := []string{waylandDisplay}
90+ if runtimeDir := os.Getenv("XDG_RUNTIME_DIR"); runtimeDir != "" && !filepath.IsAbs(waylandDisplay) {
91+ candidates = append([]string{filepath.Join(runtimeDir, waylandDisplay)}, candidates...)
92+ }
93+ for _, candidate := range candidates {
94+ if _, err := os.Stat(candidate); err == nil {
95+ return true
87 }96 }
88- for _, candidate := range candidates {97+ }
98+ return false
99+}
100+
101+func testHasUsableX11Display() bool {
102+ display := os.Getenv("DISPLAY")
103+ if display == "" {
104+ return false
105+ }
106+ if strings.HasPrefix(display, ":") {
107+ displayNum := strings.TrimPrefix(display, ":")
108+ displayNum = strings.SplitN(displayNum, ".", 2)[0]
109+ if displayNum == "" {
110+ return false
111+ }
112+ x11SocketCandidates := []string{filepath.Join(os.TempDir(), ".X11-unix", "X"+displayNum)}
113+ if fallback := "/tmp/.X11-unix/X" + displayNum; fallback != x11SocketCandidates[0] {
114+ x11SocketCandidates = append(x11SocketCandidates, fallback)
115+ }
116+ for _, candidate := range x11SocketCandidates {
89 if _, err := os.Stat(candidate); err == nil {117 if _, err := os.Stat(candidate); err == nil {
90 return true118 return true
91 }119 }
92 }120 }
121+ return false
93 }122 }
123+ return true // TCP display
124+}
94 125
95- if display := os.Getenv("DISPLAY"); display != "" {126+func testHasUsableGTKDisplay() bool {
96- if strings.HasPrefix(display, ":") {127+ return testHasUsableWaylandDisplay() || testHasUsableX11Display()
97- displayNum := strings.TrimPrefix(display, ":")128+}
98- displayNum = strings.SplitN(displayNum, ".", 2)[0]129+
99- if displayNum == "" {130+// testHasX11Auth checks whether X11 authorization is available for the current
100- return false131+// DISPLAY before any GTK/Adwaita initialization. It is a pure function that
101- }132+// inspects env vars and the .Xauthority file only; no GTK calls.
102- x11SocketCandidates := []string{filepath.Join(os.TempDir(), ".X11-unix", "X"+displayNum)}133+func testHasX11Auth() bool {
103- if fallback := "/tmp/.X11-unix/X" + displayNum; fallback != x11SocketCandidates[0] {134+ display := os.Getenv("DISPLAY")
104- x11SocketCandidates = append(x11SocketCandidates, fallback)135+ if display == "" || !strings.HasPrefix(display, ":") {
105- }136+ // No DISPLAY or TCP-style display; can't check locally.
106- for _, candidate := range x11SocketCandidates {137+ // Let GTK try and fall through to gdk.DisplayGetDefault().
107- if _, err := os.Stat(candidate); err == nil {138+ return true
108- return true139+ }
109- }140+
110- }141+ displayNum := strings.TrimPrefix(display, ":")
142+ displayNum = strings.SplitN(displayNum, ".", 2)[0]
143+ if displayNum == "" {
144+ return false
145+ }
146+
147+ // Find the X authority file.
148+ authFile := os.Getenv("XAUTHORITY")
149+ if authFile == "" {
150+ home, err := os.UserHomeDir()
151+ if err != nil {
111 return false152 return false
112 }153 }
113- return true154+ authFile = filepath.Join(home, ".Xauthority")
155+ }
156+
157+ if _, err := os.Stat(authFile); err != nil {
158+ return false // No authority file; X11 auth will fail.
114 }159 }
115 160
161+ // Parse the .Xauthority file to look for a matching display entry.
162+ // Format: sequence of {family(2B), addrLen(2B), addr, numLen(2B), num, nameLen(2B), name, dataLen(2B), data}
163+ // All multi-byte values are big-endian (network byte order).
164+ data, err := os.ReadFile(authFile)
165+ if err != nil {
166+ return false
167+ }
168+
169+ return testXauthorityHasDisplay(data, displayNum)
170+}
171+
172+func testXauthorityHasDisplay(data []byte, displayNum string) bool {
173+ for len(data) > 0 {
174+ if len(data) < 2 {
175+ return false
176+ }
177+ data = data[2:] // family, unused
178+
179+ _, rest, ok := testReadXauthorityField(data) // address
180+ if !ok {
181+ return false
182+ }
183+ num, rest, ok := testReadXauthorityField(rest)
184+ if !ok {
185+ return false
186+ }
187+ name, rest, ok := testReadXauthorityField(rest)
188+ if !ok {
189+ return false
190+ }
191+ cookie, rest, ok := testReadXauthorityField(rest)
192+ if !ok {
193+ return false
194+ }
195+ if string(num) == displayNum {
196+ return len(name) > 0 && len(cookie) > 0
197+ }
198+ data = rest
199+ }
116 return false200 return false
117 }201 }
118 202
203+func testReadXauthorityField(data []byte) ([]byte, []byte, bool) {
204+ if len(data) < 2 {
205+ return nil, nil, false
206+ }
207+ fieldLen := int(binary.BigEndian.Uint16(data[:2]))
208+ data = data[2:]
209+ if len(data) < fieldLen {
210+ return nil, nil, false
211+ }
212+ return data[:fieldLen], data[fieldLen:], true
213+}
214+
215+// testNeedsX11Auth reports whether GTK is expected to use the X11 backend,
216+// meaning X11 authorization must be available. When a usable Wayland display
217+// is present and GDK_BACKEND is not forced to x11, X11 auth is not required
218+// because GTK will use the Wayland backend.
219+func testNeedsX11Auth() bool {
220+ return !testHasUsableWaylandDisplay() || os.Getenv("GDK_BACKEND") == "x11"
221+}
222+
119 func requireGTKDisplayApp(t *testing.T) *gtk.Application {223 func requireGTKDisplayApp(t *testing.T) *gtk.Application {
120 t.Helper()224 t.Helper()
121- if !testHasUsableGTKDisplay() {225+ hasWayland := testHasUsableWaylandDisplay()
122- t.Skip("GTK display not available")226+ hasX11 := testHasUsableX11Display()
227+ if !hasWayland && !hasX11 {
228+ t.Skip("GTK display not available (no DISPLAY/WAYLAND_DISPLAY socket)")
229+ }
230+ // Only require X11 auth when the X11 backend will be used:
231+ // no Wayland (fallback to X11), or GDK_BACKEND explicitly set to x11.
232+ if testNeedsX11Auth() && !testHasX11Auth() {
233+ t.Skip("GTK display not available (X11 authorization unavailable)")
123 }234 }
124 EnsureAdwaitaInitialized()235 EnsureAdwaitaInitialized()
125 if gdk.DisplayGetDefault() == nil {236 if gdk.DisplayGetDefault() == nil {
126- t.Skip("GTK display not available")237+ t.Skip("GTK display not available (gdk.DisplayGetDefault returned nil)")
127 }238 }
128 appID := AppID239 appID := AppID
129 gtkApp := gtk.NewApplication(&appID, gio.GApplicationNonUniqueValue)240 gtkApp := gtk.NewApplication(&appID, gio.GApplicationNonUniqueValue)
@@ -133,6 +244,424 @@ func requireGTKDisplayApp(t *testing.T) *gtk.Application {
133 return gtkApp244 return gtkApp
134 }245 }
135 246
247+func Test_testHasUsableGTKDisplay_PureProbe(t *testing.T) {
248+ t.Run("no display env vars", func(t *testing.T) {
249+ t.Setenv("DISPLAY", "")
250+ t.Setenv("WAYLAND_DISPLAY", "")
251+ if got := testHasUsableGTKDisplay(); got {
252+ t.Errorf("testHasUsableGTKDisplay() = %v, want false", got)
253+ }
254+ })
255+
256+ t.Run("DISPLAY numeric without socket", func(t *testing.T) {
257+ t.Setenv("DISPLAY", ":99")
258+ t.Setenv("WAYLAND_DISPLAY", "")
259+ // No /tmp/.X11-unix/X99 socket exists
260+ if got := testHasUsableGTKDisplay(); got {
261+ t.Errorf("testHasUsableGTKDisplay() = %v, want false", got)
262+ }
263+ })
264+
265+ t.Run("DISPLAY with screen without socket", func(t *testing.T) {
266+ t.Setenv("DISPLAY", ":99.0")
267+ t.Setenv("WAYLAND_DISPLAY", "")
268+ if got := testHasUsableGTKDisplay(); got {
269+ t.Errorf("testHasUsableGTKDisplay() = %v, want false", got)
270+ }
271+ })
272+
273+ t.Run("DISPLAY colon only no number", func(t *testing.T) {
274+ t.Setenv("DISPLAY", ":")
275+ t.Setenv("WAYLAND_DISPLAY", "")
276+ if got := testHasUsableGTKDisplay(); got {
277+ t.Errorf("testHasUsableGTKDisplay() = %v, want false for empty display number", got)
278+ }
279+ })
280+
281+ t.Run("DISPLAY TCP always passes socket check", func(t *testing.T) {
282+ t.Setenv("DISPLAY", "remote-host:0")
283+ t.Setenv("WAYLAND_DISPLAY", "")
284+ if got := testHasUsableGTKDisplay(); !got {
285+ t.Errorf("testHasUsableGTKDisplay() = %v, want true for TCP display", got)
286+ }
287+ })
288+
289+ t.Run("DISPLAY with existing socket", func(t *testing.T) {
290+ t.Setenv("DISPLAY", ":0")
291+ t.Setenv("WAYLAND_DISPLAY", "")
292+ // /tmp/.X11-unix/X0 exists on this machine
293+ got := testHasUsableGTKDisplay()
294+ // We can't guarantee the socket exists in all environments,
295+ // but this verifies it doesn't crash.
296+ t.Logf("testHasUsableGTKDisplay() = %v (depends on /tmp/.X11-unix/X0)", got)
297+ })
298+
299+ t.Run("WAYLAND_DISPLAY with existing socket", func(t *testing.T) {
300+ t.Setenv("DISPLAY", "")
301+ t.Setenv("WAYLAND_DISPLAY", "wayland-1")
302+ tmpDir := t.TempDir()
303+ socketPath := filepath.Join(tmpDir, "wayland-1")
304+ if err := os.WriteFile(socketPath, nil, 0644); err != nil {
305+ t.Fatal(err)
306+ }
307+ // XDG_RUNTIME_DIR controls where Wayland sockets are looked up
308+ t.Setenv("XDG_RUNTIME_DIR", tmpDir)
309+ if got := testHasUsableGTKDisplay(); !got {
310+ t.Errorf("testHasUsableGTKDisplay() = %v, want true with existing wayland socket", got)
311+ }
312+ })
313+
314+ t.Run("WAYLAND_DISPLAY without socket", func(t *testing.T) {
315+ t.Setenv("DISPLAY", "")
316+ t.Setenv("WAYLAND_DISPLAY", "wayland-nonexistent")
317+ t.Setenv("XDG_RUNTIME_DIR", t.TempDir())
318+ if got := testHasUsableGTKDisplay(); got {
319+ t.Errorf("testHasUsableGTKDisplay() = %v, want false without wayland socket", got)
320+ }
321+ })
322+
323+ t.Run("WAYLAND_DISPLAY absolute path with existing socket", func(t *testing.T) {
324+ t.Setenv("DISPLAY", "")
325+ socketPath := filepath.Join(t.TempDir(), "wayland.sock")
326+ if err := os.WriteFile(socketPath, nil, 0644); err != nil {
327+ t.Fatal(err)
328+ }
329+ t.Setenv("WAYLAND_DISPLAY", socketPath)
330+ t.Setenv("XDG_RUNTIME_DIR", "/nonexistent") // Should be ignored for absolute paths
331+ if got := testHasUsableGTKDisplay(); !got {
332+ t.Errorf("testHasUsableGTKDisplay() = %v, want true with absolute wayland socket path", got)
333+ }
334+ })
335+}
336+
337+func Test_testHasX11Auth_PureProbe(t *testing.T) {
338+ t.Run("DISPLAY unset", func(t *testing.T) {
339+ t.Setenv("DISPLAY", "")
340+ t.Setenv("XAUTHORITY", "")
341+ if got := testHasX11Auth(); !got {
342+ t.Errorf("testHasX11Auth() = %v, want true when DISPLAY unset", got)
343+ }
344+ })
345+
346+ t.Run("DISPLAY TCP remote", func(t *testing.T) {
347+ t.Setenv("DISPLAY", "remote-host:0")
348+ t.Setenv("XAUTHORITY", "")
349+ if got := testHasX11Auth(); !got {
350+ t.Errorf("testHasX11Auth() = %v, want true for TCP display", got)
351+ }
352+ })
353+
354+ t.Run("DISPLAY local no auth file", func(t *testing.T) {
355+ t.Setenv("DISPLAY", ":0")
356+ t.Setenv("XAUTHORITY", "")
357+ // Use a temp home dir with no .Xauthority
358+ t.Setenv("HOME", t.TempDir())
359+ if got := testHasX11Auth(); got {
360+ t.Errorf("testHasX11Auth() = %v, want false without auth file", got)
361+ }
362+ })
363+
364+ t.Run("DISPLAY local XAUTHORITY env missing file", func(t *testing.T) {
365+ t.Setenv("DISPLAY", ":0")
366+ t.Setenv("XAUTHORITY", "/nonexistent/.Xauthority")
367+ if got := testHasX11Auth(); got {
368+ t.Errorf("testHasX11Auth() = %v, want false when XAUTHORITY file missing", got)
369+ }
370+ })
371+
372+ t.Run("DISPLAY local XAUTHORITY with matching entry", func(t *testing.T) {
373+ t.Setenv("DISPLAY", ":1")
374+ authFile := filepath.Join(t.TempDir(), ".Xauthority")
375+ createXauthFile(t, authFile, "1", "MIT-MAGIC-COOKIE-1", []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10})
376+ t.Setenv("XAUTHORITY", authFile)
377+ if got := testHasX11Auth(); !got {
378+ t.Errorf("testHasX11Auth() = %v, want true with matching auth entry", got)
379+ }
380+ })
381+
382+ t.Run("DISPLAY local XAUTHORITY with non-matching entry", func(t *testing.T) {
383+ t.Setenv("DISPLAY", ":2")
384+ authFile := filepath.Join(t.TempDir(), ".Xauthority")
385+ createXauthFile(t, authFile, "0", "MIT-MAGIC-COOKIE-1", []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10})
386+ t.Setenv("XAUTHORITY", authFile)
387+ if got := testHasX11Auth(); got {
388+ t.Errorf("testHasX11Auth() = %v, want false with non-matching auth entry", got)
389+ }
390+ })
391+
392+ t.Run("DISPLAY local with screen number", func(t *testing.T) {
393+ t.Setenv("DISPLAY", ":0.0")
394+ authFile := filepath.Join(t.TempDir(), ".Xauthority")
395+ createXauthFile(t, authFile, "0", "MIT-MAGIC-COOKIE-1", []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10})
396+ t.Setenv("XAUTHORITY", authFile)
397+ if got := testHasX11Auth(); !got {
398+ t.Errorf("testHasX11Auth() = %v, want true with screen suffix stripped", got)
399+ }
400+ })
401+
402+ t.Run("DISPLAY colon only no number defaults to false", func(t *testing.T) {
403+ t.Setenv("DISPLAY", ":")
404+ t.Setenv("XAUTHORITY", "")
405+ if got := testHasX11Auth(); got {
406+ t.Errorf("testHasX11Auth() = %v, want false for empty display number", got)
407+ }
408+ })
409+}
410+
411+// writeXauthRecord appends one local-family .Xauthority record to buf with the given fields.
412+// If name or data is empty, the length byte is written as zero but the field
413+// data is omitted, producing a truncated record suitable for malformed tests.
414+func writeXauthRecord(buf *bytes.Buffer, num, name string, data []byte) {
415+ // family (2 bytes, big-endian)
416+ familyBytes := make([]byte, 2)
417+ binary.BigEndian.PutUint16(familyBytes, 256)
418+ buf.Write(familyBytes)
419+
420+ // address
421+ addrLen := make([]byte, 2)
422+ addr := "host"
423+ binary.BigEndian.PutUint16(addrLen, uint16(len(addr)))
424+ buf.Write(addrLen)
425+ buf.WriteString(addr)
426+
427+ // number
428+ numLen := make([]byte, 2)
429+ binary.BigEndian.PutUint16(numLen, uint16(len(num)))
430+ buf.Write(numLen)
431+ buf.WriteString(num)
432+
433+ // name
434+ nameLen := make([]byte, 2)
435+ binary.BigEndian.PutUint16(nameLen, uint16(len(name)))
436+ buf.Write(nameLen)
437+ if name != "" {
438+ buf.WriteString(name)
439+ }
440+
441+ // data
442+ dLen := make([]byte, 2)
443+ binary.BigEndian.PutUint16(dLen, uint16(len(data)))
444+ buf.Write(dLen)
445+ if len(data) > 0 {
446+ buf.Write(data)
447+ }
448+}
449+
450+func Test_testHasX11Auth_MalformedRecords(t *testing.T) {
451+ t.Run("matching display with empty name field", func(t *testing.T) {
452+ t.Setenv("DISPLAY", ":1")
453+ authFile := filepath.Join(t.TempDir(), ".Xauthority")
454+ var buf bytes.Buffer
455+ writeXauthRecord(&buf, "1", "", []byte{0x01, 0x02, 0x03, 0x04})
456+ if err := os.WriteFile(authFile, buf.Bytes(), 0644); err != nil {
457+ t.Fatal(err)
458+ }
459+ t.Setenv("XAUTHORITY", authFile)
460+ if got := testHasX11Auth(); got {
461+ t.Errorf("testHasX11Auth() = %v, want false for matching display with empty name", got)
462+ }
463+ })
464+
465+ t.Run("matching display with empty data field", func(t *testing.T) {
466+ t.Setenv("DISPLAY", ":1")
467+ authFile := filepath.Join(t.TempDir(), ".Xauthority")
468+ var buf bytes.Buffer
469+ writeXauthRecord(&buf, "1", "MIT-MAGIC-COOKIE-1", nil)
470+ if err := os.WriteFile(authFile, buf.Bytes(), 0644); err != nil {
471+ t.Fatal(err)
472+ }
473+ t.Setenv("XAUTHORITY", authFile)
474+ if got := testHasX11Auth(); got {
475+ t.Errorf("testHasX11Auth() = %v, want false for matching display with empty data", got)
476+ }
477+ })
478+
479+ t.Run("truncated record after display number", func(t *testing.T) {
480+ t.Setenv("DISPLAY", ":1")
481+ authFile := filepath.Join(t.TempDir(), ".Xauthority")
482+ var buf bytes.Buffer
483+ family := make([]byte, 2)
484+ binary.BigEndian.PutUint16(family, 256)
485+ buf.Write(family)
486+ addr := "host"
487+ addrLen := make([]byte, 2)
488+ binary.BigEndian.PutUint16(addrLen, uint16(len(addr)))
489+ buf.Write(addrLen)
490+ buf.WriteString(addr)
491+ num := "1"
492+ numLen := make([]byte, 2)
493+ binary.BigEndian.PutUint16(numLen, uint16(len(num)))
494+ buf.Write(numLen)
495+ buf.WriteString(num)
496+ // Record ends here — no name or data fields follow.
497+ if err := os.WriteFile(authFile, buf.Bytes(), 0644); err != nil {
498+ t.Fatal(err)
499+ }
500+ t.Setenv("XAUTHORITY", authFile)
501+ if got := testHasX11Auth(); got {
502+ t.Errorf("testHasX11Auth() = %v, want false for truncated record", got)
503+ }
504+ })
505+
506+ t.Run("multiple entries with one matching and valid", func(t *testing.T) {
507+ t.Setenv("DISPLAY", ":3")
508+ authFile := filepath.Join(t.TempDir(), ".Xauthority")
509+ var buf bytes.Buffer
510+ writeXauthRecord(&buf, "0", "MIT-MAGIC-COOKIE-1", []byte{0x01, 0x02, 0x03, 0x04})
511+ writeXauthRecord(&buf, "1", "MIT-MAGIC-COOKIE-1", []byte{0x05, 0x06, 0x07, 0x08})
512+ writeXauthRecord(&buf, "3", "MIT-MAGIC-COOKIE-1", []byte{0x09, 0x0a, 0x0b, 0x0c})
513+ if err := os.WriteFile(authFile, buf.Bytes(), 0644); err != nil {
514+ t.Fatal(err)
515+ }
516+ t.Setenv("XAUTHORITY", authFile)
517+ if got := testHasX11Auth(); !got {
518+ t.Errorf("testHasX11Auth() = %v, want true for third entry matching display :3", got)
519+ }
520+ })
521+}
522+
523+func Test_testHasUsableWaylandDisplay_PureProbe(t *testing.T) {
524+ t.Run("no WAYLAND_DISPLAY", func(t *testing.T) {
525+ t.Setenv("WAYLAND_DISPLAY", "")
526+ if got := testHasUsableWaylandDisplay(); got {
527+ t.Errorf("testHasUsableWaylandDisplay() = %v, want false", got)
528+ }
529+ })
530+
531+ t.Run("WAYLAND_DISPLAY with existing socket", func(t *testing.T) {
532+ t.Setenv("WAYLAND_DISPLAY", "wayland-1")
533+ tmpDir := t.TempDir()
534+ socketPath := filepath.Join(tmpDir, "wayland-1")
535+ if err := os.WriteFile(socketPath, nil, 0644); err != nil {
536+ t.Fatal(err)
537+ }
538+ t.Setenv("XDG_RUNTIME_DIR", tmpDir)
539+ if got := testHasUsableWaylandDisplay(); !got {
540+ t.Errorf("testHasUsableWaylandDisplay() = %v, want true with existing wayland socket", got)
541+ }
542+ })
543+
544+ t.Run("WAYLAND_DISPLAY absolute path", func(t *testing.T) {
545+ socketPath := filepath.Join(t.TempDir(), "wayland.sock")
546+ if err := os.WriteFile(socketPath, nil, 0644); err != nil {
547+ t.Fatal(err)
548+ }
549+ t.Setenv("WAYLAND_DISPLAY", socketPath)
550+ t.Setenv("XDG_RUNTIME_DIR", "/nonexistent")
551+ if got := testHasUsableWaylandDisplay(); !got {
552+ t.Errorf("testHasUsableWaylandDisplay() = %v, want true with absolute path", got)
553+ }
554+ })
555+}
556+
557+func Test_testHasUsableX11Display_PureProbe(t *testing.T) {
558+ t.Run("no DISPLAY", func(t *testing.T) {
559+ t.Setenv("DISPLAY", "")
560+ if got := testHasUsableX11Display(); got {
561+ t.Errorf("testHasUsableX11Display() = %v, want false", got)
562+ }
563+ })
564+
565+ t.Run("DISPLAY numeric without socket", func(t *testing.T) {
566+ t.Setenv("DISPLAY", ":99")
567+ if got := testHasUsableX11Display(); got {
568+ t.Errorf("testHasUsableX11Display() = %v, want false", got)
569+ }
570+ })
571+
572+ t.Run("DISPLAY TCP always passes socket check", func(t *testing.T) {
573+ t.Setenv("DISPLAY", "remote-host:0")
574+ if got := testHasUsableX11Display(); !got {
575+ t.Errorf("testHasUsableX11Display() = %v, want true for TCP display", got)
576+ }
577+ })
578+}
579+
580+func Test_testNeedsX11Auth_PureProbe(t *testing.T) {
581+ t.Run("Wayland available forces no X11 auth", func(t *testing.T) {
582+ t.Setenv("GDK_BACKEND", "")
583+ t.Setenv("WAYLAND_DISPLAY", "wayland-1")
584+ tmpDir := t.TempDir()
585+ socketPath := filepath.Join(tmpDir, "wayland-1")
586+ if err := os.WriteFile(socketPath, nil, 0644); err != nil {
587+ t.Fatal(err)
588+ }
589+ t.Setenv("XDG_RUNTIME_DIR", tmpDir)
590+ t.Setenv("DISPLAY", ":0")
591+ if got := testNeedsX11Auth(); got {
592+ t.Errorf("testNeedsX11Auth() = %v, want false when Wayland is available and GDK_BACKEND is unset", got)
593+ }
594+ })
595+
596+ t.Run("GDK_BACKEND=x11 forces X11 auth even with Wayland", func(t *testing.T) {
597+ t.Setenv("GDK_BACKEND", "x11")
598+ t.Setenv("WAYLAND_DISPLAY", "wayland-1")
599+ tmpDir := t.TempDir()
600+ socketPath := filepath.Join(tmpDir, "wayland-1")
601+ if err := os.WriteFile(socketPath, nil, 0644); err != nil {
602+ t.Fatal(err)
603+ }
604+ t.Setenv("XDG_RUNTIME_DIR", tmpDir)
605+ if got := testNeedsX11Auth(); !got {
606+ t.Errorf("testNeedsX11Auth() = %v, want true when GDK_BACKEND=x11", got)
607+ }
608+ })
609+
610+ t.Run("No Wayland fallback needs X11 auth", func(t *testing.T) {
611+ t.Setenv("GDK_BACKEND", "")
612+ t.Setenv("WAYLAND_DISPLAY", "")
613+ t.Setenv("DISPLAY", ":0")
614+ if got := testNeedsX11Auth(); !got {
615+ t.Errorf("testNeedsX11Auth() = %v, want true when only X11 is available", got)
616+ }
617+ })
618+}
619+
620+// createXauthFile writes a minimal .Xauthority file with one entry.
621+// family=256 (FamilyLocal), address=hostname, number=displayNum, name=proto, data=cookie.
622+func createXauthFile(t *testing.T, path, displayNum, proto string, cookie []byte) {
623+ t.Helper()
624+
625+ hostname, err := os.Hostname()
626+ if err != nil {
627+ hostname = "localhost"
628+ }
629+
630+ var buf bytes.Buffer
631+ // family (2 bytes, big-endian) - 256 = FamilyLocal
632+ family := make([]byte, 2)
633+ binary.BigEndian.PutUint16(family, 256)
634+ buf.Write(family)
635+
636+ // address length + address (hostname)
637+ addrLen := make([]byte, 2)
638+ binary.BigEndian.PutUint16(addrLen, uint16(len(hostname)))
639+ buf.Write(addrLen)
640+ buf.WriteString(hostname)
641+
642+ // number length + number (display number)
643+ numLen := make([]byte, 2)
644+ binary.BigEndian.PutUint16(numLen, uint16(len(displayNum)))
645+ buf.Write(numLen)
646+ buf.WriteString(displayNum)
647+
648+ // name length + name (auth protocol)
649+ nameLen := make([]byte, 2)
650+ binary.BigEndian.PutUint16(nameLen, uint16(len(proto)))
651+ buf.Write(nameLen)
652+ buf.WriteString(proto)
653+
654+ // data length + data (auth cookie)
655+ dataLen := make([]byte, 2)
656+ binary.BigEndian.PutUint16(dataLen, uint16(len(cookie)))
657+ buf.Write(dataLen)
658+ buf.Write(cookie)
659+
660+ if err := os.WriteFile(path, buf.Bytes(), 0644); err != nil {
661+ t.Fatal(err)
662+ }
663+}
664+
136 // setDependencyField uses reflection to reach unexported dependency fields for tests.665 // setDependencyField uses reflection to reach unexported dependency fields for tests.
137 // Keep this test-only so production visibility stays narrow.666 // Keep this test-only so production visibility stays narrow.
138 func setDependencyField(t *testing.T, deps *Dependencies, field string, value any) {667 func setDependencyField(t *testing.T, deps *Dependencies, field string, value any) {

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