返回 DeepSeek-Reasonix
native_windows_test.go
根目录 / internal / desktopinstance / native_windows_test.go
1 //go:build windows
2
3 package desktopinstance
4
5 import (
6 "encoding/json"
7 "errors"
8 "fmt"
9 "os"
10 "os/exec"
11 "path/filepath"
12 "strings"
13 "testing"
14 "time"
15
16 "golang.org/x/sys/windows"
17 )
18
19 func TestNativeCanonicalFollowsJunctionAndRejectsMissingImage(t *testing.T) {
20 root := t.TempDir()
21 target, alias := filepath.Join(root, "target"), filepath.Join(root, "alias")
22 if err := os.Mkdir(target, 0o700); err != nil {
23 t.Fatal(err)
24 }
25 if out, err := exec.Command("cmd", "/c", "mklink", "/J", alias, target).CombinedOutput(); err != nil {
26 t.Fatalf("create junction: %v: %s", err, out)
27 }
28 left, err := canonical(alias)
29 right, rightErr := canonical(target)
30 if err != nil || rightErr != nil || left != right {
31 t.Fatalf("instance identity split: %q / %q, %v / %v", left, right, err, rightErr)
32 }
33 if _, err := canonical(filepath.Join(alias, "missing.exe")); !os.IsNotExist(err) {
34 t.Fatalf("missing executable accepted: %v", err)
35 }
36 }
37
38 func TestNativeStatusPipeValidatesIdentityAndProtocol(t *testing.T) {
39 p, err := openProcess(windows.GetCurrentProcessId(), 0)
40 if err != nil {
41 t.Fatal(err)
42 }
43 defer p.close()
44 for _, tc := range []struct {
45 name string
46 schema int
47 pid uint32
48 valid bool
49 }{
50 {"valid", 1, p.pid, true}, {"wrong-pid", 1, p.pid + 1, false}, {"future-schema", 2, p.pid, false},
51 } {
52 t.Run(tc.name, func(t *testing.T) {
53 name, _ := windows.UTF16PtrFromString(fmt.Sprintf(`\\.\pipe\reasonix-shell-v1-%d`, p.pid))
54 pipe, err := windows.CreateNamedPipe(name, windows.PIPE_ACCESS_OUTBOUND, windows.PIPE_TYPE_BYTE, 1, StatusLimit+1, 0, 2000, nil)
55 if err != nil {
56 t.Fatal(err)
57 }
58 done := make(chan error, 1)
59 go func() {
60 defer windows.CloseHandle(pipe)
61 err := windows.ConnectNamedPipe(pipe, nil)
62 if err != nil && !errors.Is(err, windows.ERROR_PIPE_CONNECTED) {
63 done <- err
64 return
65 }
66 data, _ := json.Marshal(Status{SchemaVersion: tc.schema, Product: "com.reasonix.desktop", PID: tc.pid, Version: "v1.38.7", Generation: "test", HomeKey: ProfileKey(t.TempDir()), Lifecycle: "failed", Service: "exited"})
67 var n uint32
68 err = windows.WriteFile(pipe, append(data, '\n'), &n, nil)
69 if err == nil {
70 err = windows.FlushFileBuffers(pipe)
71 }
72 done <- err
73 }()
74 _, err = readStatus(p)
75 if (err == nil) != tc.valid {
76 t.Errorf("valid=%v error=%v", tc.valid, err)
77 }
78 if err := <-done; err != nil {
79 t.Fatal(err)
80 }
81 })
82 }
83 }
84
85 func TestNativeProcessIdentityAndPIDReuseFence(t *testing.T) {
86 if os.Getenv("REASONIX_PROCESS_TEST_CHILD") == "1" {
87 time.Sleep(time.Minute)
88 return
89 }
90 cmd := exec.Command(os.Args[0], "-test.run=^TestNativeProcessIdentityAndPIDReuseFence$")
91 cmd.Env = append(os.Environ(), "REASONIX_PROCESS_TEST_CHILD=1")
92 if err := cmd.Start(); err != nil {
93 t.Fatal(err)
94 }
95 defer cmd.Wait()
96 defer cmd.Process.Kill()
97 p, err := openProcess(uint32(cmd.Process.Pid), uint32(os.Getpid()))
98 if err != nil {
99 t.Fatal(err)
100 }
101 defer p.close()
102 if !p.alive() {
103 t.Fatal("child already dead")
104 }
105 saved := p.created
106 p.created.LowDateTime++
107 if err := p.terminate(); err == nil {
108 t.Fatal("accepted changed creation time")
109 }
110 if !p.alive() {
111 t.Fatal("killed identity that did not match")
112 }
113 p.created = saved
114 if err := p.terminate(); err != nil {
115 t.Fatal(err)
116 }
117 if !waitProcesses([]*process{p}, time.Second*10) {
118 t.Fatal("handle did not signal exit")
119 }
120 }
121
122 func TestNativeCoordinationMutexOwnsCorrectThread(t *testing.T) {
123 root := t.TempDir()
124 unlock, err := lockInstall(root)
125 if err != nil {
126 t.Fatal(err)
127 }
128 waiting := make(chan struct{})
129 acquired := make(chan error, 1)
130 go func() {
131 close(waiting)
132 release, err := lockInstall(root)
133 if err == nil {
134 release()
135 }
136 acquired <- err
137 }()
138 <-waiting
139 select {
140 case err := <-acquired:
141 unlock()
142 t.Fatalf("mutex did not serialize callers: %v", err)
143 case <-time.After(100 * time.Millisecond):
144 }
145 unlock()
146 select {
147 case err := <-acquired:
148 if err != nil {
149 t.Fatal(err)
150 }
151 case <-time.After(5 * time.Second):
152 t.Fatal("mutex was not released on owning thread")
153 }
154 }
155
156 func TestNativeInspectCurrentUser(t *testing.T) {
157 p, err := openProcess(windows.GetCurrentProcessId(), 0)
158 if err != nil {
159 t.Fatal(err)
160 }
161 defer p.close()
162 if p.image == "" || !p.alive() {
163 t.Fatal("missing executable identity")
164 }
165 }
166
167 func TestUnsupportedPortableLocationNotificationIsActionable(t *testing.T) {
168 title, body := notificationContent(fmt.Errorf("launch: %w", NewUnsupportedPortableLocationError("unc")))
169 if title != "Reasonix 无法从当前位置启动" {
170 t.Fatalf("title = %q", title)
171 }
172 for _, want := range []string{
173 "Copy the entire extracted folder to a local Windows drive",
174 "start Reasonix.exe, or use the installer",
175 "请将整个解压目录复制到 Windows 本地磁盘",
176 "或使用安装器",
177 } {
178 if !strings.Contains(body, want) {
179 t.Errorf("notification body is missing %q: %q", want, body)
180 }
181 }
182 if strings.Contains(body, "30 seconds") || strings.Contains(body, "启动或更新") {
183 t.Fatalf("notification fell back to generic startup copy: %q", body)
184 }
185 }
186
187 func TestPortableLocationDiagnosticContainsClassificationButNoPath(t *testing.T) {
188 home := t.TempDir()
189 LogPortableLocationRejected(home, "unc", true)
190 data, err := os.ReadFile(filepath.Join(home, "desktop-shell", "logs", "recovery.log"))
191 if err != nil {
192 t.Fatal(err)
193 }
194 line := string(data)
195 for _, want := range []string{
196 "event=portable_location_rejected",
197 "platform=windows",
198 "location_type=unc",
199 "launch_mode=interactive",
200 } {
201 if !strings.Contains(line, want) {
202 t.Errorf("diagnostic is missing %q: %q", want, line)
203 }
204 }
205 if strings.Contains(line, home) {
206 t.Fatalf("diagnostic leaked the user path: %q", line)
207 }
208 }
209
210 // Optional local diagnostic: never runs against the user's default profile.
211 func TestNativeExplicitFixtureInspection(t *testing.T) {
212 root, home := os.Getenv("REASONIX_TEST_INSPECT_ROOT"), os.Getenv("REASONIX_TEST_INSPECT_HOME")
213 if root == "" || home == "" {
214 t.Skip("explicit isolated installed fixture required")
215 }
216 root, profile, err := preparePaths(root, home)
217 if err != nil {
218 t.Fatal(err)
219 }
220 list, err := inspect(root, profile, false)
221 if err != nil {
222 t.Fatal(err)
223 }
224 defer closeProcesses(list)
225 if len(list) == 0 {
226 t.Fatal("isolated fixture shell was not identified")
227 }
228 for _, p := range list {
229 t.Logf("pid=%d legacyProfile=%q status=%+v", p.pid, p.legacyProfile, p.status)
230 }
231 }
232
232 lines GO