返回 DeepSeek-Reasonix
session_display_pager_import_progress_test.go
根目录 / internal / agent / session_display_pager_import_progress_test.go
1 package agent
2
3 import (
4 "bytes"
5 "context"
6 "database/sql"
7 "encoding/json"
8 "errors"
9 "fmt"
10 "os"
11 "path/filepath"
12 "reflect"
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 TestDisplayPagerImportResumesCommittedEntries(t *testing.T) {
24 for _, mode := range []string{"header-first", "entries-first", "public-open", "last-entry", "two-cancels", "damaged-progress", "changed-source"} {
25 t.Run(mode, func(t *testing.T) {
26 messages := make([]provider.Message, 1536)
27 for i := range messages {
28 messages[i] = provider.Message{Role: provider.RoleUser, Content: "original message"}
29 }
30 digest, err := digestSessionMessages(messages)
31 if err != nil {
32 t.Fatal(err)
33 }
34 idx := BuildSessionDisplayIndex(messages, 3, true, digest)
35 indexPath := filepath.Join(t.TempDir(), "old.display-index.json")
36 source := filepath.Join(filepath.Dir(indexPath), "old.jsonl")
37 fingerprint := "source"
38 if mode == "public-open" {
39 indexPath = store.SessionDisplayIndex(source)
40 if err := writeSessionMessages(source, messages); err != nil {
41 t.Fatal(err)
42 }
43 if err := SaveBranchMeta(source, BranchMeta{Revision: 3, ContentDigest: digestString(digest)}); err != nil {
44 t.Fatal(err)
45 }
46 }
47 body, err := json.MarshalIndent(idx, "", " ")
48 if err != nil {
49 t.Fatal(err)
50 }
51 if mode == "entries-first" {
52 var header map[string]json.RawMessage
53 if err := json.Unmarshal(body, &header); err != nil {
54 t.Fatal(err)
55 }
56 entries := header["entries"]
57 delete(header, "entries")
58 rest, err := json.Marshal(header)
59 if err != nil {
60 t.Fatal(err)
61 }
62 body = append(append([]byte(`{"entries":`), entries...), append([]byte{','}, rest[1:]...)...)
63 }
64 if err := os.WriteFile(indexPath, body, 0600); err != nil {
65 t.Fatal(err)
66 }
67 opts := projectiondb.OpenOptions{Path: filepath.Join(t.TempDir(), "display.sqlite"), Migrations: displayPagerMigrations, RequireDisk: true, MaxOpenConns: 1, ResumeKey: "same-source"}
68 if mode == "public-open" {
69 info, err := os.Stat(source)
70 if err != nil {
71 t.Fatal(err)
72 }
73 after := info.ModTime().Add(time.Second)
74 if err := os.Chtimes(indexPath, after, after); err != nil {
75 t.Fatal(err)
76 }
77 indexInfo, err := os.Stat(indexPath)
78 if err != nil {
79 t.Fatal(err)
80 }
81 target, version := fileops.DiskSnapshot(source, info)
82 fingerprint = fmt.Sprintf("%s:%s:%d:%d", target.Key, version, indexInfo.Size(), indexInfo.ModTime().UnixNano())
83 opts.ResumeKey = "display-import-v1:" + fingerprint
84 }
85 ctx, cancel := context.WithCancel(t.Context())
86 stopAt := 1024
87 if mode == "last-entry" {
88 stopAt = len(messages)
89 }
90 err = projectiondb.Rebuild(ctx, opts, func(ctx context.Context, db *sql.DB) error {
91 return importDisplayPagerObserved(ctx, db, indexPath, fingerprint, func(count int) {
92 if count == stopAt {
93 cancel()
94 }
95 })
96 })
97 cancel()
98 if !errors.Is(err, context.Canceled) {
99 t.Fatalf("interrupted import: %v", err)
100 }
101 if mode == "two-cancels" {
102 ctx, cancel := context.WithCancel(t.Context())
103 err = projectiondb.Rebuild(ctx, opts, func(ctx context.Context, db *sql.DB) error {
104 return importDisplayPagerObserved(ctx, db, indexPath, fingerprint, func(count int) {
105 if count != stopAt+historywork.BatchEntries {
106 t.Fatalf("resumed at unexpected position: %d", count)
107 }
108 cancel()
109 })
110 })
111 cancel()
112 if !errors.Is(err, context.Canceled) {
113 t.Fatalf("second interrupted import: %v", err)
114 }
115 }
116 if mode == "damaged-progress" {
117 pending := opts
118 pending.Path += ".rebuild-pending"
119 handle, err := projectiondb.Open(t.Context(), pending)
120 if err != nil {
121 t.Fatal(err)
122 }
123 _, err = handle.DB.Exec(`UPDATE metadata SET value='broken' WHERE key='display_import_progress'`)
124 _ = handle.DB.Close()
125 if err != nil {
126 t.Fatal(err)
127 }
128 }
129 if mode == "changed-source" {
130 info, err := os.Stat(indexPath)
131 if err != nil {
132 t.Fatal(err)
133 }
134 body = bytes.ReplaceAll(body, []byte("original message"), []byte("replaced message"))
135 idx.ListingPreview = "replaced message"
136 if err := os.WriteFile(indexPath, body, 0600); err != nil {
137 t.Fatal(err)
138 }
139 if err := os.Chtimes(indexPath, info.ModTime(), info.ModTime()); err != nil {
140 t.Fatal(err)
141 }
142 }
143 meter := &historywork.Coordinator{}
144 first := 0
145 if mode == "public-open" {
146 pager, err := OpenDisplayPager(meter.Context(t.Context()), source, opts.Path)
147 if err != nil {
148 t.Fatal(err)
149 }
150 _ = pager.Close()
151 if pager.Built || pager.Header.ContentDigest != idx.ContentDigest {
152 t.Fatal("public reader did not retain the imported generation")
153 }
154 } else {
155 err = projectiondb.Rebuild(meter.Context(t.Context()), opts, func(ctx context.Context, db *sql.DB) error {
156 return importDisplayPagerObserved(ctx, db, indexPath, fingerprint, func(count int) {
157 if first == 0 {
158 first = count
159 }
160 })
161 })
162 if err != nil {
163 t.Fatal(err)
164 }
165 }
166 switch mode {
167 case "changed-source", "damaged-progress":
168 if first != historywork.BatchEntries {
169 t.Fatalf("invalid generation resumed at %d", first)
170 }
171 default:
172 if got := meter.Diagnostics().InstrumentedReadBytes; got >= int64(len(body))*3/4 {
173 t.Fatalf("completed prefix read again: %d/%d", got, len(body))
174 }
175 }
176 handle, err := projectiondb.Open(t.Context(), opts)
177 if err != nil {
178 t.Fatal(err)
179 }
180 defer handle.DB.Close()
181 pager := &DisplayPager{DB: handle.DB, ctx: t.Context()}
182 last, err := pager.Entry(len(messages) - 1)
183 if err != nil || !reflect.DeepEqual(last, idx.Entries[len(messages)-1]) {
184 t.Fatalf("resumed entry changed: %+v %v", last, err)
185 }
186 var header string
187 if err := handle.DB.QueryRow(`SELECT value FROM metadata WHERE key='header'`).Scan(&header); err != nil {
188 t.Fatal(err)
189 }
190 var got SessionDisplayIndex
191 if err := json.Unmarshal([]byte(header), &got); err != nil {
192 t.Fatal(err)
193 }
194 idx.Entries = nil
195 if !reflect.DeepEqual(&got, idx) {
196 t.Fatalf("header changed: %+v want %+v", got, idx)
197 }
198 after, err := os.ReadFile(indexPath)
199 if err != nil || !bytes.Equal(after, body) {
200 t.Fatalf("import changed old index: %v", err)
201 }
202 })
203 }
204 }
205
206 func TestDisplayPagerImportResumeFramingRejectsInvalidSeparators(t *testing.T) {
207 for _, tail := range []string{",]}", ", ,{}]}", "{}]}", "garbage"} {
208 t.Run(tail, func(t *testing.T) {
209 path := filepath.Join(t.TempDir(), "index")
210 if err := os.WriteFile(path, []byte("prefix"+tail), 0600); err != nil {
211 t.Fatal(err)
212 }
213 f, err := os.Open(path)
214 if err != nil {
215 t.Fatal(err)
216 }
217 defer f.Close()
218 if _, _, err := displayImportDecoder(t.Context(), f, int64(len("prefix"))); err == nil {
219 t.Fatal("invalid continuation accepted")
220 }
221 })
222 }
223 }
224
224 lines GO