返回 DeepSeek-Reasonix
recovery_stable_test.go
根目录 / internal / agent / recovery_stable_test.go
1 package agent
2
3 import (
4 "os"
5 "path/filepath"
6 "strings"
7 "testing"
8
9 "reasonix/internal/provider"
10 )
11
12 func TestStableRecoveryPathPeelsNestedNames(t *testing.T) {
13 dir := t.TempDir()
14 root := filepath.Join(dir, "chat.jsonl")
15 nested := filepath.Join(dir, "chat-recovery-aaaaaaaaaaaaaaaa-recovery-bbbbbbbbbbbbbbbb.jsonl")
16 gen := "gen-7"
17 if got, want := stableRecoverySessionPath(nested, gen), stableRecoverySessionPath(root, gen); got != want {
18 t.Fatalf("nested path %q, want %q", got, want)
19 }
20 }
21
22 func TestSaveRecoveryBranchStampsStableRootDepth(t *testing.T) {
23 dir := t.TempDir()
24
25 path, stale := divergedSessionPair(t, dir, "session.jsonl")
26 info, err := stale.SaveRecoveryBranch(RecoveryBranchOptions{OriginalPath: path})
27 if err != nil {
28 t.Fatalf("SaveRecoveryBranch: %v", err)
29 }
30 if info.Meta.RecoveryDepth != 1 {
31 t.Fatalf("first fork depth = %d, want 1", info.Meta.RecoveryDepth)
32 }
33 if info.Meta.ParentID != BranchID(path) {
34 t.Fatalf("parent = %q, want root %q", info.Meta.ParentID, BranchID(path))
35 }
36
37 deeper, staleDeeper := divergedSessionPair(t, dir, "deeper.jsonl")
38 stampRecoveryMeta(t, deeper, 1)
39 info, err = staleDeeper.SaveRecoveryBranch(RecoveryBranchOptions{OriginalPath: deeper})
40 if err != nil {
41 t.Fatalf("SaveRecoveryBranch from recovered file: %v", err)
42 }
43 if info.Meta.RecoveryDepth != 1 {
44 t.Fatalf("stable fork depth = %d, want 1", info.Meta.RecoveryDepth)
45 }
46 if info.Meta.ParentID != recoveryRootID(deeper) {
47 t.Fatalf("parent = %q, want root %q", info.Meta.ParentID, recoveryRootID(deeper))
48 }
49
50 capped, staleCapped := divergedSessionPair(t, dir, "capped.jsonl")
51 stampRecoveryMeta(t, capped, SessionRecoveryMaxDepth)
52 info, err = staleCapped.SaveRecoveryBranch(RecoveryBranchOptions{OriginalPath: capped})
53 if err != nil {
54 t.Fatalf("SaveRecoveryBranch at historical cap: %v", err)
55 }
56 if info.Path == "" || info.Path == capped {
57 t.Fatalf("capped parent did not write a stable recovery file: %q", info.Path)
58 }
59 forks, err := filepath.Glob(filepath.Join(dir, "capped-recovery-*.jsonl"))
60 if err != nil {
61 t.Fatalf("glob: %v", err)
62 }
63 var recovered []string
64 for _, fork := range forks {
65 if !strings.HasSuffix(fork, ".events.jsonl") {
66 recovered = append(recovered, fork)
67 }
68 }
69 if len(recovered) != 1 {
70 t.Fatalf("stable recovery copies = %v, want 1", recovered)
71 }
72 }
73
74 func TestRepeatedDivergenceRewritesOneRecoveryBranch(t *testing.T) {
75 dir := t.TempDir()
76 path, stale := divergedSessionPair(t, dir, "session.jsonl")
77 first, err := stale.SaveRecoveryBranch(RecoveryBranchOptions{OriginalPath: path})
78 if err != nil {
79 t.Fatalf("first SaveRecoveryBranch: %v", err)
80 }
81 stale.Add(provider.Message{Role: provider.RoleAssistant, Content: "more local"})
82 second, err := stale.SaveRecoveryBranch(RecoveryBranchOptions{OriginalPath: first.Path})
83 if err != nil {
84 t.Fatalf("second SaveRecoveryBranch: %v", err)
85 }
86 if second.Path != first.Path {
87 t.Fatalf("recovery path rotated %q -> %q", first.Path, second.Path)
88 }
89 matches, err := filepath.Glob(filepath.Join(dir, "*-recovery-*.jsonl"))
90 if err != nil {
91 t.Fatal(err)
92 }
93 var files []string
94 for _, m := range matches {
95 if !strings.HasSuffix(m, ".events.jsonl") {
96 files = append(files, m)
97 }
98 }
99 if len(files) != 1 {
100 t.Fatalf("recovery files = %v, want 1", files)
101 }
102 }
103
104 func TestRecoveryGenerationDoesNotReusePreviousProcessPath(t *testing.T) {
105 dir := t.TempDir()
106 root := filepath.Join(dir, "session.jsonl")
107 oldPath := stableRecoverySessionPath(root, "gen-1")
108 if err := os.WriteFile(oldPath, []byte(`{"role":"user","content":"previous process"}`+"\n"), 0o600); err != nil {
109 t.Fatal(err)
110 }
111
112 current := NewSession("sys")
113 current.Add(provider.Message{Role: provider.RoleUser, Content: "current process"})
114 writer, err := AcquireSessionWriter(root)
115 if err != nil {
116 t.Fatal(err)
117 }
118 defer writer.Release()
119 if err := writer.Bind(current, 1); err != nil {
120 t.Fatal(err)
121 }
122
123 info, err := current.SaveConflictRecoveryBranch(RecoveryBranchOptions{OriginalPath: root})
124 if err != nil {
125 t.Fatalf("SaveConflictRecoveryBranch: %v", err)
126 }
127 if info.Path == oldPath {
128 t.Fatalf("recovery reused previous-process path %q", oldPath)
129 }
130 }
131
132 func TestRecoveryGenerationRotatesAfterUnexpectedCollision(t *testing.T) {
133 dir := t.TempDir()
134 root := filepath.Join(dir, "session.jsonl")
135 current := NewSession("sys")
136 current.Add(provider.Message{Role: provider.RoleUser, Content: "current process"})
137 writer, err := AcquireSessionWriter(root)
138 if err != nil {
139 t.Fatal(err)
140 }
141 defer writer.Release()
142 if err := writer.Bind(current, 7); err != nil {
143 t.Fatal(err)
144 }
145
146 collidingPath := stableRecoverySessionPath(root, current.recoveryGenerationKey())
147 if err := os.WriteFile(collidingPath, []byte(`{"role":"user","content":"independent recovery"}`+"\n"), 0o600); err != nil {
148 t.Fatal(err)
149 }
150 info, err := current.SaveConflictRecoveryBranch(RecoveryBranchOptions{OriginalPath: root})
151 if err != nil {
152 t.Fatalf("SaveConflictRecoveryBranch: %v", err)
153 }
154 if info.Path == collidingPath {
155 t.Fatalf("collision did not rotate recovery path %q", collidingPath)
156 }
157 }
158
159 func TestRecoveryGenerationStaysStableAfterRecoveryLeaseRebind(t *testing.T) {
160 dir := t.TempDir()
161 root := filepath.Join(dir, "session.jsonl")
162 sess := NewSession("sys")
163 firstWriter, err := AcquireSessionWriter(root)
164 if err != nil {
165 t.Fatal(err)
166 }
167 if err := firstWriter.Bind(sess, NextSessionWriteGeneration()); err != nil {
168 t.Fatal(err)
169 }
170 lane := sess.recoveryGenerationKey()
171 firstPath := stableRecoverySessionPath(root, lane)
172 firstWriter.Release()
173
174 secondWriter, err := AcquireSessionWriter(firstPath)
175 if err != nil {
176 t.Fatal(err)
177 }
178 defer secondWriter.Release()
179 if err := secondWriter.Bind(sess, NextSessionWriteGeneration()); err != nil {
180 t.Fatal(err)
181 }
182 if got := sess.recoveryGenerationKey(); got != lane {
183 t.Fatalf("recovery lane changed across lease rebind: %q -> %q", lane, got)
184 }
185 if got := stableRecoverySessionPath(firstPath, sess.recoveryGenerationKey()); got != firstPath {
186 t.Fatalf("second recovery target = %q, want stable %q", got, firstPath)
187 }
188 }
189
189 lines GO