返回 DeepSeek-Reasonix
save_tool_checkpoint_test.go
根目录 / internal / agent / save_tool_checkpoint_test.go
1 package agent
2
3 import (
4 "bytes"
5 "fmt"
6 "os"
7 "path/filepath"
8 "testing"
9
10 "reasonix/internal/provider"
11 "reasonix/internal/store"
12 )
13
14 type checkpointObserver struct{ events []SessionPersistEvent }
15
16 func (o *checkpointObserver) EnqueueSessionPersist(e SessionPersistEvent) bool {
17 o.events = append(o.events, e)
18 return true
19 }
20
21 func TestToolCheckpointDefersOnlyDerivedProjection(t *testing.T) {
22 path := schemaOneSessionPath(t, "session.jsonl")
23 s := NewSession("system")
24 s.Add(provider.Message{Role: provider.RoleUser, Content: "task"})
25 bindSessionWriter(t, s, path)
26 observer := &checkpointObserver{}
27 s.SetPersistObserver(observer)
28 if err := s.SaveSnapshot(path); err != nil {
29 t.Fatal(err)
30 }
31 before, err := os.ReadFile(store.SessionDisplayIndex(path))
32 if err != nil {
33 t.Fatal(err)
34 }
35 observer.events = nil
36 for i := range 3 {
37 id := fmt.Sprint(i)
38 s.AddBatch(provider.Message{Role: provider.RoleAssistant, ToolCalls: []provider.ToolCall{{ID: id, Name: "write_file", Arguments: `{}`}}}, provider.Message{Role: provider.RoleTool, ToolCallID: id, Name: "write_file", Content: "done", ToolRunState: provider.ToolRunCompleted})
39 if err := s.SaveToolCheckpoint(path, false); err != nil {
40 t.Fatal(err)
41 }
42 loaded, err := LoadSession(path)
43 if err != nil {
44 t.Fatal(err)
45 }
46 if len(loaded.Snapshot()) != len(s.Snapshot()) {
47 t.Fatal("canonical receipt was not durable")
48 }
49 }
50 after, err := os.ReadFile(store.SessionDisplayIndex(path))
51 if err != nil {
52 t.Fatal(err)
53 }
54 if !bytes.Equal(before, after) || len(observer.events) != 0 {
55 t.Fatal("tool checkpoint refreshed the display projection")
56 }
57 if s.snapshotUpToDate(path) {
58 t.Fatal("deferred projection bypasses normal save")
59 }
60 if err := s.SaveSnapshot(path); err != nil {
61 t.Fatal(err)
62 }
63 after, err = os.ReadFile(store.SessionDisplayIndex(path))
64 if err != nil {
65 t.Fatal(err)
66 }
67 if bytes.Equal(before, after) || len(observer.events) != 1 || !s.snapshotUpToDate(path) {
68 t.Fatal("normal save did not publish deferred projection")
69 }
70 if observer.events[0].Rewrite {
71 t.Fatal("append checkpoint falsely invalidated history as a rewrite")
72 }
73 }
74
75 func TestToolCheckpointRewriteKeepsCanonicalEvidence(t *testing.T) {
76 path := filepath.Join(t.TempDir(), "session.jsonl")
77 s := NewSession("system")
78 s.Add(provider.Message{Role: provider.RoleUser, Content: "old"})
79 bindSessionWriter(t, s, path)
80 if err := s.SaveSnapshot(path); err != nil {
81 t.Fatal(err)
82 }
83 observer := &checkpointObserver{}
84 s.SetPersistObserver(observer)
85 s.mu.Lock()
86 s.Messages[1].Content = "repaired"
87 s.version++
88 s.rewriteVersion++
89 s.mu.Unlock()
90 if err := s.SaveToolCheckpoint(path, true); err != nil {
91 t.Fatal(err)
92 }
93 loaded, err := LoadSession(path)
94 if err != nil {
95 t.Fatal(err)
96 }
97 if len(observer.events) != 1 || !observer.events[0].Rewrite {
98 t.Fatal("rewrite checkpoint did not invalidate indexed history")
99 }
100 if loaded.Snapshot()[1].Content != "repaired" {
101 t.Fatal("rewrite was not durable")
102 }
103 if err := s.SaveSnapshot(path); err != nil {
104 t.Fatal(err)
105 }
106 }
107
108 func TestToolCheckpointReloadRefreshesProjection(t *testing.T) {
109 path := schemaOneSessionPath(t, "session.jsonl")
110 s := NewSession("system")
111 if err := s.SaveSnapshot(path); err != nil {
112 t.Fatal(err)
113 }
114 s.Add(provider.Message{Role: provider.RoleUser, Content: "durable task"})
115 if err := s.SaveToolCheckpoint(path, false); err != nil {
116 t.Fatal(err)
117 }
118 loaded, err := LoadSession(path)
119 if err != nil {
120 t.Fatal(err)
121 }
122 if err := loaded.SaveSnapshot(path); err != nil {
123 t.Fatal(err)
124 }
125 index, err := LoadSessionDisplayIndex(store.SessionDisplayIndex(path))
126 if err != nil {
127 t.Fatal(err)
128 }
129 if index.MessageCount != len(loaded.Snapshot()) {
130 t.Fatal("restart left stale projection")
131 }
132 }
133
133 lines GO