返回 DeepSeek-Reasonix
external_opener_linux_test.go
根目录 / desktop / external_opener_linux_test.go
1 //go:build !darwin && !windows
2
3 package main
4
5 import (
6 "errors"
7 "os"
8 "path/filepath"
9 "reflect"
10 "testing"
11 )
12
13 func writeLinuxDesktopFile(t *testing.T, dir, name, content string) string {
14 t.Helper()
15 path := filepath.Join(dir, name)
16 if err := os.WriteFile(path, []byte(content), 0o600); err != nil {
17 t.Fatal(err)
18 }
19 return path
20 }
21
22 func TestParseLinuxDesktopEntryReadsDesktopEntrySectionOnly(t *testing.T) {
23 dir := t.TempDir()
24 path := writeLinuxDesktopFile(t, dir, "code.desktop", `[Desktop Entry]
25 Type=Application
26 Name=Visual Studio Code
27 Icon=vscode
28
29 [Desktop Action new-window]
30 Name=New Window
31 Icon=new-window
32 `)
33 entry, ok := parseLinuxDesktopEntry(path)
34 if !ok {
35 t.Fatal("valid desktop entry was rejected")
36 }
37 want := linuxDesktopEntry{Path: path, ID: "code", Name: "Visual Studio Code", Icon: "vscode"}
38 if entry != want {
39 t.Fatalf("parseLinuxDesktopEntry = %+v, want %+v", entry, want)
40 }
41 }
42
43 func TestParseLinuxDesktopEntryRejectsHiddenAndNonApplications(t *testing.T) {
44 dir := t.TempDir()
45 for name, content := range map[string]string{
46 "hidden.desktop": "[Desktop Entry]\nType=Application\nName=Ghost\nHidden=true\n",
47 "link.desktop": "[Desktop Entry]\nType=Link\nName=Bookmark\n",
48 "unnamed.desktop": "[Desktop Entry]\nType=Application\n",
49 "wrong-section.desktop": "[Desktop Action open]\nName=Open\n",
50 } {
51 path := writeLinuxDesktopFile(t, dir, name, content)
52 if entry, ok := parseLinuxDesktopEntry(path); ok {
53 t.Errorf("%s was accepted as %+v, want rejection", name, entry)
54 }
55 }
56 }
57
58 func TestFindLinuxDesktopEntryMatchesNormalizedAliasesThenSubstrings(t *testing.T) {
59 entries := []linuxDesktopEntry{
60 {ID: "org.gnome.Terminal", Name: "Terminal"},
61 {ID: "code", Name: "Visual Studio Code"},
62 {ID: "jetbrains-idea", Name: "IntelliJ IDEA Ultimate Edition"},
63 {ID: "kitty", Name: "kitty"},
64 }
65 if entry, ok := findLinuxDesktopEntry(entries, "org.gnome.terminal"); !ok || entry.ID != "org.gnome.Terminal" {
66 t.Fatalf("normalized id lookup = (%+v, %v), want org.gnome.Terminal", entry, ok)
67 }
68 if entry, ok := findLinuxDesktopEntry(entries, "visual-studio-code"); !ok || entry.ID != "code" {
69 t.Fatalf("normalized name lookup = (%+v, %v), want code", entry, ok)
70 }
71 if entry, ok := findLinuxDesktopEntry(entries, "intellij-idea"); !ok || entry.ID != "jetbrains-idea" {
72 t.Fatalf("substring lookup = (%+v, %v), want jetbrains-idea", entry, ok)
73 }
74 if entry, ok := findLinuxDesktopEntry(entries, "kit"); ok {
75 t.Fatalf("short alias %q unexpectedly matched %+v via substring", "kit", entry)
76 }
77 }
78
79 func TestLinuxExternalOpenerCommandPerTerminalArguments(t *testing.T) {
80 const workdir = "/tmp/reasonix workspace"
81 cases := []struct {
82 mode string
83 target string
84 want []string
85 }{
86 {"path", "/usr/bin/code", []string{"/usr/bin/code", workdir}},
87 {"ghostty", "/usr/bin/ghostty", []string{"/usr/bin/ghostty", "--working-directory=" + workdir}},
88 {"gnome-terminal", "/usr/bin/gnome-terminal", []string{"/usr/bin/gnome-terminal", "--working-directory=" + workdir}},
89 {"konsole", "/usr/bin/konsole", []string{"/usr/bin/konsole", "--workdir", workdir}},
90 {"kitty", "/usr/bin/kitty", []string{"/usr/bin/kitty", "--directory", workdir}},
91 {"alacritty", "/usr/bin/alacritty", []string{"/usr/bin/alacritty", "--working-directory", workdir}},
92 {"cwd", "/usr/bin/x-terminal-emulator", []string{"/usr/bin/x-terminal-emulator"}},
93 }
94 for _, tc := range cases {
95 spec := externalOpenerSpec{Target: tc.target, LaunchMode: tc.mode}
96 cmd, err := linuxExternalOpenerCommand(spec, workdir)
97 if err != nil {
98 t.Errorf("%s: linuxExternalOpenerCommand error = %v", tc.mode, err)
99 continue
100 }
101 if !reflect.DeepEqual(cmd.Args, tc.want) {
102 t.Errorf("%s: args = %#v, want %#v", tc.mode, cmd.Args, tc.want)
103 }
104 if cmd.Dir != workdir {
105 t.Errorf("%s: dir = %q, want workspace directory", tc.mode, cmd.Dir)
106 }
107 }
108 }
109
110 func TestLinuxExternalOpenerCommandUsesParentForRegularFiles(t *testing.T) {
111 dir := t.TempDir()
112 path := filepath.Join(dir, "report.md")
113 if err := os.WriteFile(path, []byte("report"), 0o600); err != nil {
114 t.Fatal(err)
115 }
116
117 cases := []struct {
118 name string
119 spec externalOpenerSpec
120 want []string
121 }{
122 {
123 name: "editor",
124 spec: externalOpenerSpec{View: ExternalOpenerView{Kind: externalOpenerEditor}, Target: "/usr/bin/code", LaunchMode: "path"},
125 want: []string{"/usr/bin/code", path},
126 },
127 {
128 name: "terminal",
129 spec: externalOpenerSpec{View: ExternalOpenerView{Kind: externalOpenerTerminal}, Target: "/usr/bin/ghostty", LaunchMode: "ghostty"},
130 want: []string{"/usr/bin/ghostty", "--working-directory=" + dir},
131 },
132 }
133 for _, tc := range cases {
134 t.Run(tc.name, func(t *testing.T) {
135 cmd, err := linuxExternalOpenerCommand(tc.spec, path)
136 if err != nil {
137 t.Fatal(err)
138 }
139 if !reflect.DeepEqual(cmd.Args, tc.want) {
140 t.Fatalf("args = %#v, want %#v", cmd.Args, tc.want)
141 }
142 if cmd.Dir != dir {
143 t.Fatalf("dir = %q, want parent directory %q", cmd.Dir, dir)
144 }
145 })
146 }
147 }
148
149 func TestLinuxExternalOpenerCommandLaunchesDesktopEntriesViaGio(t *testing.T) {
150 binDir := t.TempDir()
151 gio := filepath.Join(binDir, "gio")
152 if err := os.WriteFile(gio, []byte("#!/bin/sh\n"), 0o755); err != nil {
153 t.Fatal(err)
154 }
155 t.Setenv("PATH", binDir)
156
157 spec := externalOpenerSpec{Target: "/usr/share/applications/code.desktop", LaunchMode: "gio"}
158 cmd, err := linuxExternalOpenerCommand(spec, "/tmp/project")
159 if err != nil {
160 t.Fatalf("linuxExternalOpenerCommand error = %v", err)
161 }
162 want := []string{gio, "launch", "/usr/share/applications/code.desktop", "/tmp/project"}
163 if !reflect.DeepEqual(cmd.Args, want) {
164 t.Fatalf("gio args = %#v, want %#v", cmd.Args, want)
165 }
166
167 t.Setenv("PATH", t.TempDir())
168 if _, err := linuxExternalOpenerCommand(spec, "/tmp/project"); !errors.Is(err, os.ErrNotExist) {
169 t.Fatalf("missing gio error = %v, want os.ErrNotExist", err)
170 }
171 }
172
173 func TestResolveLinuxDesktopIconAcceptsOnlyExistingAbsoluteFiles(t *testing.T) {
174 dir := t.TempDir()
175 icon := filepath.Join(dir, "vscode.png")
176 if err := os.WriteFile(icon, []byte("png"), 0o600); err != nil {
177 t.Fatal(err)
178 }
179 if got := resolveLinuxDesktopIcon(icon); got != icon {
180 t.Fatalf("existing absolute icon = %q, want %q", got, icon)
181 }
182 if got := resolveLinuxDesktopIcon(filepath.Join(dir, "missing.png")); got != "" {
183 t.Fatalf("missing absolute icon = %q, want empty", got)
184 }
185 if got := resolveLinuxDesktopIcon(dir); got != "" {
186 t.Fatalf("directory icon = %q, want empty", got)
187 }
188 }
189
189 lines GO