返回 DeepSeek-Reasonix
updater_mac_payload_test.go
根目录 / desktop / updater_mac_payload_test.go
1 //go:build darwin
2
3 package main
4
5 import (
6 "errors"
7 "os"
8 "os/exec"
9 "path/filepath"
10 "strings"
11 "syscall"
12 "testing"
13
14 "reasonix/internal/repair"
15 )
16
17 func TestMacUpdateHandoffPublishesPayloadDigestAcrossModeChange(t *testing.T) {
18 probeA := filepath.Join(t.TempDir(), "marker")
19 probeB := filepath.Join(t.TempDir(), "marker")
20 for _, path := range []string{probeA, probeB} {
21 if err := os.WriteFile(path, []byte("probe"), 0o600); err != nil {
22 t.Fatal(err)
23 }
24 }
25 if err := os.Chmod(probeB, 0o700); err != nil {
26 t.Fatal(err)
27 }
28 requireDistinctPOSIXMode(t, probeA, probeB)
29
30 root := t.TempDir()
31 oldApp := filepath.Join(root, "Reasonix.app")
32 newApp := filepath.Join(root, "staging", "Reasonix.app")
33 backupApp := oldApp + ".reasonix-update-backup"
34 pending := filepath.Join(root, "pending.json")
35 for _, dir := range []string{oldApp, newApp} {
36 if err := os.MkdirAll(dir, 0o700); err != nil {
37 t.Fatal(err)
38 }
39 }
40 if err := os.WriteFile(filepath.Join(oldApp, "marker"), []byte("old"), 0o600); err != nil {
41 t.Fatal(err)
42 }
43 if err := os.WriteFile(filepath.Join(newApp, "marker"), []byte("new"), 0o600); err != nil {
44 t.Fatal(err)
45 }
46 if err := os.WriteFile(pending, []byte("pending"), 0o600); err != nil {
47 t.Fatal(err)
48 }
49 payload, err := repair.AppBundlePayloadTreeDigest(newApp)
50 if err != nil {
51 t.Fatal(err)
52 }
53 strict, err := repair.AppBundleTreeDigest(newApp)
54 if err != nil {
55 t.Fatal(err)
56 }
57 if payload == strict {
58 t.Fatal("payload digest collapsed onto the strict digest")
59 }
60 tx := &repair.UpdateTransaction{
61 ToVersion: "v2",
62 CreatedAt: "2026-07-28T00:00:00Z",
63 TargetKind: "app-bundle",
64 TargetPath: oldApp,
65 BackupPath: backupApp,
66 HandoffAppPath: newApp,
67 HandoffAppTreeID: payload,
68 HandoffStagingPath: filepath.Dir(newApp),
69 HandoffOwnerPID: 99999999,
70 }
71 installMacHandoffTestDeps(t, tx, pending, filepath.Join(root, "update.log"), nil)
72 originalCopy := macHandoffCopy
73 macHandoffCopy = func(oldPath, newPath string) error {
74 if err := originalCopy(oldPath, newPath); err != nil {
75 return err
76 }
77 copied := filepath.Join(newPath, "marker")
78 return os.Chmod(copied, 0o700)
79 }
80 originalOpen := openCommand
81 openCommand = func(args ...string) *exec.Cmd {
82 return exec.Command("/bin/sh", "-c", "exit 0")
83 }
84 t.Cleanup(func() {
85 macHandoffCopy = originalCopy
86 openCommand = originalOpen
87 })
88
89 if code := runMacUpdateHandoff(macHandoffConfigFor(tx)); code != 0 {
90 t.Fatal("handoff rejected a mode-only copy of a payload digest")
91 }
92 if got, err := os.ReadFile(filepath.Join(oldApp, "marker")); err != nil || string(got) != "new" {
93 t.Fatalf("installed marker = %q, %v", got, err)
94 }
95 }
96
97 func requireDistinctPOSIXMode(t *testing.T, a, b string) {
98 t.Helper()
99 modeA, err := os.Lstat(a)
100 if err != nil {
101 t.Fatal(err)
102 }
103 modeB, err := os.Lstat(b)
104 if err != nil {
105 t.Fatal(err)
106 }
107 if modeA.Mode() == modeB.Mode() {
108 t.Skip("filesystem does not preserve POSIX mode bits")
109 }
110 }
111
112 func TestMacUpdateRenameFallsBackWhenExclusiveUnsupported(t *testing.T) {
113 for _, unsupported := range []error{syscall.ENOTSUP, syscall.ENOSYS} {
114 t.Run(unsupported.Error(), func(t *testing.T) {
115 dir := t.TempDir()
116 source := filepath.Join(dir, "source")
117 destination := filepath.Join(dir, "destination")
118 if err := os.WriteFile(source, []byte("payload"), 0o600); err != nil {
119 t.Fatal(err)
120 }
121 if err := macRenameNoReplace(func(string, string) error { return unsupported }, source, destination); err != nil {
122 t.Fatal(err)
123 }
124 got, err := os.ReadFile(destination)
125 if err != nil || string(got) != "payload" {
126 t.Fatalf("destination = %q, %v", got, err)
127 }
128 if _, err := os.Lstat(source); !os.IsNotExist(err) {
129 t.Fatalf("source still exists: %v", err)
130 }
131 })
132 }
133 }
134
135 func TestMacUpdateRenameFallbackDoesNotReplaceExisting(t *testing.T) {
136 dir := t.TempDir()
137 source := filepath.Join(dir, "source")
138 destination := filepath.Join(dir, "destination")
139 if err := os.WriteFile(source, []byte("source"), 0o600); err != nil {
140 t.Fatal(err)
141 }
142 if err := os.WriteFile(destination, []byte("destination"), 0o600); err != nil {
143 t.Fatal(err)
144 }
145 if err := macRenameNoReplace(func(string, string) error { return syscall.ENOTSUP }, source, destination); err == nil {
146 t.Fatal("fallback renamed over an existing destination")
147 } else if !errors.Is(err, os.ErrExist) || !strings.Contains(err.Error(), "best-effort under Reasonix mutation lock") {
148 t.Fatalf("fallback err = %v, want ErrExist", err)
149 }
150 for path, want := range map[string]string{source: "source", destination: "destination"} {
151 got, err := os.ReadFile(path)
152 if err != nil || string(got) != want {
153 t.Fatalf("%s = %q, %v; want %q", filepath.Base(path), got, err, want)
154 }
155 }
156 }
157
158 func TestMacUpdateRenameDoesNotFallbackOnOtherErrors(t *testing.T) {
159 dir := t.TempDir()
160 source := filepath.Join(dir, "source")
161 destination := filepath.Join(dir, "destination")
162 if err := os.WriteFile(source, []byte("payload"), 0o600); err != nil {
163 t.Fatal(err)
164 }
165 err := macRenameNoReplace(func(string, string) error { return syscall.EPERM }, source, destination)
166 if !errors.Is(err, syscall.EPERM) {
167 t.Fatalf("err = %v, want EPERM", err)
168 }
169 got, readErr := os.ReadFile(source)
170 if readErr != nil || string(got) != "payload" {
171 t.Fatalf("source = %q, %v", got, readErr)
172 }
173 if _, statErr := os.Lstat(destination); !os.IsNotExist(statErr) {
174 t.Fatalf("destination created after non-fallback error: %v", statErr)
175 }
176 }
177
177 lines GO