返回 DeepSeek-Reasonix
write_recovery_test.go
根目录 / internal / tool / builtin / write_recovery_test.go
1 package builtin
2
3 import (
4 "context"
5 "encoding/json"
6 "errors"
7 "os"
8 "path/filepath"
9 "testing"
10
11 fileenc "reasonix/internal/fileutil/encoding"
12 "reasonix/internal/tool"
13 )
14
15 func TestWriteIntentDurabilityBeforeMutation(t *testing.T) {
16 path := filepath.Join(t.TempDir(), "a.txt")
17 if err := os.WriteFile(path, []byte("before"), 0600); err != nil {
18 t.Fatal(err)
19 }
20 failure := errors.New("intent store unavailable")
21 ctx := tool.WithWriteIntentHook(context.Background(), func(tool.FileWriteIntent) error { return failure })
22 args, _ := json.Marshal(map[string]string{"path": path, "content": "after"})
23 if _, err := (writeFile{}).Execute(ctx, args); !errors.Is(err, failure) {
24 t.Fatalf("err=%v", err)
25 }
26 b, _ := os.ReadFile(path)
27 if string(b) != "before" {
28 t.Fatal("write preceded durable intent")
29 }
30 }
31 func TestWriteRecoveryVerifiesActualPostcondition(t *testing.T) {
32 path := filepath.Join(t.TempDir(), "a.txt")
33 var intent tool.FileWriteIntent
34 ctx := tool.WithWriteIntentHook(context.Background(), func(i tool.FileWriteIntent) error { intent = i; return nil })
35 args, _ := json.Marshal(map[string]string{"path": path, "content": "after"})
36 writer := writeFile{}
37 if _, err := writer.Execute(ctx, args); err != nil {
38 t.Fatal(err)
39 }
40 if got := writer.VerifyWrite(ctx, intent); got != tool.WriteSatisfied {
41 t.Fatalf("got %s intent=%+v", got, intent)
42 }
43 if err := os.WriteFile(path, []byte("external change"), 0600); err != nil {
44 t.Fatal(err)
45 }
46 if got := writer.VerifyWrite(ctx, intent); got != tool.WriteConflict {
47 t.Fatalf("got %s", got)
48 }
49 intent.Version = 99
50 if got := writer.VerifyWrite(ctx, intent); got != tool.WriteUnknown {
51 t.Fatalf("future version=%s", got)
52 }
53 }
54
55 type recoverableOverlay struct {
56 content string
57 available bool
58 id string
59 }
60
61 func (o *recoverableOverlay) ReadTextFile(context.Context, string) (string, bool) {
62 return o.content, o.available
63 }
64 func (o *recoverableOverlay) WriteTextFile(_ context.Context, _ string, text string) (bool, error) {
65 if !o.available {
66 return false, nil
67 }
68 o.content = text
69 return true, nil
70 }
71 func (o *recoverableOverlay) RecoveryIdentity() string { return o.id }
72 func TestWriteRecoveryUsesOriginalOverlay(t *testing.T) {
73 path := filepath.Join(t.TempDir(), "buffer.txt")
74 if err := os.WriteFile(path, []byte("disk"), 0600); err != nil {
75 t.Fatal(err)
76 }
77 overlay := &recoverableOverlay{content: "unsaved", available: true, id: "transport-one"}
78 writer := writeFile{overlay: overlay}
79 var intent tool.FileWriteIntent
80 ctx := tool.WithWriteIntentHook(context.Background(), func(i tool.FileWriteIntent) error { intent = i; return nil })
81 args, _ := json.Marshal(map[string]string{"path": path, "content": "edited buffer"})
82 if _, err := writer.Execute(ctx, args); err != nil {
83 t.Fatal(err)
84 }
85 if got := writer.VerifyWrite(ctx, intent); got != tool.WriteSatisfied {
86 t.Fatalf("overlay=%s", got)
87 }
88 overlay.available = false
89 if got := writer.VerifyWrite(ctx, intent); got != tool.WriteUnknown {
90 t.Fatalf("unavailable=%s", got)
91 }
92 overlay.available = true
93 overlay.id = "replacement-transport"
94 if got := writer.VerifyWrite(ctx, intent); got != tool.WriteUnknown {
95 t.Fatalf("replacement=%s", got)
96 }
97 }
98
99 func TestWriteIntentFailureDoesNotCreateDirectories(t *testing.T) {
100 root := t.TempDir()
101 path := filepath.Join(root, "new", "nested", "file.txt")
102 ctx := tool.WithWriteIntentHook(context.Background(), func(tool.FileWriteIntent) error { return errors.New("disk full") })
103 args, _ := json.Marshal(map[string]string{"path": path, "content": "new"})
104 if _, err := (writeFile{}).Execute(ctx, args); err == nil {
105 t.Fatal("expected durable intent error")
106 }
107 if _, err := os.Stat(filepath.Join(root, "new")); !os.IsNotExist(err) {
108 t.Fatalf("directory created before evidence: %v", err)
109 }
110 }
111
112 func TestWriteRecoveryRejectsChangedSymlinkTarget(t *testing.T) {
113 root := t.TempDir()
114 first, second, link := filepath.Join(root, "first"), filepath.Join(root, "second"), filepath.Join(root, "link")
115 for _, p := range []string{first, second} {
116 if err := os.WriteFile(p, []byte("before"), 0600); err != nil {
117 t.Fatal(err)
118 }
119 }
120 if err := os.Symlink(first, link); err != nil {
121 t.Skipf("symlinks unavailable: %v", err)
122 }
123 var intent tool.FileWriteIntent
124 ctx := tool.WithWriteIntentHook(context.Background(), func(i tool.FileWriteIntent) error { intent = i; return nil })
125 args, _ := json.Marshal(map[string]string{"path": link, "content": "after"})
126 w := writeFile{}
127 if _, err := w.Execute(ctx, args); err != nil {
128 t.Fatal(err)
129 }
130 if err := os.Remove(link); err != nil {
131 t.Fatal(err)
132 }
133 if err := os.Symlink(second, link); err != nil {
134 t.Fatal(err)
135 }
136 if got := w.VerifyWrite(ctx, intent); got != tool.WriteUnknown {
137 t.Fatalf("changed symlink=%s", got)
138 }
139 }
140
141 func TestWriteRecoveryChecksEncoding(t *testing.T) {
142 path := filepath.Join(t.TempDir(), "encoded.txt")
143 if err := os.WriteFile(path, fileenc.Encode("before", fileenc.UTF16LE), 0600); err != nil {
144 t.Fatal(err)
145 }
146 var intent tool.FileWriteIntent
147 ctx := tool.WithWriteIntentHook(context.Background(), func(i tool.FileWriteIntent) error { intent = i; return nil })
148 args, _ := json.Marshal(map[string]string{"path": path, "content": "after"})
149 w := writeFile{}
150 if _, err := w.Execute(ctx, args); err != nil {
151 t.Fatal(err)
152 }
153 if got := w.VerifyWrite(ctx, intent); got != tool.WriteSatisfied {
154 t.Fatalf("encoded=%s", got)
155 }
156 if err := os.WriteFile(path, []byte("after"), 0600); err != nil {
157 t.Fatal(err)
158 }
159 if got := w.VerifyWrite(ctx, intent); got != tool.WriteConflict {
160 t.Fatalf("changed encoding=%s", got)
161 }
162 }
163
163 lines GO