返回 DeepSeek-Reasonix
history_outline_test.go
根目录 / internal / session / history_outline_test.go
1 package session
2
3 import (
4 "context"
5 "encoding/json"
6 "fmt"
7 "path/filepath"
8 "reasonix/internal/projectiondb"
9 "reasonix/internal/provider"
10 "strings"
11 "testing"
12 )
13
14 func TestHistoryOutlineWholeHistoryAndFixedCut(t *testing.T) {
15 service, err := NewService("local", NewFilesystemPersistence(filepath.Join(t.TempDir(), "sessions")))
16 if err != nil {
17 t.Fatal(err)
18 }
19 t.Cleanup(func() { _ = service.CloseAll(context.Background()) })
20 runtime, err := service.Create(t.Context(), CreateOptions{SessionID: "outline"})
21 if err != nil {
22 t.Fatal(err)
23 }
24 ids := make([]string, 240)
25 for i := range ids {
26 ids[i] = fmt.Sprintf("question-%d", i+1)
27 }
28 appendWindowMessages(t, runtime, ids...)
29 query, ref := service.Query(), runtime.Ref()
30 if _, _, err := query.prepareHistoryIndex(t.Context(), ref); err != nil {
31 t.Fatal(err)
32 }
33 windowReady(t, query, ref, HistoryWindowRequest{Anchor: "newest", Limit: 1})
34 first, err := query.ReadHistoryOutline(t.Context(), ref, HistoryOutlineRequest{Limit: 2})
35 if err != nil || first.Status != "ready" || first.TotalTurns != 240 || len(first.Entries) != 2 || first.Entries[0].MessageID != ids[0] {
36 t.Fatalf("first: %+v, %v", first, err)
37 }
38 appendWindowMessages(t, runtime, "later")
39 if _, _, err := query.prepareHistoryIndex(t.Context(), ref); err != nil {
40 t.Fatal(err)
41 }
42 windowReady(t, query, ref, HistoryWindowRequest{Anchor: "newest"})
43 last, err := query.ReadHistoryOutline(t.Context(), ref, HistoryOutlineRequest{Generation: first.Generation, SnapshotSequence: &first.SnapshotSequence, StartTurn: 239})
44 if err != nil || last.TotalTurns != 240 || len(last.Entries) != 2 || !last.Done || last.Entries[1].MessageID != ids[239] {
45 t.Fatalf("last: %+v, %v", last, err)
46 }
47 stale, err := query.ReadHistoryOutline(t.Context(), ref, HistoryOutlineRequest{Generation: "replaced"})
48 if err != nil || stale.Status != "stale_cursor" || stale.Entries == nil {
49 t.Fatalf("stale: %+v, %v", stale, err)
50 }
51 }
52
53 func TestHistoryOutlinePreviewsVersionsAndCompatibleIndexes(t *testing.T) {
54 root := filepath.Join(t.TempDir(), "sessions")
55 service, err := NewService("local", NewFilesystemPersistence(root))
56 if err != nil {
57 t.Fatal(err)
58 }
59 t.Cleanup(func() { _ = service.CloseAll(context.Background()) })
60 runtime, err := service.Create(t.Context(), CreateOptions{SessionID: "outline-versions"})
61 if err != nil {
62 t.Fatal(err)
63 }
64 write := func(operation string, messages ...provider.Message) {
65 t.Helper()
66 events := make([]Event, 0, len(messages))
67 for _, message := range messages {
68 payload, _ := json.Marshal(map[string]any{"message": message})
69 events = append(events, Event{Kind: "message/upsert", Payload: payload})
70 }
71 if _, err := runtime.Session().Append(t.Context(), Batch{OperationID: operation, Events: events}); err != nil {
72 t.Fatal(err)
73 }
74 if _, err := runtime.Session().Flush(t.Context()); err != nil {
75 t.Fatal(err)
76 }
77 if _, _, err := service.Query().prepareHistoryIndex(t.Context(), runtime.Ref()); err != nil {
78 t.Fatal(err)
79 }
80 }
81 write("initial", provider.Message{ID: "u1", Role: provider.RoleUser, Content: strings.Repeat("问", 100)},
82 provider.Message{ID: "a1", Role: provider.RoleAssistant, Content: "first answer"},
83 provider.Message{ID: "a2", Role: provider.RoleAssistant, Content: "last answer"},
84 provider.Message{ID: "u2", Role: provider.RoleUser, Content: "second"})
85 first, err := service.Query().ReadHistoryOutline(t.Context(), runtime.Ref(), HistoryOutlineRequest{})
86 if err != nil || len(first.Entries) != 2 || first.Entries[0].Answer != "last answer" || len([]rune(first.Entries[0].Prompt)) > 50 {
87 t.Fatalf("previews: %+v %v", first, err)
88 }
89 write("edit", provider.Message{ID: "a2", Role: provider.RoleAssistant, Content: "edited answer"})
90 old, err := service.Query().ReadHistoryOutline(t.Context(), runtime.Ref(), HistoryOutlineRequest{Generation: first.Generation, SnapshotSequence: &first.SnapshotSequence})
91 if err != nil || old.Entries[0].Answer != "last answer" {
92 t.Fatalf("fixed version: %+v %v", old, err)
93 }
94 latest, err := service.Query().ReadHistoryOutline(t.Context(), runtime.Ref(), HistoryOutlineRequest{})
95 if err != nil || latest.Entries[0].Answer != "edited answer" {
96 t.Fatalf("latest: %+v %v", latest, err)
97 }
98 // Optional indexes must not advance the schema version older readers support.
99 handle, err := projectiondb.Open(t.Context(), projectiondb.OpenOptions{Path: historyIndexPath(root, runtime.Ref().SessionID), Migrations: historyMigrations, RequireDisk: true})
100 if err != nil {
101 t.Fatal(err)
102 }
103 defer handle.DB.Close()
104 var version int
105 if err := handle.DB.QueryRow(`SELECT MAX(version) FROM schema_migrations`).Scan(&version); err != nil || version != 10 {
106 t.Fatalf("schema=%d err=%v", version, err)
107 }
108 for _, query := range []string{
109 `SELECT message_id FROM messages WHERE visible_user=1 AND visible_turn>=1 AND event_sequence<=100 AND (valid_to=0 OR valid_to>100) ORDER BY visible_turn,position LIMIT 128`,
110 `SELECT preview FROM messages WHERE visible_turn=1 AND role='assistant' AND event_sequence<=100 AND (valid_to=0 OR valid_to>100) ORDER BY position DESC LIMIT 1`,
111 } {
112 rows, err := handle.DB.Query("EXPLAIN QUERY PLAN " + query)
113 if err != nil {
114 t.Fatal(err)
115 }
116 var plan strings.Builder
117 for rows.Next() {
118 var a, b, c int
119 var detail string
120 if err := rows.Scan(&a, &b, &c, &detail); err != nil {
121 t.Fatal(err)
122 }
123 plan.WriteString(detail)
124 }
125 rows.Close()
126 if !strings.Contains(plan.String(), "messages_outline_") || strings.Contains(plan.String(), "SCAN messages") {
127 t.Fatalf("unindexed directory query: %s", plan.String())
128 }
129 }
130 // Interrupted additive installation can be retried with no data migration.
131 if _, err := handle.DB.Exec(`DROP INDEX messages_outline_answers`); err != nil {
132 t.Fatal(err)
133 }
134 if err := ensureHistoryOutlineIndexes(t.Context(), handle.DB); err != nil {
135 t.Fatal(err)
136 }
137 if err := handle.DB.Close(); err != nil {
138 t.Fatal(err)
139 }
140 for i, ids := range []string{`["u1","a1","a2"]`, `["u2"]`} {
141 payload := json.RawMessage(`{"messageIds":` + ids + `}`)
142 if _, err := runtime.Session().Append(t.Context(), Batch{OperationID: fmt.Sprintf("retract-%d", i), Events: []Event{{Kind: "message/retract", Payload: payload}}}); err != nil {
143 t.Fatal(err)
144 }
145 if _, err := runtime.Session().Flush(t.Context()); err != nil {
146 t.Fatal(err)
147 }
148 if _, _, err := service.Query().prepareHistoryIndex(t.Context(), runtime.Ref()); err != nil {
149 t.Fatal(err)
150 }
151 page, err := service.Query().ReadHistoryOutline(t.Context(), runtime.Ref(), HistoryOutlineRequest{})
152 if err != nil || page.Status != "ready" || page.TotalTurns != 1-i || len(page.Entries) != 1-i || page.Entries == nil {
153 t.Fatalf("retraction %d: %+v %v", i, page, err)
154 }
155 if i == 0 && (page.Entries[0].Turn != 1 || page.Entries[0].MessageID != "u2") {
156 t.Fatalf("reindexed identity: %+v", page)
157 }
158 if i == 1 && !page.Done {
159 t.Fatal("empty directory must be complete")
160 }
161 }
162 }
163
163 lines GO