返回 DeepSeek-Reasonix
turn_changes_test.go
根目录 / internal / checkpoint / turn_changes_test.go
1 package checkpoint
2
3 import (
4 "encoding/json"
5 "os"
6 "path/filepath"
7 "runtime"
8 "strings"
9 "testing"
10 )
11
12 func turnChangeWrite(t *testing.T, s *Store, path, content string) {
13 t.Helper()
14 s.CaptureBefore(path, CaptureBeforeOpts{})
15 if err := os.WriteFile(filepath.Join(s.root, path), []byte(content), 0o644); err != nil {
16 t.Fatal(err)
17 }
18 s.CaptureAfter(path, CaptureAfterOpts{Seq: 1})
19 }
20
21 func TestTurnChangesNetAndFrozenHistory(t *testing.T) {
22 root, dir := t.TempDir(), t.TempDir()
23 s := New(dir, root)
24 if err := os.WriteFile(filepath.Join(root, "dirty.txt"), []byte("user dirty line\nold\n"), 0o644); err != nil {
25 t.Fatal(err)
26 }
27 s.Begin(0, "first", 0)
28 turnChangeWrite(t, s, "dirty.txt", "user dirty line\ntemporary\n")
29 turnChangeWrite(t, s, "dirty.txt", "user dirty line\nnew\n")
30 turnChangeWrite(t, s, "created.txt", "created\n")
31 first := s.FreezeTurnChanges(0)
32 if first.Coverage != "complete" || len(first.Files) != 2 || first.Added != 2 || first.Removed != 1 {
33 t.Fatalf("net result: %+v", first)
34 }
35 if !strings.Contains(first.Files[0].Patch, "-old") || strings.Contains(first.Files[0].Patch, "-user dirty") {
36 t.Fatalf("wrong baseline: %s", first.Files[0].Patch)
37 }
38 s.Begin(1, "second", 1)
39 turnChangeWrite(t, s, "dirty.txt", "future\n")
40 s.FreezeTurnChanges(1)
41 if got := s.TurnChanges(0); got.Files[0].Patch != first.Files[0].Patch {
42 t.Fatalf("history changed: %+v", got)
43 }
44 first.Files[0].Patch = "caller mutation"
45 if got := s.TurnChanges(0); got.Files[0].Patch == first.Files[0].Patch {
46 t.Fatal("caller mutated store")
47 }
48 reloaded := New(dir, root).TurnChanges(0)
49 if reloaded.ID == "" || reloaded.Added != 2 || !strings.Contains(reloaded.Files[0].Patch, "+new") {
50 t.Fatalf("reload: %+v", reloaded)
51 }
52 if got := reloaded.Summary(); got.Files[0].Patch != "" || got.Added != reloaded.Added {
53 t.Fatal("summary leaked patch or lost facts")
54 }
55 }
56
57 func TestTurnChangesNoOpDeletionBinaryAndMode(t *testing.T) {
58 for _, tc := range []struct {
59 name, before, after string
60 remove, chmod bool
61 count, added, removed int
62 binary bool
63 }{
64 {name: "restore", before: "same\n", after: "same\n"},
65 {name: "delete", before: "one\ntwo\n", remove: true, count: 1, removed: 2},
66 {name: "binary", before: "a\x00b", after: "a\x00c", count: 1, binary: true},
67 {name: "mode", before: "same\n", after: "same\n", chmod: true, count: 1},
68 } {
69 t.Run(tc.name, func(t *testing.T) {
70 if tc.chmod && runtime.GOOS == "windows" {
71 t.Skip("Windows does not preserve POSIX execute mode")
72 }
73 root := t.TempDir()
74 s := New("", root)
75 path := filepath.Join(root, "f")
76 if err := os.WriteFile(path, []byte(tc.before), 0o644); err != nil {
77 t.Fatal(err)
78 }
79 s.Begin(0, "change", 0)
80 turnChangeWrite(t, s, "f", "temporary\n")
81 if tc.remove {
82 if err := os.Remove(path); err != nil {
83 t.Fatal(err)
84 }
85 } else {
86 if err := os.WriteFile(path, []byte(tc.after), 0o644); err != nil {
87 t.Fatal(err)
88 }
89 }
90 if tc.chmod {
91 if err := os.Chmod(path, 0o755); err != nil {
92 t.Fatal(err)
93 }
94 }
95 s.CaptureAfter("f", CaptureAfterOpts{})
96 r := s.FreezeTurnChanges(0)
97 if r.Coverage != "complete" || len(r.Files) != tc.count || r.Added != tc.added || r.Removed != tc.removed {
98 t.Fatalf("result: %+v", r)
99 }
100 if tc.count > 0 && (r.Files[0].Binary != tc.binary || r.Files[0].ModeOnly != tc.chmod) {
101 t.Fatalf("file: %+v", r.Files[0])
102 }
103 })
104 }
105 }
106
107 func TestTurnChangesFailClosed(t *testing.T) {
108 for _, cause := range []string{"external", "writer", "gap", "budget", "approximate", "unknown-after"} {
109 t.Run(cause, func(t *testing.T) {
110 s := New("", t.TempDir())
111 s.Begin(0, "change", 0)
112 content := "new\n"
113 if cause == "budget" {
114 content = strings.Repeat("a", TurnChangesBudget+1)
115 }
116 if cause == "approximate" {
117 content = strings.Repeat("line\n", 2100)
118 }
119 turnChangeWrite(t, s, "f", content)
120 switch cause {
121 case "external":
122 if err := os.WriteFile(filepath.Join(s.root, "f"), []byte("external\n"), 0o644); err != nil {
123 t.Fatal(err)
124 }
125 case "writer":
126 if !s.Barrier().TryEnterWrite() {
127 t.Fatal("enter write")
128 }
129 defer s.Barrier().ExitWrite()
130 case "gap":
131 s.RecordGap(CoverageGap{Reason: GapCaptureFailed})
132 case "unknown-after":
133 s.cur.Files[0].AfterSHA256 = ""
134 }
135 r := s.FreezeTurnChanges(0)
136 if r.Coverage != "partial" || len(r.Reasons) == 0 {
137 t.Fatalf("result: %+v", r)
138 }
139 if cause != "gap" && (r.Added != 0 || r.Removed != 0) {
140 t.Fatalf("unproven tally: %+v", r)
141 }
142 if cause == "approximate" && (len(r.Files) != 1 || !r.Files[0].Uncounted) {
143 t.Fatalf("approximation presented as exact: %+v", r)
144 }
145 })
146 }
147 }
148
149 func TestTurnChangesOldCheckpointAndJSON(t *testing.T) {
150 var c Checkpoint
151 if err := json.Unmarshal([]byte(`{"turn":0,"files":[]}`), &c); err != nil {
152 t.Fatal(err)
153 }
154 s := New("", t.TempDir())
155 s.done = []*Checkpoint{&c}
156 old := s.TurnChanges(0)
157 if old.Coverage != "unknown" || old.Files == nil || old.Reasons == nil {
158 t.Fatalf("old result: %+v", old)
159 }
160 b, err := json.Marshal(old)
161 if err != nil {
162 t.Fatal(err)
163 }
164 if strings.Contains(string(b), "null") {
165 t.Fatalf("null array: %s", b)
166 }
167 }
168
168 lines GO