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