返回 DeepSeek-Reasonix
branch_test.go
根目录 / internal / agent / branch_test.go
1 package agent
2
3 import (
4 "os"
5 "path/filepath"
6 "strings"
7 "testing"
8 "time"
9
10 "reasonix/internal/provider"
11 )
12
13 func TestBranchMetaIgnoresRetiredAutoRecoveryField(t *testing.T) {
14 dir := t.TempDir()
15 sessionPath := filepath.Join(dir, "legacy.jsonl")
16 metaPath := BranchMetaPath(sessionPath)
17 legacy := `{"id":"legacy","name":"kept","recovery_checkpoint_enabled":false}`
18 if err := os.WriteFile(metaPath, []byte(legacy), 0o600); err != nil {
19 t.Fatalf("write legacy branch meta: %v", err)
20 }
21
22 meta, ok, err := LoadBranchMeta(sessionPath)
23 if err != nil || !ok {
24 t.Fatalf("LoadBranchMeta ok=%v err=%v", ok, err)
25 }
26 if meta.Name != "kept" {
27 t.Fatalf("name = %q, want kept", meta.Name)
28 }
29 if err := SaveBranchMeta(sessionPath, meta); err != nil {
30 t.Fatalf("SaveBranchMeta: %v", err)
31 }
32 written, err := os.ReadFile(metaPath)
33 if err != nil {
34 t.Fatalf("read rewritten branch meta: %v", err)
35 }
36 if strings.Contains(string(written), "recovery_checkpoint_enabled") {
37 t.Fatalf("retired recovery field survived rewrite: %s", written)
38 }
39 }
40
41 func TestBranchMetaRoundTripAndList(t *testing.T) {
42 dir := t.TempDir()
43 rootPath := filepath.Join(dir, "root.jsonl")
44 childPath := filepath.Join(dir, "child.jsonl")
45
46 root := NewSession("sys")
47 root.Add(provider.Message{Role: provider.RoleUser, Content: "root prompt"})
48 if err := root.Save(rootPath); err != nil {
49 t.Fatal(err)
50 }
51 if err := TouchBranchMeta(rootPath); err != nil {
52 t.Fatal(err)
53 }
54
55 child := NewSession("sys")
56 child.Add(provider.Message{Role: provider.RoleUser, Content: "child prompt"})
57 if err := child.Save(childPath); err != nil {
58 t.Fatal(err)
59 }
60 if err := SaveBranchMeta(childPath, BranchMeta{Name: "experiment", ParentID: BranchID(rootPath), ForkTurn: 2}); err != nil {
61 t.Fatal(err)
62 }
63
64 branches, err := ListBranches(dir)
65 if err != nil {
66 t.Fatal(err)
67 }
68 if len(branches) != 2 {
69 t.Fatalf("branches = %d, want 2", len(branches))
70 }
71 var rootFound, childFound bool
72 for _, b := range branches {
73 if b.ID == "root" {
74 rootFound = true
75 }
76 if b.ParentID == "root" && b.Name == "experiment" {
77 childFound = true
78 }
79 }
80 if !rootFound {
81 t.Fatal("root branch not found")
82 }
83 if !childFound {
84 t.Fatalf("child with parent root and name experiment not found among %+v", branches)
85 }
86 }
87
88 func TestListBranchesSkipsCleanupPending(t *testing.T) {
89 dir := t.TempDir()
90 visiblePath := filepath.Join(dir, "visible.jsonl")
91 pendingPath := filepath.Join(dir, "pending.jsonl")
92
93 visible := NewSession("sys")
94 visible.Add(provider.Message{Role: provider.RoleUser, Content: "visible prompt"})
95 if err := visible.Save(visiblePath); err != nil {
96 t.Fatal(err)
97 }
98 if err := TouchBranchMeta(visiblePath); err != nil {
99 t.Fatal(err)
100 }
101
102 pending := NewSession("sys")
103 pending.Add(provider.Message{Role: provider.RoleUser, Content: "pending prompt"})
104 if err := pending.Save(pendingPath); err != nil {
105 t.Fatal(err)
106 }
107 if err := SaveBranchMeta(pendingPath, BranchMeta{Name: "pending experiment"}); err != nil {
108 t.Fatal(err)
109 }
110 if err := MarkCleanupPending(pendingPath, "delete"); err != nil {
111 t.Fatal(err)
112 }
113
114 branches, err := ListBranches(dir)
115 if err != nil {
116 t.Fatal(err)
117 }
118 if len(branches) != 1 {
119 t.Fatalf("branches = %d, want 1: %+v", len(branches), branches)
120 }
121 if branches[0].Path != visiblePath {
122 t.Fatalf("listed branch path = %q, want %q", branches[0].Path, visiblePath)
123 }
124 }
125
126 func TestSessionInFlightTurnMetaRoundTrip(t *testing.T) {
127 dir := t.TempDir()
128 path := filepath.Join(dir, "in-flight.jsonl")
129 sess := NewSession("sys")
130 sess.Add(provider.Message{Role: provider.RoleUser, Content: "work"})
131 if err := sess.Save(path); err != nil {
132 t.Fatal(err)
133 }
134 if err := TouchBranchMeta(path); err != nil {
135 t.Fatal(err)
136 }
137 before, ok, err := LoadBranchMeta(path)
138 if err != nil || !ok {
139 t.Fatalf("LoadBranchMeta ok=%v err=%v", ok, err)
140 }
141 updatedAt := before.UpdatedAt
142
143 if err := MarkSessionInFlightTurn(path, 1, true); err != nil {
144 t.Fatal(err)
145 }
146 marked, ok, err := LoadBranchMeta(path)
147 if err != nil || !ok {
148 t.Fatalf("LoadBranchMeta marked ok=%v err=%v", ok, err)
149 }
150 if marked.InFlightTurn == nil {
151 t.Fatal("in-flight turn marker missing")
152 }
153 if marked.InFlightTurn.StartMessageIndex != 1 || !marked.InFlightTurn.PreserveUser {
154 t.Fatalf("in-flight marker = %+v, want index=1 preserveUser=true", marked.InFlightTurn)
155 }
156 if marked.InFlightTurn.StartedAt.IsZero() || time.Since(marked.InFlightTurn.StartedAt) > time.Minute {
157 t.Fatalf("unexpected marker timestamp: %v", marked.InFlightTurn.StartedAt)
158 }
159 if !marked.UpdatedAt.Equal(updatedAt) {
160 t.Fatalf("MarkSessionInFlightTurn updated activity time: got %v want %v", marked.UpdatedAt, updatedAt)
161 }
162
163 if err := UpdateSessionMeta(path, "model-a", "preview", 1, true); err != nil {
164 t.Fatal(err)
165 }
166 refreshed, ok, err := LoadBranchMeta(path)
167 if err != nil || !ok {
168 t.Fatalf("LoadBranchMeta refreshed ok=%v err=%v", ok, err)
169 }
170 if refreshed.InFlightTurn == nil {
171 t.Fatal("UpdateSessionMeta dropped in-flight marker")
172 }
173 if refreshed.InFlightTurn.StartMessageIndex != 1 || !refreshed.InFlightTurn.PreserveUser {
174 t.Fatalf("refreshed in-flight marker = %+v, want index=1 preserveUser=true", refreshed.InFlightTurn)
175 }
176 updatedAt = refreshed.UpdatedAt
177
178 if err := ClearSessionInFlightTurn(path); err != nil {
179 t.Fatal(err)
180 }
181 cleared, ok, err := LoadBranchMeta(path)
182 if err != nil || !ok {
183 t.Fatalf("LoadBranchMeta cleared ok=%v err=%v", ok, err)
184 }
185 if cleared.InFlightTurn != nil {
186 t.Fatalf("in-flight marker survived clear: %+v", cleared.InFlightTurn)
187 }
188 if !cleared.UpdatedAt.Equal(updatedAt) {
189 t.Fatalf("ClearSessionInFlightTurn updated activity time: got %v want %v", cleared.UpdatedAt, updatedAt)
190 }
191 }
192
193 func TestSessionModelRoundTripPreservesActivity(t *testing.T) {
194 dir := t.TempDir()
195 path := filepath.Join(dir, "session.jsonl")
196 session := NewSession("sys")
197 session.Add(provider.Message{Role: provider.RoleUser, Content: "hello"})
198 if err := session.Save(path); err != nil {
199 t.Fatal(err)
200 }
201 if _, ok := LoadSessionModel(path); ok {
202 t.Fatal("fresh session should not have a stored model")
203 }
204 meta, err := EnsureBranchMeta(path)
205 if err != nil {
206 t.Fatal(err)
207 }
208
209 if err := SetBranchModelPreserveUpdated(path, "openrouter/anthropic/claude-sonnet"); err != nil {
210 t.Fatal(err)
211 }
212 model, ok := LoadSessionModel(path)
213 if !ok || model != "openrouter/anthropic/claude-sonnet" {
214 t.Fatalf("LoadSessionModel = %q, %v", model, ok)
215 }
216 updated, ok, err := LoadBranchMeta(path)
217 if err != nil || !ok {
218 t.Fatalf("LoadBranchMeta ok=%v err=%v", ok, err)
219 }
220 if !updated.UpdatedAt.Equal(meta.UpdatedAt) {
221 t.Fatalf("model write refreshed activity: before=%s after=%s", meta.UpdatedAt, updated.UpdatedAt)
222 }
223 }
224
224 lines GO