返回 DeepSeek-Reasonix
update_file_claim_test.go
根目录 / internal / repair / update_file_claim_test.go
1 package repair
2
3 import (
4 "os"
5 "path/filepath"
6 "strings"
7 "testing"
8 "time"
9 )
10
11 func prepareTestFileUpdateClaim(t *testing.T) (*UpdateTransaction, string, []string) {
12 t.Helper()
13 t.Setenv("REASONIX_HOME", t.TempDir())
14 dir, err := filepath.EvalSymlinks(t.TempDir())
15 if err != nil {
16 t.Fatal(err)
17 }
18 paths := []string{
19 filepath.Join(dir, "reasonix-desktop"),
20 filepath.Join(dir, "reasonix-guard"),
21 filepath.Join(dir, "reasonix"),
22 }
23 for _, path := range paths {
24 if err := os.WriteFile(path, []byte(filepath.Base(path)), 0o700); err != nil {
25 t.Fatal(err)
26 }
27 }
28 tx, err := PrepareFileUpdate("v1", "v2", paths[0], paths[1:]...)
29 if err != nil {
30 t.Fatal(err)
31 }
32 return tx, paths[0], paths
33 }
34
35 func TestClaimPendingFileUpdateRejectsReleaseUnitMismatch(t *testing.T) {
36 tx, launcher, paths := prepareTestFileUpdateClaim(t)
37 _, release, err := ClaimPendingFileUpdate(
38 tx.ToVersion,
39 tx.CreatedAt,
40 launcher,
41 paths[:len(paths)-1],
42 time.Second,
43 )
44 if release != nil {
45 release()
46 }
47 if err == nil || !strings.Contains(err.Error(), "release unit does not match") {
48 t.Fatalf("claim error = %v, want release-unit mismatch", err)
49 }
50 }
51
52 func TestClaimPendingFileUpdateExactRejectsRewrittenTransaction(t *testing.T) {
53 tx, launcher, paths := prepareTestFileUpdateClaim(t)
54 changed := *tx
55 changed.FromVersion = "rewritten"
56 if err := overwritePendingUpdateForTest(&changed); err != nil {
57 t.Fatal(err)
58 }
59
60 _, release, err := ClaimPendingFileUpdateExact(
61 tx.ToVersion,
62 tx.CreatedAt,
63 UpdateTransactionID(tx),
64 launcher,
65 paths,
66 time.Second,
67 )
68 if release != nil {
69 release()
70 }
71 if err == nil || !strings.Contains(err.Error(), "pending transaction changed") {
72 t.Fatalf("claim error = %v, want full transaction rejection", err)
73 }
74 current, readErr := readPendingUpdateForLauncher(launcher)
75 if readErr != nil || current.FromVersion != changed.FromVersion {
76 t.Fatalf("rewritten transaction = %+v, %v", current, readErr)
77 }
78 }
79
80 func TestClaimPendingFileUpdateRejectsLiveReleaseUnitDrift(t *testing.T) {
81 tx, launcher, paths := prepareTestFileUpdateClaim(t)
82 if err := os.WriteFile(paths[1], []byte("changed-after-prepare"), 0o700); err != nil {
83 t.Fatal(err)
84 }
85 _, release, err := ClaimPendingFileUpdate(
86 tx.ToVersion,
87 tx.CreatedAt,
88 launcher,
89 paths,
90 time.Second,
91 )
92 if release != nil {
93 release()
94 }
95 if err == nil || !strings.Contains(err.Error(), "changed after backup") {
96 t.Fatalf("claim error = %v, want live release-unit drift rejection", err)
97 }
98 if _, statErr := os.Stat(PendingUpdatePath()); statErr != nil {
99 t.Fatalf("rejected claim removed pending transaction: %v", statErr)
100 }
101 }
102
103 func TestClaimPendingFileUpdateRejectsRollbackBackupDrift(t *testing.T) {
104 tx, launcher, paths := prepareTestFileUpdateClaim(t)
105 if err := os.WriteFile(tx.Files[1].BackupPath, []byte("changed-backup"), 0o700); err != nil {
106 t.Fatal(err)
107 }
108 _, release, err := ClaimPendingFileUpdate(
109 tx.ToVersion,
110 tx.CreatedAt,
111 launcher,
112 paths,
113 time.Second,
114 )
115 if release != nil {
116 release()
117 }
118 if err == nil || !strings.Contains(err.Error(), "prepared backup") {
119 t.Fatalf("claim error = %v, want rollback-backup drift rejection", err)
120 }
121 }
122
123 func TestClaimPendingFileUpdateRejectsPreparedMissingFileAppearing(t *testing.T) {
124 t.Setenv("REASONIX_HOME", t.TempDir())
125 dir, err := filepath.EvalSymlinks(t.TempDir())
126 if err != nil {
127 t.Fatal(err)
128 }
129 target := filepath.Join(dir, "reasonix-desktop")
130 guard := filepath.Join(dir, "reasonix-guard")
131 added := filepath.Join(dir, "reasonix-update-helper.exe")
132 for _, path := range []string{target, guard} {
133 if err := os.WriteFile(path, []byte(filepath.Base(path)), 0o700); err != nil {
134 t.Fatal(err)
135 }
136 }
137 tx, err := PrepareFileUpdate("v1", "v2", target, guard, added)
138 if err != nil {
139 t.Fatal(err)
140 }
141 if err := os.WriteFile(added, []byte("appeared"), 0o700); err != nil {
142 t.Fatal(err)
143 }
144 _, release, err := ClaimPendingFileUpdate(
145 tx.ToVersion,
146 tx.CreatedAt,
147 guard,
148 []string{target, guard, added},
149 time.Second,
150 )
151 if release != nil {
152 release()
153 }
154 if err == nil || !strings.Contains(err.Error(), "appeared after backup") {
155 t.Fatalf("claim error = %v, want prepared-missing drift rejection", err)
156 }
157 }
158
159 func TestReadPendingFileUpdateRejectsDuplicateReleaseTarget(t *testing.T) {
160 tx, launcher, _ := prepareTestFileUpdateClaim(t)
161 tx.Files = append(tx.Files, tx.Files[0])
162 if err := overwritePendingUpdateForTest(tx); err != nil {
163 t.Fatal(err)
164 }
165 if _, err := readPendingUpdateForLauncher(launcher); err == nil || !strings.Contains(err.Error(), "duplicate release file") {
166 t.Fatalf("read error = %v, want duplicate release target rejection", err)
167 }
168 }
169
170 func TestClaimPendingFileUpdateRejectsReplacementWhileLocking(t *testing.T) {
171 tx, launcher, paths := prepareTestFileUpdateClaim(t)
172 holder, err := LockRepairMutations(paths...)
173 if err != nil {
174 t.Fatal(err)
175 }
176 entered := make(chan struct{}, 1)
177 originalBeforeLock := repairMutationBeforeLock
178 repairMutationBeforeLock = func([]string) {
179 entered <- struct{}{}
180 }
181 t.Cleanup(func() { repairMutationBeforeLock = originalBeforeLock })
182
183 done := make(chan error, 1)
184 go func() {
185 _, release, err := ClaimPendingFileUpdate(
186 tx.ToVersion,
187 tx.CreatedAt,
188 launcher,
189 paths,
190 2*time.Second,
191 )
192 if release != nil {
193 release()
194 }
195 done <- err
196 }()
197
198 select {
199 case <-entered:
200 case err := <-done:
201 holder()
202 t.Fatalf("claim failed before taking release-unit locks: %v", err)
203 }
204 replacement := *tx
205 replacement.CreatedAt = time.Now().UTC().Add(time.Second).Format(time.RFC3339Nano)
206 if err := overwritePendingUpdateForTest(&replacement); err != nil {
207 holder()
208 t.Fatal(err)
209 }
210 holder()
211
212 err = <-done
213 if err == nil || !strings.Contains(err.Error(), "changed while waiting") {
214 t.Fatalf("claim error = %v, want replacement rejection", err)
215 }
216 }
217
218 func TestClaimPendingFileUpdateHoldsCompleteReleaseUnit(t *testing.T) {
219 tx, launcher, paths := prepareTestFileUpdateClaim(t)
220 _, release, err := ClaimPendingFileUpdate(
221 tx.ToVersion,
222 tx.CreatedAt,
223 launcher,
224 paths,
225 time.Second,
226 )
227 if err != nil {
228 t.Fatal(err)
229 }
230
231 waiter := make(chan error, 1)
232 go func() {
233 unlock, err := LockRepairMutationsTimeout(time.Second, paths...)
234 if err == nil {
235 unlock()
236 }
237 waiter <- err
238 }()
239 select {
240 case err := <-waiter:
241 release()
242 t.Fatalf("release-unit lock escaped the claim: %v", err)
243 case <-time.After(300 * time.Millisecond):
244 }
245 release()
246 if err := <-waiter; err != nil {
247 t.Fatalf("release-unit lock after claim: %v", err)
248 }
249 }
250
250 lines GO