返回 DeepSeek-Reasonix
session_display_pager_dag_progress_test.go
根目录 / internal / agent / session_display_pager_dag_progress_test.go
1 package agent
2
3 import (
4 "context"
5 "database/sql"
6 "errors"
7 "fmt"
8 "os"
9 "os/exec"
10 "path/filepath"
11 "reflect"
12 "strings"
13 "testing"
14 "time"
15
16 "reasonix/internal/fileops"
17 "reasonix/internal/historywork"
18 "reasonix/internal/projectiondb"
19 "reasonix/internal/provider"
20 "reasonix/internal/store"
21 )
22
23 func dagResumeOptions(t *testing.T, source, cache, head string) (projectiondb.OpenOptions, string, int64) {
24 t.Helper()
25 info, err := os.Stat(source)
26 if err != nil {
27 t.Fatal(err)
28 }
29 target, version := fileops.DiskSnapshot(source, info)
30 event, err := os.Stat(store.SessionEventLog(source))
31 if err != nil {
32 t.Fatal(err)
33 }
34 eventTarget, eventVersion := fileops.DiskSnapshot(store.SessionEventLog(source), event)
35 fingerprint := fmt.Sprintf("%s:%s:event:%s:%s:dag:%d:%d:%s", target.Key, version, eventTarget.Key, eventVersion, event.Size(), event.ModTime().UnixNano(), head)
36 return projectiondb.OpenOptions{Path: cache, Migrations: displayPagerMigrations, RequireDisk: true, MaxOpenConns: 1, ResumeKey: "dag-v1:" + fingerprint}, fingerprint, info.Size()
37 }
38
39 func dagResumeFixture(t *testing.T) (string, string) {
40 t.Helper()
41 source := dagTestSession(t)
42 if err := os.WriteFile(source, nil, 0600); err != nil {
43 t.Fatal(err)
44 }
45 at := time.Unix(1700000000, 0).UTC()
46 system, err := encodeSessionDAGMessage(dagMsg(provider.RoleSystem, "inherited system", "sys"))
47 if err != nil {
48 t.Fatal(err)
49 }
50 entries := []sessionDAGEntry{{Type: sessionDAGTypeSystem, Head: SessionMainHead, Msgs: system, At: at},
51 {Type: sessionDAGTypeFork, Head: SessionMainHead, NewHead: "fork", At: at},
52 {Type: sessionDAGTypeSelect, Head: "fork", At: at}}
53 parent := ""
54 for i := range 3 * historywork.BatchEntries {
55 id := fmt.Sprintf("node-%d", i)
56 entries = append(entries, dagMessageEntry(t, "fork", parent, "turn", dagMsg(provider.RoleUser, fmt.Sprintf("question %d %s", i, strings.Repeat("a", 4096)), id), at))
57 parent = id
58 }
59 patch, err := encodeSessionDAGMessage(dagMsg(provider.RoleUser, "patched", "node-5"))
60 if err != nil {
61 t.Fatal(err)
62 }
63 entries = append(entries, sessionDAGEntry{Type: sessionDAGTypePatch, Target: "node-5", Msgs: patch, At: at})
64 dagAppend(t, source, entries...)
65 return source, filepath.Join(t.TempDir(), "dag.sqlite")
66 }
67
68 func TestDAGPagerResumesAllDurablePhases(t *testing.T) {
69 for _, phase := range []string{"scan", "chain", "projection"} {
70 for _, mode := range []string{"cancel", "restart", "repeat", "missing", "damaged"} {
71 t.Run(phase+"/"+mode, func(t *testing.T) { checkDAGPagerResume(t, phase, mode) })
72 }
73 }
74 }
75
76 func checkDAGPagerResume(t *testing.T, phase, mode string) {
77 t.Helper()
78 source, cache := dagResumeFixture(t)
79 original, err := os.ReadFile(store.SessionEventLog(source))
80 if err != nil {
81 t.Fatal(err)
82 }
83 opts, fingerprint, size := dagResumeOptions(t, source, cache, "")
84 interrupt := func(threshold int) {
85 ctx, cancel := context.WithCancel(t.Context())
86 defer cancel()
87 err := projectiondb.Rebuild(ctx, opts, func(ctx context.Context, db *sql.DB) error {
88 return buildDAGDisplayPagerObserved(ctx, db, source, fingerprint, "", size, func(stage string, count int) {
89 if stage == phase && count >= threshold {
90 cancel()
91 }
92 })
93 })
94 if !errors.Is(err, context.Canceled) {
95 t.Fatalf("cancellation lost: %v", err)
96 }
97 }
98 if mode == "restart" {
99 executable, err := os.Executable()
100 if err != nil {
101 t.Fatal(err)
102 }
103 child := exec.CommandContext(t.Context(), executable, "-test.run=^TestDAGPagerCrashHelper$")
104 child.Env = append(os.Environ(), "REASONIX_DAG_CRASH_SOURCE="+source, "REASONIX_DAG_CRASH_CACHE="+cache, "REASONIX_DAG_CRASH_PHASE="+phase)
105 if output, err := child.CombinedOutput(); err != nil {
106 t.Fatalf("child failed: %v\n%s", err, output)
107 }
108 } else {
109 interrupt(historywork.BatchEntries)
110 }
111 if mode == "repeat" {
112 interrupt(2 * historywork.BatchEntries)
113 }
114 if mode == "missing" || mode == "damaged" {
115 pending := opts
116 pending.Path += ".rebuild-pending"
117 handle, err := projectiondb.Open(t.Context(), pending)
118 if err != nil {
119 t.Fatal(err)
120 }
121 query := `UPDATE metadata SET value='broken' WHERE key='dag_progress'`
122 if mode == "missing" {
123 query = `DELETE FROM metadata WHERE key='dag_progress'`
124 }
125 _, err = handle.DB.Exec(query)
126 _ = handle.DB.Close()
127 if err != nil {
128 t.Fatal(err)
129 }
130 }
131 var meter historywork.Coordinator
132 pager, err := OpenDisplayPager(meter.Context(t.Context()), source, cache)
133 if err != nil {
134 t.Fatal(err)
135 }
136 defer pager.Close()
137 read := meter.Diagnostics().InstrumentedReadBytes
138 if mode != "missing" && mode != "damaged" {
139 // Capture accounting before asserting pages: projection resume must not
140 // repeat the graph scan, and scan resume must skip its validated prefix.
141 limit := int64(len(original)) * 25 / 10
142 switch phase {
143 case "chain":
144 limit = int64(len(original)) * 11 / 10
145 case "projection":
146 limit = int64(len(original)) * 9 / 10
147 }
148 if read >= limit {
149 t.Fatalf("completed %s prefix read again: %d >= %d", phase, read, limit)
150 }
151 }
152 assertDAGPagerReplay(t, pager, source, "fork")
153 after, err := os.ReadFile(store.SessionEventLog(source))
154 if err != nil || string(after) != string(original) {
155 t.Fatalf("source changed: %v", err)
156 }
157 }
158
159 func assertDAGPagerReplay(t *testing.T, pager *DisplayPager, source, head string) {
160 t.Helper()
161 want, _ := dagReplay(t, source).materialize(head)
162 got, err := pager.DAGMessages(0, pager.Header.MessageCount)
163 if err != nil || len(got) != len(want) {
164 t.Fatalf("resumed page length %d != %d: %v", len(got), len(want), err)
165 }
166 for i := range got {
167 got[i].CreatedAt = want[i].CreatedAt
168 }
169 if !reflect.DeepEqual(got, want) {
170 t.Fatal("resumed view differs from native replay")
171 }
172 digest, err := ContentDigestForMessages(want)
173 if err != nil || pager.Header.ContentDigest != digest {
174 t.Fatalf("resumed digest mismatch: %v", err)
175 }
176 }
177
178 func TestDAGPagerCrashHelper(t *testing.T) {
179 source, cache, phase := os.Getenv("REASONIX_DAG_CRASH_SOURCE"), os.Getenv("REASONIX_DAG_CRASH_CACHE"), os.Getenv("REASONIX_DAG_CRASH_PHASE")
180 if source == "" || cache == "" {
181 return
182 }
183 opts, fingerprint, size := dagResumeOptions(t, source, cache, "")
184 err := projectiondb.Rebuild(t.Context(), opts, func(ctx context.Context, db *sql.DB) error {
185 return buildDAGDisplayPagerObserved(ctx, db, source, fingerprint, "", size, func(stage string, count int) {
186 if stage == phase && count >= historywork.BatchEntries {
187 os.Exit(0)
188 }
189 })
190 })
191 t.Fatalf("child did not exit: %v", err)
192 }
193
194 func TestDAGPagerResumeAfterPopulateBeforePublish(t *testing.T) {
195 source, cache := dagResumeFixture(t)
196 opts, fingerprint, size := dagResumeOptions(t, source, cache, "")
197 ctx, cancel := context.WithCancel(t.Context())
198 err := projectiondb.Rebuild(ctx, opts, func(ctx context.Context, db *sql.DB) error {
199 if err := buildDAGDisplayPager(ctx, db, source, fingerprint, "", size); err != nil {
200 return err
201 }
202 cancel()
203 return ctx.Err()
204 })
205 cancel()
206 if !errors.Is(err, context.Canceled) {
207 t.Fatal(err)
208 }
209 var meter historywork.Coordinator
210 pager, err := OpenDisplayPager(meter.Context(t.Context()), source, cache)
211 if err != nil {
212 t.Fatal(err)
213 }
214 defer pager.Close()
215 if meter.Diagnostics().InstrumentedReadBytes > historywork.ReadChunk {
216 t.Fatal("completed projection decoded again before publication")
217 }
218 assertDAGPagerReplay(t, pager, source, "fork")
219 }
220
220 lines GO