返回 DeepSeek-Reasonix
external_opener_test.go
根目录 / desktop / external_opener_test.go
1 package main
2
3 import (
4 "bytes"
5 "encoding/json"
6 "image/color"
7 "image/png"
8 "os"
9 "path/filepath"
10 "reflect"
11 "strings"
12 "sync"
13 "testing"
14 "time"
15 )
16
17 func testExternalOpener(id, name, kind string) externalOpenerSpec {
18 return externalOpenerSpec{View: ExternalOpenerView{ID: id, Name: name, Kind: kind}, Target: id}
19 }
20
21 func TestResolveExternalOpenerPrefersInstalledSelection(t *testing.T) {
22 specs := []externalOpenerSpec{
23 testExternalOpener("files", "Files", externalOpenerFileManager),
24 testExternalOpener("code", "Code", externalOpenerEditor),
25 }
26 got, ok := resolveExternalOpener(specs, " CODE ")
27 if !ok || got.View.ID != "code" {
28 t.Fatalf("resolveExternalOpener = (%+v, %v), want installed code", got, ok)
29 }
30 }
31
32 func TestResolveExternalOpenerFallsBackAcrossOperatingSystems(t *testing.T) {
33 specs := []externalOpenerSpec{
34 testExternalOpener("files", "Files", externalOpenerFileManager),
35 testExternalOpener("code", "Code", externalOpenerEditor),
36 }
37 got, ok := resolveExternalOpener(specs, "finder")
38 if !ok || got.View.ID != "files" {
39 t.Fatalf("resolveExternalOpener unavailable preference = (%+v, %v), want file manager fallback", got, ok)
40 }
41 }
42
43 func TestExternalOpenerViewsAreStableAndDeduplicated(t *testing.T) {
44 specs := []externalOpenerSpec{
45 testExternalOpener("code", "VS Code", externalOpenerEditor),
46 testExternalOpener("CODE", "Duplicate", externalOpenerEditor),
47 testExternalOpener("", "Invalid", externalOpenerEditor),
48 }
49 want := []ExternalOpenerView{{ID: "code", Name: "VS Code", Kind: externalOpenerEditor}}
50 if got := externalOpenerViews(specs); !reflect.DeepEqual(got, want) {
51 t.Fatalf("externalOpenerViews = %+v, want %+v", got, want)
52 }
53 }
54
55 func TestExternalOpenerCatalogCachesUntilTTLExpires(t *testing.T) {
56 now := time.Unix(100, 0)
57 discoveryCalls := 0
58 cache := newExternalOpenerCatalogCache(15*time.Second, func() []externalOpenerSpec {
59 discoveryCalls++
60 return []externalOpenerSpec{testExternalOpener("code", "Code", externalOpenerEditor)}
61 })
62 cache.now = func() time.Time { return now }
63
64 first := cache.get()
65 first[0].View.Name = "mutated"
66 if got := cache.get(); discoveryCalls != 1 || got[0].View.Name != "Code" {
67 t.Fatalf("fresh cache = (%d calls, %+v), want one isolated discovery result", discoveryCalls, got)
68 }
69
70 now = now.Add(15 * time.Second)
71 if got := cache.get(); discoveryCalls != 2 || got[0].View.Name != "Code" {
72 t.Fatalf("expired cache = (%d calls, %+v), want a refreshed result", discoveryCalls, got)
73 }
74 }
75
76 func TestExternalOpenerCatalogCoalescesConcurrentRefreshes(t *testing.T) {
77 started := make(chan struct{})
78 release := make(chan struct{})
79 var callsMu sync.Mutex
80 discoveryCalls := 0
81 cache := newExternalOpenerCatalogCache(time.Minute, func() []externalOpenerSpec {
82 callsMu.Lock()
83 discoveryCalls++
84 callsMu.Unlock()
85 close(started)
86 <-release
87 return []externalOpenerSpec{testExternalOpener("code", "Code", externalOpenerEditor)}
88 })
89
90 const callers = 8
91 results := make(chan []externalOpenerSpec, callers)
92 for range callers {
93 go func() { results <- cache.get() }()
94 }
95 <-started
96 close(release)
97 for range callers {
98 if got := <-results; len(got) != 1 || got[0].View.ID != "code" {
99 t.Fatalf("coalesced cache result = %+v, want code", got)
100 }
101 }
102 callsMu.Lock()
103 defer callsMu.Unlock()
104 if discoveryCalls != 1 {
105 t.Fatalf("concurrent discovery calls = %d, want 1", discoveryCalls)
106 }
107 }
108
109 func BenchmarkExternalOpenerCatalogCacheHit(b *testing.B) {
110 cache := newExternalOpenerCatalogCache(time.Minute, func() []externalOpenerSpec {
111 return []externalOpenerSpec{testExternalOpener("code", "Code", externalOpenerEditor)}
112 })
113 cache.get()
114 b.ResetTimer()
115 for b.Loop() {
116 cache.get()
117 }
118 }
119
120 func TestPlatformExternalOpenersHaveUniqueSafeIds(t *testing.T) {
121 specs := platformExternalOpenerSpecs()
122 if len(specs) == 0 {
123 t.Fatal("platformExternalOpenerSpecs returned no fallback opener")
124 }
125 views := externalOpenerViews(specs)
126 if len(views) != len(specs) {
127 t.Fatalf("platform opener ids are invalid or duplicated: specs=%+v views=%+v", specs, views)
128 }
129 if _, ok := resolveExternalOpener(specs, "definitely-not-installed"); !ok {
130 t.Fatal("platform opener list has no usable fallback")
131 }
132 }
133
134 func TestSetPreferredExternalOpenerRejectsRendererCommands(t *testing.T) {
135 app := NewApp()
136 for _, id := range []string{"", "../../bin/sh", "vscode; rm -rf /"} {
137 if err := app.SetPreferredExternalOpener(id); err == nil {
138 t.Fatalf("SetPreferredExternalOpener(%q) unexpectedly succeeded", id)
139 }
140 }
141 }
142
143 func TestExternalOpenerIconFileDataURLAcceptsBoundedImages(t *testing.T) {
144 path := filepath.Join(t.TempDir(), "icon.png")
145 if err := os.WriteFile(path, []byte("png-data"), 0o600); err != nil {
146 t.Fatal(err)
147 }
148 got := externalOpenerIconFileDataURL(path)
149 if !strings.HasPrefix(got, "data:image/png;base64,") {
150 t.Fatalf("externalOpenerIconFileDataURL = %q, want PNG data URL", got)
151 }
152 }
153
154 func TestExternalOpenerPNGRestoresAlphaFromBlackAndWhiteComposites(t *testing.T) {
155 black := []byte{
156 0, 0, 0, 255,
157 0, 0, 0, 255,
158 0, 0, 128, 255,
159 }
160 white := []byte{
161 0, 0, 0, 255,
162 255, 255, 255, 255,
163 127, 127, 255, 255,
164 }
165 encoded := externalOpenerPNGFromBGRAComposites(black, white, 3, 1)
166 decoded, err := png.Decode(bytes.NewReader(encoded))
167 if err != nil {
168 t.Fatal(err)
169 }
170 want := []color.NRGBA{
171 {R: 0, G: 0, B: 0, A: 255},
172 {0, 0, 0, 0},
173 {R: 255, G: 0, B: 0, A: 128},
174 }
175 for x, expected := range want {
176 got := color.NRGBAModel.Convert(decoded.At(x, 0)).(color.NRGBA)
177 if got != expected {
178 t.Fatalf("pixel %d = %#v, want %#v", x, got, expected)
179 }
180 }
181 }
182
183 func TestExternalOpenerViewIconIsBackwardCompatible(t *testing.T) {
184 withoutIcon, err := json.Marshal(ExternalOpenerView{ID: "vscode", Name: "VS Code", Kind: externalOpenerEditor})
185 if err != nil {
186 t.Fatal(err)
187 }
188 if strings.Contains(string(withoutIcon), "iconDataUrl") {
189 t.Fatalf("empty optional icon should be omitted: %s", withoutIcon)
190 }
191 withIcon, err := json.Marshal(ExternalOpenerView{ID: "vscode", Name: "VS Code", Kind: externalOpenerEditor, IconDataURL: "data:image/png;base64,AA=="})
192 if err != nil {
193 t.Fatal(err)
194 }
195 if !strings.Contains(string(withIcon), `"iconDataUrl":"data:image/png;base64,AA=="`) {
196 t.Fatalf("native icon missing from JSON contract: %s", withIcon)
197 }
198 }
199
199 lines GO