返回 DeepSeek-Reasonix
versioned_windows_test.go
根目录 / desktop / cmd / update-helper / versioned_windows_test.go
1 //go:build windows
2
3 package main
4
5 import (
6 "os"
7 "path/filepath"
8 "testing"
9
10 "reasonix/internal/installlayout"
11 "reasonix/internal/repair"
12 )
13
14 func writeVersionedWindowsStaging(t *testing.T, dir, prefix, version string, names ...string) {
15 t.Helper()
16 if len(names) == 0 {
17 names = []string{"reasonix-desktop.exe", "reasonix-cli.exe", "reasonix-update-helper.exe", "reasonix-launcher.exe"}
18 }
19 for _, name := range names {
20 path := filepath.Join(dir, filepath.FromSlash(name))
21 if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
22 t.Fatal(err)
23 }
24 if err := os.WriteFile(path, []byte(prefix+name), 0o700); err != nil {
25 t.Fatal(err)
26 }
27 }
28 writeWindowsPayloadManifestForTest(t, dir, version)
29 }
30
31 func versionedWindowsTransaction(installDir, version, createdAt string) *repair.UpdateTransaction {
32 return &repair.UpdateTransaction{
33 SchemaVersion: 1,
34 ToVersion: version,
35 TargetKind: "file",
36 TargetPath: filepath.Join(installDir, "reasonix-desktop.exe"),
37 CreatedAt: createdAt,
38 }
39 }
40
41 func TestPreferVersionedWindowsActivation(t *testing.T) {
42 staging := t.TempDir()
43 if preferVersionedWindowsActivation(staging) {
44 t.Fatal("empty staging must not prefer versioned")
45 }
46 for _, name := range []string{
47 "reasonix-desktop.exe",
48 "reasonix-cli.exe",
49 "reasonix-update-helper.exe",
50 "reasonix-launcher.exe",
51 } {
52 if err := os.WriteFile(filepath.Join(staging, name), []byte(name), 0o700); err != nil {
53 t.Fatal(err)
54 }
55 }
56 if !preferVersionedWindowsActivation(staging) {
57 t.Fatal("complete staging must prefer versioned")
58 }
59 }
60
61 func TestActivateVersionedWindowsFromStaging(t *testing.T) {
62 acceptWindowsPayloadManifestForTest(t)
63 installDir := t.TempDir()
64 staging := t.TempDir()
65 writeVersionedWindowsStaging(t, staging, "payload:", "v1.20.0",
66 "reasonix-desktop.exe",
67 "reasonix-cli.exe",
68 "reasonix-update-helper.exe",
69 "reasonix-launcher.exe",
70 "reasonix-guard.exe",
71 )
72 // Seed a flat desktop so cleanup is observable.
73 if err := os.WriteFile(filepath.Join(installDir, "reasonix-desktop.exe"), []byte("old"), 0o700); err != nil {
74 t.Fatal(err)
75 }
76 if err := os.WriteFile(filepath.Join(installDir, "reasonix-guard.exe"), []byte("old-guard"), 0o700); err != nil {
77 t.Fatal(err)
78 }
79
80 claimed := versionedWindowsTransaction(installDir, "v1.20.0", "2026-01-01T00:00:00Z")
81 if err := activateVersionedWindowsFromStaging(claimed, staging); err != nil {
82 t.Fatal(err)
83 }
84 if !installlayout.HasCurrent(installDir) {
85 t.Fatal("current.json missing")
86 }
87 ptr, err := installlayout.ReadCurrent(installDir)
88 if err != nil || ptr.ActiveVersion != "v1.20.0" {
89 t.Fatalf("pointer=%+v err=%v", ptr, err)
90 }
91 desktop, err := installlayout.ActiveDesktopPath(installDir)
92 if err != nil {
93 t.Fatal(err)
94 }
95 raw, err := os.ReadFile(desktop)
96 if err != nil || string(raw) != "payload:reasonix-desktop.exe" {
97 t.Fatalf("desktop payload = %q err=%v", raw, err)
98 }
99 for _, name := range []string{"Reasonix.exe", "reasonix-cli.exe"} {
100 if _, err := os.Stat(filepath.Join(installDir, name)); err != nil {
101 t.Fatalf("root entry %s: %v", name, err)
102 }
103 }
104 if _, err := os.Lstat(filepath.Join(installDir, "reasonix-launcher.exe")); !os.IsNotExist(err) {
105 t.Fatalf("new versioned root created a legacy entry: %v", err)
106 }
107 if _, err := os.Stat(filepath.Join(installDir, "reasonix-desktop.exe")); !os.IsNotExist(err) {
108 t.Fatal("flat desktop should be removed")
109 }
110 if _, err := os.Stat(filepath.Join(installDir, "reasonix-guard.exe")); !os.IsNotExist(err) {
111 t.Fatal("flat guard should be removed")
112 }
113 if prefer := preferRelaunchPath("", installDir); filepath.Base(prefer) != "Reasonix.exe" {
114 t.Fatalf("prefer relaunch = %s", prefer)
115 }
116 }
117
118 func TestActivateVersionedWindowsFromStagingPublishesShellTree(t *testing.T) {
119 acceptWindowsPayloadManifestForTest(t)
120 installDir := t.TempDir()
121 staging := t.TempDir()
122 tree := []string{"app/Reasonix.exe", "app/resources/app.asar", "app/locales/en-US.pak"}
123 writeVersionedWindowsStaging(t, staging, "payload:", "v1.30.0", append([]string{
124 "reasonix-desktop.exe",
125 "reasonix-cli.exe",
126 "reasonix-update-helper.exe",
127 "reasonix-launcher.exe",
128 }, tree...)...)
129 if err := activateVersionedWindowsFromStaging(versionedWindowsTransaction(installDir, "v1.30.0", "2026-01-01T00:00:00Z"), staging); err != nil {
130 t.Fatal(err)
131 }
132 versionDir := filepath.Join(installDir, installlayout.VersionsDirName, "v1.30.0")
133 for _, name := range tree {
134 raw, err := os.ReadFile(filepath.Join(versionDir, filepath.FromSlash(name)))
135 if err != nil || string(raw) != "payload:"+name {
136 t.Fatalf("tree member %s = %q err=%v", name, raw, err)
137 }
138 }
139 if _, err := os.Stat(filepath.Join(installDir, "app")); !os.IsNotExist(err) {
140 t.Fatalf("shell tree leaked into the install root: %v", err)
141 }
142 }
143
144 func TestVersionedWindowsUpgradePreservesExistingLegacyEntry(t *testing.T) {
145 acceptWindowsPayloadManifestForTest(t)
146 root := t.TempDir()
147 if err := os.WriteFile(filepath.Join(root, "reasonix-launcher.exe"), []byte("old"), 0o700); err != nil {
148 t.Fatal(err)
149 }
150 for _, version := range []string{"v1.38.9", "v1.39.0"} {
151 staging := t.TempDir()
152 writeVersionedWindowsStaging(t, staging, version+":", version)
153 if err := activateVersionedWindowsFromStaging(versionedWindowsTransaction(root, version, version), staging); err != nil {
154 t.Fatal(err)
155 }
156 for _, name := range []string{"Reasonix.exe", "reasonix-launcher.exe"} {
157 body, err := os.ReadFile(filepath.Join(root, name))
158 if err != nil || string(body) != version+":reasonix-launcher.exe" {
159 t.Fatalf("%s=%q (%v)", name, body, err)
160 }
161 }
162 }
163 }
164
165 func TestActivateVersionedWindowsUsesVerifiedThinCLIEntry(t *testing.T) {
166 acceptWindowsPayloadManifestForTest(t)
167 installDir := t.TempDir()
168 staging := t.TempDir()
169 const entry = "app/resources/bin/reasonix-cli-launcher.exe"
170 writeVersionedWindowsStaging(t, staging, "payload:", "v1.39.0",
171 "reasonix-desktop.exe", "reasonix-cli.exe", "reasonix-update-helper.exe",
172 "reasonix-launcher.exe", "app/Reasonix.exe", entry)
173 if err := activateVersionedWindowsFromStaging(versionedWindowsTransaction(installDir, "v1.39.0", "2026-01-01T00:00:00Z"), staging); err != nil {
174 t.Fatal(err)
175 }
176 rootCLI, err := os.ReadFile(filepath.Join(installDir, "reasonix-cli.exe"))
177 if err != nil || string(rootCLI) != "payload:"+entry {
178 t.Fatalf("root CLI=%q err=%v, want verified thin entry", rootCLI, err)
179 }
180 fullCLI, err := os.ReadFile(filepath.Join(installDir, "versions", "v1.39.0", "reasonix-cli.exe"))
181 if err != nil || string(fullCLI) != "payload:reasonix-cli.exe" {
182 t.Fatalf("version CLI=%q err=%v, want full CLI", fullCLI, err)
183 }
184 }
185
186 func TestActivateVersionedWindowsFromStagingRejectsManifestDrift(t *testing.T) {
187 acceptWindowsPayloadManifestForTest(t)
188 installDir := t.TempDir()
189 staging := t.TempDir()
190 writeVersionedWindowsStaging(t, staging, "payload:", "v1.30.0",
191 "reasonix-desktop.exe",
192 "reasonix-cli.exe",
193 "reasonix-update-helper.exe",
194 "reasonix-launcher.exe",
195 "app/Reasonix.exe",
196 )
197 if err := os.WriteFile(filepath.Join(staging, "app", "Reasonix.exe"), []byte("tampered"), 0o700); err != nil {
198 t.Fatal(err)
199 }
200 if err := activateVersionedWindowsFromStaging(versionedWindowsTransaction(installDir, "v1.30.0", "2026-01-01T00:00:00Z"), staging); err == nil {
201 t.Fatal("staged tree member that differs from the signed manifest was activated")
202 }
203 if installlayout.HasCurrent(installDir) {
204 t.Fatal("current.json was written after a rejected payload")
205 }
206 if err := os.Remove(filepath.Join(staging, "reasonix-payload.json")); err != nil {
207 t.Fatal(err)
208 }
209 if err := activateVersionedWindowsFromStaging(versionedWindowsTransaction(installDir, "v1.30.0", "2026-01-01T00:00:00Z"), staging); err == nil {
210 t.Fatal("staged payload without a signed manifest was activated")
211 }
212 }
213
214 func TestPreferRelaunchPathIgnoresStaleVersionedDesktop(t *testing.T) {
215 acceptWindowsPayloadManifestForTest(t)
216 installDir := t.TempDir()
217 oldSrc := t.TempDir()
218 newSrc := t.TempDir()
219 writeVersionedWindowsStaging(t, oldSrc, "old-", "v1.24.0")
220 if err := activateVersionedWindowsFromStaging(versionedWindowsTransaction(installDir, "v1.24.0", "2026-01-01T00:00:00Z"), oldSrc); err != nil {
221 t.Fatal(err)
222 }
223 oldDesktop, err := installlayout.ActiveDesktopPath(installDir)
224 if err != nil {
225 t.Fatal(err)
226 }
227 writeVersionedWindowsStaging(t, newSrc, "new-", "v1.24.1")
228 if err := activateVersionedWindowsFromStaging(versionedWindowsTransaction(installDir, "v1.24.1", "2026-01-01T00:00:01Z"), newSrc); err != nil {
229 t.Fatal(err)
230 }
231 if _, err := os.Stat(oldDesktop); err != nil {
232 t.Fatalf("previous desktop should remain: %v", err)
233 }
234 got := preferRelaunchPath(oldDesktop, installDir)
235 if filepath.Base(got) != "Reasonix.exe" {
236 t.Fatalf("preferRelaunchPath(%s) = %s, want install-root launcher", oldDesktop, got)
237 }
238 }
239
240 func TestInstallStagedWindowsReleaseUnitPrefersVersioned(t *testing.T) {
241 acceptWindowsPayloadManifestForTest(t)
242 installDir := t.TempDir()
243 staging := t.TempDir()
244 writeVersionedWindowsStaging(t, staging, "p:", "v1.20.0",
245 "reasonix-desktop.exe",
246 "reasonix-cli.exe",
247 "reasonix-update-helper.exe",
248 "reasonix-launcher.exe",
249 "reasonix-guard.exe",
250 )
251 // Minimal claimed unit (versioned path does not need full flat file list).
252 claimed := versionedWindowsTransaction(installDir, "v1.20.0", "2026-01-01T00:00:00Z")
253 started, receipts, err := installStagedWindowsReleaseUnit(claimed, staging)
254 if err != nil {
255 t.Fatal(err)
256 }
257 if !started || receipts != nil {
258 t.Fatalf("started=%v receipts=%v want started with nil receipts", started, receipts)
259 }
260 if !installlayout.HasCurrent(installDir) {
261 t.Fatal("versioned activation did not write current.json")
262 }
263 }
264
264 lines GO