返回 DeepSeek-Reasonix
activate_test.go
根目录 / internal / installlayout / activate_test.go
1 package installlayout
2
3 import (
4 "errors"
5 "os"
6 "path/filepath"
7 "runtime"
8 "testing"
9 "time"
10 )
11
12 func TestActivateVersionRollsBackWhenProcessAppearsBeforeCommit(t *testing.T) {
13 root, src := t.TempDir(), t.TempDir()
14 members := []Member{}
15 for _, name := range AllowedVersionMembers() {
16 members = append(members, Member{Name: name, Path: writeTempMember(t, src, name, "payload")})
17 }
18 seed := ActivationRequest{InstallRoot: root, Version: "v1.38.5", RequestID: "seed", Members: members}
19 if err := ActivateVersion(seed); err != nil {
20 t.Fatal(err)
21 }
22 before, err := os.ReadFile(filepath.Join(root, "current.json"))
23 if err != nil {
24 t.Fatal(err)
25 }
26 checks := 0
27 seed.Version = "v1.38.7"
28 seed.RequestID = "race"
29 seed.CheckProcesses = func() error {
30 checks++
31 if checks == 2 {
32 return errors.New("old launcher started a process")
33 }
34 return nil
35 }
36 if err := ActivateVersion(seed); err == nil {
37 t.Fatal("committed despite a new process")
38 }
39 after, err := os.ReadFile(filepath.Join(root, "current.json"))
40 if err != nil {
41 t.Fatal(err)
42 }
43 if string(before) != string(after) {
44 t.Fatal("pointer changed")
45 }
46 if _, err := os.Stat(filepath.Join(root, "versions", "v1.38.7")); !os.IsNotExist(err) {
47 t.Fatal("uncommitted version left behind", err)
48 }
49 }
50
51 func writeTempMember(t *testing.T, dir, name, body string) string {
52 t.Helper()
53 path := filepath.Join(dir, name)
54 if err := os.WriteFile(path, []byte(body), 0o755); err != nil {
55 t.Fatal(err)
56 }
57 return path
58 }
59
60 func TestActivateVersionAtomicPointerSwap(t *testing.T) {
61 root := t.TempDir()
62 src := t.TempDir()
63 version := "v1.20.0"
64 members := make([]Member, 0, 3)
65 for _, name := range AllowedVersionMembers() {
66 members = append(members, Member{
67 Name: name,
68 Path: writeTempMember(t, src, name, "payload-"+name),
69 })
70 }
71 if err := ActivateVersion(ActivationRequest{
72 InstallRoot: root,
73 Version: version,
74 RequestID: "req-activate-1",
75 Members: members,
76 }); err != nil {
77 t.Fatal(err)
78 }
79 ptr, err := ReadCurrent(root)
80 if err != nil {
81 t.Fatal(err)
82 }
83 if ptr.ActiveVersion != version {
84 t.Fatalf("active=%s", ptr.ActiveVersion)
85 }
86 desktop, err := ActiveDesktopPath(root)
87 if err != nil {
88 t.Fatal(err)
89 }
90 raw, err := os.ReadFile(desktop)
91 if err != nil {
92 t.Fatal(err)
93 }
94 if string(raw) != "payload-"+DesktopBinaryName() {
95 t.Fatalf("desktop payload = %q", raw)
96 }
97 }
98
99 func TestActivateVersionKeepsOldPointerOnMissingMember(t *testing.T) {
100 root := t.TempDir()
101 // Seed an existing active version.
102 oldSrc := t.TempDir()
103 oldMembers := make([]Member, 0, 3)
104 for _, name := range AllowedVersionMembers() {
105 oldMembers = append(oldMembers, Member{Name: name, Path: writeTempMember(t, oldSrc, name, "old")})
106 }
107 if err := ActivateVersion(ActivationRequest{
108 InstallRoot: root,
109 Version: "v1.19.1",
110 RequestID: "seed",
111 Members: oldMembers,
112 }); err != nil {
113 t.Fatal(err)
114 }
115
116 src := t.TempDir()
117 // Omit update helper — activation must fail and leave v1.19.1 active.
118 bad := []Member{
119 {Name: DesktopBinaryName(), Path: writeTempMember(t, src, DesktopBinaryName(), "new")},
120 {Name: CLIBinaryName(), Path: writeTempMember(t, src, CLIBinaryName(), "new")},
121 }
122 if err := ActivateVersion(ActivationRequest{
123 InstallRoot: root,
124 Version: "v1.20.0",
125 RequestID: "bad",
126 Members: bad,
127 }); err == nil {
128 t.Fatal("expected missing member failure")
129 }
130 ptr, err := ReadCurrent(root)
131 if err != nil {
132 t.Fatal(err)
133 }
134 if ptr.ActiveVersion != "v1.19.1" {
135 t.Fatalf("active changed to %s", ptr.ActiveVersion)
136 }
137 }
138
139 func TestActivateVersionRollsBackVersionAndRootEntriesBeforePointerCommit(t *testing.T) {
140 root := t.TempDir()
141 src := t.TempDir()
142 seedMembers := make([]Member, 0, len(AllowedVersionMembers()))
143 for _, name := range AllowedVersionMembers() {
144 seedMembers = append(seedMembers, Member{Name: name, Path: writeTempMember(t, src, "old-"+name, "old-"+name)})
145 }
146 if err := ActivateVersion(ActivationRequest{
147 InstallRoot: root,
148 Version: "v1.19.1",
149 RequestID: "seed-root-rollback",
150 Members: seedMembers,
151 }); err != nil {
152 t.Fatal(err)
153 }
154 oldLauncher := filepath.Join(root, LauncherBinaryName())
155 if err := os.WriteFile(oldLauncher, []byte("old-launcher"), 0o755); err != nil {
156 t.Fatal(err)
157 }
158 blockedAlias := filepath.Join(root, "blocked-alias")
159 if err := os.Mkdir(blockedAlias, 0o755); err != nil {
160 t.Fatal(err)
161 }
162
163 newMembers := make([]Member, 0, len(AllowedVersionMembers()))
164 for _, name := range AllowedVersionMembers() {
165 newMembers = append(newMembers, Member{Name: name, Path: writeTempMember(t, src, "new-"+name, "new-"+name)})
166 }
167 newLauncher := writeTempMember(t, src, "new-launcher", "new-launcher")
168 err := ActivateVersion(ActivationRequest{
169 InstallRoot: root,
170 Version: "v1.20.0",
171 RequestID: "root-rollback",
172 Members: newMembers,
173 RootMembers: []Member{
174 {Name: LauncherBinaryName(), Path: newLauncher},
175 {Name: "blocked-alias", Path: newLauncher},
176 },
177 RequiredRootNames: []string{LauncherBinaryName(), "blocked-alias"},
178 })
179 if err == nil {
180 t.Fatal("expected root entry publication failure")
181 }
182 ptr, readErr := ReadCurrent(root)
183 if readErr != nil || ptr.ActiveVersion != "v1.19.1" {
184 t.Fatalf("pointer=%+v err=%v", ptr, readErr)
185 }
186 body, readErr := os.ReadFile(oldLauncher)
187 if readErr != nil || string(body) != "old-launcher" {
188 t.Fatalf("launcher=%q err=%v", body, readErr)
189 }
190 if _, statErr := os.Stat(filepath.Join(root, VersionsDirName, "v1.20.0")); !os.IsNotExist(statErr) {
191 t.Fatalf("uncommitted version survived: %v", statErr)
192 }
193 }
194
195 func TestActivateVersionRejectsExtraAndPathTraversalNames(t *testing.T) {
196 root := t.TempDir()
197 src := t.TempDir()
198 members := []Member{
199 {Name: DesktopBinaryName(), Path: writeTempMember(t, src, DesktopBinaryName(), "x")},
200 {Name: CLIBinaryName(), Path: writeTempMember(t, src, CLIBinaryName(), "x")},
201 {Name: UpdateHelperBinaryName(), Path: writeTempMember(t, src, UpdateHelperBinaryName(), "x")},
202 {Name: "evil.exe", Path: writeTempMember(t, src, "evil.exe", "x")},
203 }
204 if err := ActivateVersion(ActivationRequest{
205 InstallRoot: root,
206 Version: "v1.20.0",
207 RequestID: "extra",
208 Members: members,
209 }); err == nil {
210 t.Fatal("expected extra member rejection")
211 }
212
213 members = []Member{
214 {Name: "../" + DesktopBinaryName(), Path: writeTempMember(t, src, "d", "x")},
215 {Name: CLIBinaryName(), Path: writeTempMember(t, src, "c", "x")},
216 {Name: UpdateHelperBinaryName(), Path: writeTempMember(t, src, "u", "x")},
217 }
218 if err := ActivateVersion(ActivationRequest{
219 InstallRoot: root,
220 Version: "v1.20.0",
221 RequestID: "trav",
222 Members: members,
223 }); err == nil {
224 t.Fatal("expected traversal name rejection")
225 }
226 }
227
228 func TestActivateVersionRejectsSymlinkSource(t *testing.T) {
229 if runtime.GOOS == "windows" {
230 t.Skip("symlink privilege varies on Windows CI")
231 }
232 root := t.TempDir()
233 src := t.TempDir()
234 real := writeTempMember(t, src, "real", "body")
235 link := filepath.Join(src, DesktopBinaryName())
236 if err := os.Symlink(real, link); err != nil {
237 t.Fatal(err)
238 }
239 members := []Member{
240 {Name: DesktopBinaryName(), Path: link},
241 {Name: CLIBinaryName(), Path: writeTempMember(t, src, CLIBinaryName(), "x")},
242 {Name: UpdateHelperBinaryName(), Path: writeTempMember(t, src, UpdateHelperBinaryName(), "x")},
243 }
244 if err := ActivateVersion(ActivationRequest{
245 InstallRoot: root,
246 Version: "v1.20.0",
247 RequestID: "symlink",
248 Members: members,
249 }); err == nil {
250 t.Fatal("expected symlink source rejection")
251 }
252 if HasCurrent(root) {
253 t.Fatal("current.json must not be written after failed activation")
254 }
255 }
256
257 func TestCleanupStaleStaging(t *testing.T) {
258 root := t.TempDir()
259 versions := filepath.Join(root, VersionsDirName)
260 if err := os.MkdirAll(versions, 0o755); err != nil {
261 t.Fatal(err)
262 }
263 old := filepath.Join(versions, ".staging-v1.20.0-old")
264 fresh := filepath.Join(versions, ".staging-v1.20.0-fresh")
265 oldReplaced := filepath.Join(versions, "v1.19.0.replaced-old")
266 freshReplaced := filepath.Join(versions, "v1.19.0.replaced-fresh")
267 for _, dir := range []string{old, fresh, oldReplaced, freshReplaced} {
268 if err := os.Mkdir(dir, 0o755); err != nil {
269 t.Fatal(err)
270 }
271 }
272 oldTime := time.Now().Add(-48 * time.Hour)
273 for _, dir := range []string{old, oldReplaced} {
274 if err := os.Chtimes(dir, oldTime, oldTime); err != nil {
275 t.Fatal(err)
276 }
277 }
278 if err := CleanupStaleStaging(root, 24*time.Hour); err != nil {
279 t.Fatal(err)
280 }
281 for _, dir := range []string{old, oldReplaced} {
282 if _, err := os.Stat(dir); !os.IsNotExist(err) {
283 t.Fatalf("%s should be removed", filepath.Base(dir))
284 }
285 }
286 for _, dir := range []string{fresh, freshReplaced} {
287 if _, err := os.Stat(dir); err != nil {
288 t.Fatalf("%s should remain", filepath.Base(dir))
289 }
290 }
291 }
292
293 func TestRetainPreviousVersionsKeepsOneRecent(t *testing.T) {
294 root := t.TempDir()
295 src := t.TempDir()
296 makeVersion := func(v, body string) {
297 members := make([]Member, 0, 3)
298 for _, name := range AllowedVersionMembers() {
299 members = append(members, Member{Name: name, Path: writeTempMember(t, src, v+"-"+name, body)})
300 }
301 if err := ActivateVersion(ActivationRequest{
302 InstallRoot: root,
303 Version: v,
304 RequestID: v,
305 Members: members,
306 }); err != nil {
307 t.Fatal(err)
308 }
309 }
310 makeVersion("v1.18.0", "a")
311 // Age v1.18.0 so retention deletes it.
312 oldDir := filepath.Join(root, "versions", "v1.18.0")
313 oldTime := time.Now().Add(-30 * 24 * time.Hour)
314 _ = os.Chtimes(oldDir, oldTime, oldTime)
315 makeVersion("v1.19.0", "b")
316 makeVersion("v1.20.0", "c")
317
318 if err := RetainPreviousVersions(root, 7*24*time.Hour); err != nil {
319 t.Fatal(err)
320 }
321 if _, err := os.Stat(filepath.Join(root, "versions", "v1.20.0")); err != nil {
322 t.Fatal("active version must remain")
323 }
324 if _, err := os.Stat(filepath.Join(root, "versions", "v1.19.0")); err != nil {
325 t.Fatal("one previous version within window should remain")
326 }
327 if _, err := os.Stat(filepath.Join(root, "versions", "v1.18.0")); !os.IsNotExist(err) {
328 t.Fatal("aged previous version should be GC'd")
329 }
330 ptr, err := ReadCurrent(root)
331 if err != nil || ptr.ActiveVersion != "v1.20.0" {
332 t.Fatalf("pointer = %+v err=%v", ptr, err)
333 }
334 }
335
335 lines GO