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