返回 DeepSeek-Reasonix
instance_endpoint_windows_test.go
根目录 / desktop / cmd / update-helper / instance_endpoint_windows_test.go
1 //go:build windows
2
3 package main
4
5 import (
6 "fmt"
7 "os"
8 "os/exec"
9 "path/filepath"
10 "runtime"
11 "syscall"
12 "testing"
13 "time"
14 "unsafe"
15
16 "golang.org/x/sys/windows"
17
18 "reasonix/desktop/internal/instanceidentity"
19 )
20
21 // This worker uses the real message-only window and mutex contract from Wails.
22 func TestInstanceEndpointWorker(t *testing.T) {
23 id := os.Getenv("REASONIX_TEST_ENDPOINT")
24 if id == "" {
25 t.Skip("subprocess fixture")
26 }
27 runtime.LockOSThread()
28 defer runtime.UnlockOSThread()
29 name := "wails-app-" + id
30 mutexName, _ := windows.UTF16PtrFromString(name + "sim")
31 mutex, err := windows.CreateMutex(nil, false, mutexName)
32 if err != nil {
33 t.Fatal(err)
34 }
35 defer windows.CloseHandle(mutex)
36 class, _ := windows.UTF16PtrFromString(name + "-sic")
37 title, _ := windows.UTF16PtrFromString(name + "-siw")
38 def := handoffUser32.NewProc("DefWindowProcW")
39 callback := syscall.NewCallback(func(hwnd uintptr, msg uint32, w, l uintptr) uintptr {
40 r, _, _ := def.Call(hwnd, uintptr(msg), w, l)
41 return r
42 })
43 wc := struct {
44 Size, Style uint32
45 Proc uintptr
46 ClassExtra, WindowExtra int32
47 Instance, Icon, Cursor, Background uintptr
48 Menu, Class *uint16
49 SmallIcon uintptr
50 }{Proc: callback, Class: class}
51 wc.Size = uint32(unsafe.Sizeof(wc))
52 if r, _, err := handoffUser32.NewProc("RegisterClassExW").Call(uintptr(unsafe.Pointer(&wc))); r == 0 {
53 t.Fatal(err)
54 }
55 hwnd, _, err := handoffUser32.NewProc("CreateWindowExW").Call(0, uintptr(unsafe.Pointer(class)), uintptr(unsafe.Pointer(title)), 0, 0, 0, 0, 0, ^uintptr(2), 0, 0, 0)
56 if hwnd == 0 {
57 t.Fatal(err)
58 }
59 defer handoffUser32.NewProc("DestroyWindow").Call(hwnd)
60 marker := os.Getenv("REASONIX_TEST_MARKER")
61 if err := os.WriteFile(marker, []byte("ready"), 0600); err != nil {
62 t.Fatal(err)
63 }
64 deadline := time.Now().Add(30 * time.Second)
65 for time.Now().Before(deadline) {
66 if _, err := os.Stat(marker + ".stop"); err == nil {
67 return
68 }
69 time.Sleep(20 * time.Millisecond)
70 }
71 t.Fatal("fixture timed out")
72 }
73
74 func TestNativeEndpointIsolationAndHandoff(t *testing.T) {
75 root, homeA, homeB := t.TempDir(), t.TempDir(), t.TempDir()
76 source, err := os.Executable()
77 if err != nil {
78 t.Fatal(err)
79 }
80 payload, err := os.ReadFile(source)
81 if err != nil {
82 t.Fatal(err)
83 }
84 target := filepath.Join(root, "reasonix-desktop.exe")
85 old := filepath.Join(root, "versions", "old", "reasonix-desktop.exe")
86 if err := os.MkdirAll(filepath.Dir(old), 0700); err != nil {
87 t.Fatal(err)
88 }
89 for _, p := range []string{target, old} {
90 if err := os.WriteFile(p, payload, 0700); err != nil {
91 t.Fatal(err)
92 }
93 }
94 t.Setenv("REASONIX_HOME", homeA)
95 t.Setenv(instanceidentity.UpdateEnvironmentKey, instanceidentity.ForHome(homeA))
96 start := func(path, home string) func() {
97 marker := filepath.Join(t.TempDir(), "ready")
98 cmd := exec.Command(path, "-test.run=^TestInstanceEndpointWorker$")
99 cmd.Env = append(os.Environ(), "REASONIX_TEST_ENDPOINT="+instanceidentity.ForHome(home), "REASONIX_TEST_MARKER="+marker)
100 if err := cmd.Start(); err != nil {
101 t.Fatal(err)
102 }
103 stopped := false
104 stop := func() {
105 if stopped {
106 return
107 }
108 stopped = true
109 _ = os.WriteFile(marker+".stop", nil, 0600)
110 if err := cmd.Wait(); err != nil {
111 t.Errorf("worker: %v", err)
112 }
113 }
114 t.Cleanup(stop)
115 deadline := time.Now().Add(10 * time.Second)
116 for {
117 if _, err := os.Stat(marker); err == nil {
118 break
119 }
120 if time.Now().After(deadline) {
121 t.Fatal("worker did not register endpoint")
122 }
123 time.Sleep(20 * time.Millisecond)
124 }
125 return stop
126 }
127 start(old, homeB)
128 if err := verifyDesktopHandoff(root, false); err != nil {
129 t.Fatalf("independent old home blocked upgrade: %v", err)
130 }
131 stopA := start(old, homeA)
132 if err := verifyDesktopHandoff(root, false); err == nil {
133 t.Fatal("same-home old owner was accepted")
134 }
135 if image, err := desktopEndpointImage(instanceidentity.ForHome(homeB)); err != nil || image == "" {
136 t.Fatalf("independent owner lost: %s %v", image, err)
137 }
138 stopA()
139 start(target, homeA)
140 if err := verifyDesktopHandoff(root, true); err != nil {
141 t.Fatal(err)
142 }
143 if image, err := desktopEndpointImage(instanceidentity.ForHome(homeB)); err != nil || image == "" {
144 t.Fatal(fmt.Errorf("independent owner did not survive: %w", err))
145 }
146 }
147
147 lines GO