返回 DeepSeek-Reasonix
mutation_lock_identity_test.go
根目录 / internal / repair / mutation_lock_identity_test.go
1 package repair
2
3 import (
4 "context"
5 "crypto/sha256"
6 "errors"
7 "fmt"
8 "os"
9 "path/filepath"
10 "strings"
11 "testing"
12 "time"
13
14 "reasonix/internal/config"
15 "reasonix/internal/filelock"
16 )
17
18 func TestRepairMutationLockRejectsLeafRedirectBeforeWrite(t *testing.T) {
19 home := t.TempDir()
20 t.Setenv("REASONIX_HOME", home)
21 root := t.TempDir()
22 first := filepath.Join(root, "first")
23 second := filepath.Join(root, "second")
24 if err := os.Mkdir(first, 0o700); err != nil {
25 t.Fatal(err)
26 }
27 if err := os.Mkdir(second, 0o700); err != nil {
28 t.Fatal(err)
29 }
30 link := filepath.Join(root, "current")
31 if err := os.Symlink(first, link); err != nil {
32 t.Skipf("symlink unavailable: %v", err)
33 }
34 original := repairMutationBeforeLock
35 repairMutationBeforeLock = func([]string) {
36 if err := os.Remove(link); err != nil {
37 t.Fatal(err)
38 }
39 if err := os.Symlink(second, link); err != nil {
40 t.Fatal(err)
41 }
42 }
43 t.Cleanup(func() { repairMutationBeforeLock = original })
44 unlock, err := lockRepairMutations(link)
45 if unlock != nil {
46 unlock()
47 }
48 if err == nil || !strings.Contains(err.Error(), "identity changed") {
49 t.Fatalf("error = %v", err)
50 }
51 }
52
53 func TestRepairMutationLockAllowsPriorHolderRegularFileReplacement(t *testing.T) {
54 t.Setenv("REASONIX_HOME", t.TempDir())
55 target := filepath.Join(t.TempDir(), "state.json")
56 if err := os.WriteFile(target, []byte("before"), 0o600); err != nil {
57 t.Fatal(err)
58 }
59 original := repairMutationBeforeLock
60 repairMutationBeforeLock = func([]string) {
61 replacement := target + ".new"
62 if err := os.WriteFile(replacement, []byte("after"), 0o600); err != nil {
63 t.Fatal(err)
64 }
65 if err := os.Rename(replacement, target); err != nil {
66 t.Fatal(err)
67 }
68 }
69 t.Cleanup(func() { repairMutationBeforeLock = original })
70 unlock, err := lockRepairMutations(target)
71 if err != nil {
72 t.Fatalf("lock after regular-file replacement: %v", err)
73 }
74 unlock()
75 }
76
77 func TestRepairMutationRejectsRedirectEvenWithReusedNativeIdentity(t *testing.T) {
78 root := t.TempDir()
79 link := filepath.Join(root, "current")
80 if err := os.Symlink(filepath.Join(root, "first"), link); err != nil {
81 t.Skipf("symlink unavailable: %v", err)
82 }
83 targets, _, _, err := repairMutationTargets([]string{link})
84 if err != nil {
85 t.Fatal(err)
86 }
87 if err := os.Remove(link); err != nil {
88 t.Fatal(err)
89 }
90 if err := os.Symlink(filepath.Join(root, "second"), link); err != nil {
91 t.Fatal(err)
92 }
93 // Force the identity comparison to pass, as when unlink/recreate reuses
94 // a filesystem inode; the saved destination must still reject the write.
95 targets[0].info, err = os.Lstat(link)
96 if err != nil {
97 t.Fatal(err)
98 }
99 if err := revalidateRepairMutationTargets(targets); err == nil || !strings.Contains(err.Error(), "identity changed") {
100 t.Fatalf("redirect with reused identity accepted: %v", err)
101 }
102 }
103
104 func TestRepairMutationLockContendsWithLegacyLockBothDirections(t *testing.T) {
105 t.Setenv("REASONIX_HOME", t.TempDir())
106 target := filepath.Join(t.TempDir(), "state.json")
107 legacyKey := legacyCanonicalRepairPath(target)
108 digest := sha256.Sum256([]byte(legacyKey))
109 legacyLock := filepath.Join(config.RepairMutationLockDir(), fmt.Sprintf("%x.lock", digest))
110 if err := os.MkdirAll(filepath.Dir(legacyLock), 0o700); err != nil {
111 t.Fatal(err)
112 }
113 holdLegacy, err := filelock.Acquire(context.Background(), legacyLock)
114 if err != nil {
115 t.Fatal(err)
116 }
117 if unlock, err := LockRepairMutationsTimeout(80*time.Millisecond, target); !errors.Is(err, context.DeadlineExceeded) {
118 if unlock != nil {
119 unlock()
120 }
121 holdLegacy()
122 t.Fatalf("new writer bypassed legacy lock: %v", err)
123 }
124 holdLegacy()
125
126 holdNew, err := LockRepairMutations(target)
127 if err != nil {
128 t.Fatal(err)
129 }
130 defer holdNew()
131 if release, err := filelock.TryAcquire(legacyLock); !errors.Is(err, filelock.ErrHeld) {
132 if release != nil {
133 release()
134 }
135 t.Fatalf("legacy writer bypassed new lock set: %v", err)
136 }
137 }
138
138 lines GO