返回 DeepSeek-Reasonix
plan_tree_test.go
根目录 / internal / repair / plan_tree_test.go
1 package repair
2
3 import (
4 "os"
5 "path/filepath"
6 "runtime"
7 "testing"
8 )
9
10 func requireDistinctPOSIXMode(t *testing.T, a, b string) {
11 t.Helper()
12 modeA, err := os.Lstat(a)
13 if err != nil {
14 t.Fatal(err)
15 }
16 modeB, err := os.Lstat(b)
17 if err != nil {
18 t.Fatal(err)
19 }
20 if modeA.Mode() == modeB.Mode() {
21 t.Skip("filesystem does not preserve POSIX mode bits")
22 }
23 }
24
25 func TestRepairPlanTreeContentStateIDIncludesPOSIXMode(t *testing.T) {
26 a := t.TempDir()
27 b := t.TempDir()
28 for _, root := range []string{a, b} {
29 if err := os.Mkdir(filepath.Join(root, "Contents"), 0o755); err != nil {
30 t.Fatal(err)
31 }
32 if err := os.WriteFile(filepath.Join(root, "Contents", "marker"), []byte("hello"), 0o644); err != nil {
33 t.Fatal(err)
34 }
35 }
36 if err := os.Chmod(filepath.Join(b, "Contents", "marker"), 0o700); err != nil {
37 t.Fatal(err)
38 }
39 requireDistinctPOSIXMode(t, filepath.Join(a, "Contents", "marker"), filepath.Join(b, "Contents", "marker"))
40 idA, err := repairPlanTreeContentStateID(a)
41 if err != nil {
42 t.Fatal(err)
43 }
44 idB, err := repairPlanTreeContentStateID(b)
45 if err != nil {
46 t.Fatal(err)
47 }
48 if idA == idB {
49 t.Fatal("strict digest ignored a POSIX mode change")
50 }
51 }
52
53 func TestRepairPlanTreePayloadStateIDIgnoresPOSIXMode(t *testing.T) {
54 a := t.TempDir()
55 b := t.TempDir()
56 for _, root := range []string{a, b} {
57 if err := os.Mkdir(filepath.Join(root, "Contents"), 0o755); err != nil {
58 t.Fatal(err)
59 }
60 if err := os.WriteFile(filepath.Join(root, "Contents", "marker"), []byte("hello"), 0o644); err != nil {
61 t.Fatal(err)
62 }
63 }
64 if err := os.Chmod(filepath.Join(b, "Contents", "marker"), 0o700); err != nil {
65 t.Fatal(err)
66 }
67 requireDistinctPOSIXMode(t, filepath.Join(a, "Contents", "marker"), filepath.Join(b, "Contents", "marker"))
68 idA, err := repairPlanTreePayloadStateID(a)
69 if err != nil {
70 t.Fatal(err)
71 }
72 idB, err := repairPlanTreePayloadStateID(b)
73 if err != nil {
74 t.Fatal(err)
75 }
76 if idA != idB {
77 t.Fatal("payload digest changed for a POSIX mode-only copy")
78 }
79 }
80
81 func TestRepairPlanTreePayloadStateIDIgnoresAppleDoubleSidecarFile(t *testing.T) {
82 a := t.TempDir()
83 b := t.TempDir()
84 for _, root := range []string{a, b} {
85 if err := os.Mkdir(filepath.Join(root, "Contents"), 0o755); err != nil {
86 t.Fatal(err)
87 }
88 if err := os.WriteFile(filepath.Join(root, "Contents", "Info.plist"), []byte("plist"), 0o644); err != nil {
89 t.Fatal(err)
90 }
91 }
92 if err := os.WriteFile(filepath.Join(b, "Contents", "._Info.plist"), []byte("appledouble"), 0o644); err != nil {
93 t.Fatal(err)
94 }
95 idA, err := repairPlanTreePayloadStateID(a)
96 if err != nil {
97 t.Fatal(err)
98 }
99 idB, err := repairPlanTreePayloadStateID(b)
100 if err != nil {
101 t.Fatal(err)
102 }
103 if idA != idB {
104 t.Fatal("payload digest changed for an AppleDouble sidecar file")
105 }
106 strictA, err := repairPlanTreeContentStateID(a)
107 if err != nil {
108 t.Fatal(err)
109 }
110 strictB, err := repairPlanTreeContentStateID(b)
111 if err != nil {
112 t.Fatal(err)
113 }
114 if strictA == strictB {
115 t.Fatal("strict digest ignored an AppleDouble sidecar file")
116 }
117 }
118
119 func TestRepairPlanTreePayloadStateIDHashesAppleDoubleDirectory(t *testing.T) {
120 a := t.TempDir()
121 b := t.TempDir()
122 for _, root := range []string{a, b} {
123 if err := os.Mkdir(filepath.Join(root, "Contents"), 0o755); err != nil {
124 t.Fatal(err)
125 }
126 }
127 if err := os.Mkdir(filepath.Join(b, "._Contents"), 0o755); err != nil {
128 t.Fatal(err)
129 }
130 idA, err := repairPlanTreePayloadStateID(a)
131 if err != nil {
132 t.Fatal(err)
133 }
134 idB, err := repairPlanTreePayloadStateID(b)
135 if err != nil {
136 t.Fatal(err)
137 }
138 if idA == idB {
139 t.Fatal("payload digest ignored a ._ directory")
140 }
141 }
142
143 func TestRepairPlanTreePayloadStateIDHashesAppleDoubleSymlink(t *testing.T) {
144 if runtime.GOOS == "windows" {
145 t.Skip("creating symlinks requires elevated privileges on Windows CI")
146 }
147 a := t.TempDir()
148 b := t.TempDir()
149 for _, root := range []string{a, b} {
150 if err := os.WriteFile(filepath.Join(root, "marker"), []byte("hello"), 0o644); err != nil {
151 t.Fatal(err)
152 }
153 }
154 if err := os.Symlink("marker", filepath.Join(b, "._marker")); err != nil {
155 t.Fatal(err)
156 }
157 idA, err := repairPlanTreePayloadStateID(a)
158 if err != nil {
159 t.Fatal(err)
160 }
161 idB, err := repairPlanTreePayloadStateID(b)
162 if err != nil {
163 t.Fatal(err)
164 }
165 if idA == idB {
166 t.Fatal("payload digest ignored a ._ symlink")
167 }
168 }
169
170 func TestRepairPlanTreePayloadStateIDDetectsPayloadChange(t *testing.T) {
171 a := t.TempDir()
172 b := t.TempDir()
173 if err := os.WriteFile(filepath.Join(a, "marker"), []byte("hello"), 0o644); err != nil {
174 t.Fatal(err)
175 }
176 if err := os.WriteFile(filepath.Join(b, "marker"), []byte("other"), 0o644); err != nil {
177 t.Fatal(err)
178 }
179 idA, err := repairPlanTreePayloadStateID(a)
180 if err != nil {
181 t.Fatal(err)
182 }
183 idB, err := repairPlanTreePayloadStateID(b)
184 if err != nil {
185 t.Fatal(err)
186 }
187 if idA == idB {
188 t.Fatal("payload digest ignored a content change")
189 }
190 }
191
192 func TestRepairPlanTreePayloadStateIDDetectsExtraFile(t *testing.T) {
193 a := t.TempDir()
194 b := t.TempDir()
195 if err := os.WriteFile(filepath.Join(a, "marker"), []byte("hello"), 0o644); err != nil {
196 t.Fatal(err)
197 }
198 if err := os.WriteFile(filepath.Join(b, "marker"), []byte("hello"), 0o644); err != nil {
199 t.Fatal(err)
200 }
201 if err := os.WriteFile(filepath.Join(b, "extra"), []byte("x"), 0o644); err != nil {
202 t.Fatal(err)
203 }
204 idA, err := repairPlanTreePayloadStateID(a)
205 if err != nil {
206 t.Fatal(err)
207 }
208 idB, err := repairPlanTreePayloadStateID(b)
209 if err != nil {
210 t.Fatal(err)
211 }
212 if idA == idB {
213 t.Fatal("payload digest ignored an extra file")
214 }
215 }
216
217 func TestRepairPlanTreePayloadStateIDHashesOrphanAppleDoubleName(t *testing.T) {
218 a := t.TempDir()
219 b := t.TempDir()
220 if err := os.WriteFile(filepath.Join(a, "marker"), []byte("hello"), 0o644); err != nil {
221 t.Fatal(err)
222 }
223 if err := os.WriteFile(filepath.Join(b, "marker"), []byte("hello"), 0o644); err != nil {
224 t.Fatal(err)
225 }
226 if err := os.WriteFile(filepath.Join(b, "._orphan"), []byte("sidecar"), 0o644); err != nil {
227 t.Fatal(err)
228 }
229 idA, err := repairPlanTreePayloadStateID(a)
230 if err != nil {
231 t.Fatal(err)
232 }
233 idB, err := repairPlanTreePayloadStateID(b)
234 if err != nil {
235 t.Fatal(err)
236 }
237 if idA == idB {
238 t.Fatal("payload digest ignored an orphan ._ file")
239 }
240 }
241
242 func TestRepairPlanTreeHandoffAppMatchesLegacyStrictDigest(t *testing.T) {
243 root := t.TempDir()
244 if err := os.WriteFile(filepath.Join(root, "marker"), []byte("hello"), 0o644); err != nil {
245 t.Fatal(err)
246 }
247 strict, err := repairPlanTreeContentStateID(root)
248 if err != nil {
249 t.Fatal(err)
250 }
251 payload, err := repairPlanTreePayloadStateID(root)
252 if err != nil {
253 t.Fatal(err)
254 }
255 if strict == payload {
256 t.Fatal("payload digest collapsed onto the strict digest")
257 }
258 matched, err := repairPlanTreeHandoffAppMatches(root, strict)
259 if err != nil || !matched {
260 t.Fatalf("legacy strict digest rejected: matched=%v err=%v", matched, err)
261 }
262 matched, err = repairPlanTreeHandoffAppMatches(root, payload)
263 if err != nil || !matched {
264 t.Fatalf("payload digest rejected: matched=%v err=%v", matched, err)
265 }
266 }
267
268 func TestVerifyAppBundleUpdateHandoffReplacementAcceptsModeOnlyCopy(t *testing.T) {
269 src := t.TempDir()
270 dst := t.TempDir()
271 for _, root := range []string{src, dst} {
272 if err := os.WriteFile(filepath.Join(root, "marker"), []byte("hello"), 0o644); err != nil {
273 t.Fatal(err)
274 }
275 }
276 if err := os.Chmod(filepath.Join(dst, "marker"), 0o700); err != nil {
277 t.Fatal(err)
278 }
279 requireDistinctPOSIXMode(t, filepath.Join(src, "marker"), filepath.Join(dst, "marker"))
280 payload, err := repairPlanTreePayloadStateID(src)
281 if err != nil {
282 t.Fatal(err)
283 }
284 tx := &UpdateTransaction{TargetKind: "app-bundle", HandoffAppTreeID: payload}
285 if err := VerifyAppBundleUpdateHandoffReplacement(tx, dst); err != nil {
286 t.Fatalf("mode-only copy rejected: %v", err)
287 }
288 }
289
290 func TestVerifyAppBundleUpdateHandoffReplacementAcceptsLegacyStrictDigest(t *testing.T) {
291 root := t.TempDir()
292 if err := os.WriteFile(filepath.Join(root, "marker"), []byte("hello"), 0o644); err != nil {
293 t.Fatal(err)
294 }
295 strict, err := repairPlanTreeContentStateID(root)
296 if err != nil {
297 t.Fatal(err)
298 }
299 tx := &UpdateTransaction{TargetKind: "app-bundle", HandoffAppTreeID: strict}
300 if err := VerifyAppBundleUpdateHandoffReplacement(tx, root); err != nil {
301 t.Fatalf("legacy strict digest rejected: %v", err)
302 }
303 }
304
305 func TestPrepareAppBundleUpdateHandoffRecordsPayloadAppDigest(t *testing.T) {
306 tx, _ := prepareTestAppBundleHandoff(t)
307 payload, err := repairPlanTreePayloadStateID(tx.HandoffAppPath)
308 if err != nil {
309 t.Fatal(err)
310 }
311 strict, err := repairPlanTreeContentStateID(tx.HandoffAppPath)
312 if err != nil {
313 t.Fatal(err)
314 }
315 if tx.HandoffAppTreeID != payload {
316 t.Fatalf("staged app digest = %s, want payload %s", tx.HandoffAppTreeID, payload)
317 }
318 if tx.HandoffAppTreeID == strict {
319 t.Fatal("staged app digest used the strict tree identity")
320 }
321 backup, err := repairPlanTreeContentStateID(tx.TargetPath)
322 if err != nil {
323 t.Fatal(err)
324 }
325 if tx.BackupTreeID != backup {
326 t.Fatalf("backup digest = %s, want strict %s", tx.BackupTreeID, backup)
327 }
328 staging, err := repairPlanTreeContentStateID(tx.HandoffStagingPath)
329 if err != nil {
330 t.Fatal(err)
331 }
332 if tx.HandoffStagingTreeID != staging {
333 t.Fatalf("staging digest = %s, want strict %s", tx.HandoffStagingTreeID, staging)
334 }
335 }
336
336 lines GO