返回 DeepSeek-Reasonix
catalog_reducer_test.go
根目录 / internal / session / catalog_reducer_test.go
1 package session
2
3 import (
4 "context"
5 "encoding/json"
6 "fmt"
7 "reflect"
8 "runtime"
9 "strings"
10 "testing"
11
12 "reasonix/internal/provider"
13 )
14
15 func TestCatalogReducerMatchesCanonicalProjection(t *testing.T) {
16 r := catalogReducer{}
17 var commits []Commit
18 add := func(kind string, body any) {
19 t.Helper()
20 payload, err := json.Marshal(body)
21 if err != nil {
22 t.Fatal(err)
23 }
24 sequence := uint64(len(commits) + 1)
25 commit := Commit{TurnID: "turn", FirstSequence: sequence, EventCount: 1, Events: []Event{{Kind: kind, Sequence: sequence, Payload: payload}}}
26 commits = append(commits, commit)
27 full, err := Project(commits)
28 if err != nil {
29 t.Fatal(err)
30 }
31 if err := r.apply(commit); err != nil {
32 t.Fatal(err)
33 }
34 want, got := metadataFromProjection(Manifest{}, sequence, full), r.metadata(Manifest{})
35 if !reflect.DeepEqual(got, want) {
36 t.Fatalf("after %s: got %+v want %+v", kind, got, want)
37 }
38 if len(r.state.Messages)+len(r.state.ModelMessages)+len(r.state.Turns)+len(r.state.ActiveTools)+len(r.state.Interactions) != 0 {
39 t.Fatal("body or authority state survived metadata reduction")
40 }
41 }
42 message := func(id, text string) provider.Message {
43 return provider.Message{ID: id, Role: provider.RoleUser, RawContent: text, Content: text}
44 }
45 add("legacy/import", map[string]any{"messages": []provider.Message{message("old", "old request")}, "modelRef": "old-model"})
46 add("session/title", map[string]any{"title": "custom title"})
47 add("session/config", map[string]any{"modelRef": "new-model", "modelIdentity": "identity"})
48 add("turn/start", map[string]any{})
49 add("message/complete", map[string]any{"message": message("second", "second request")})
50 add("message/upsert", map[string]any{"message": message("old", "")})
51 add("model/context-replace", map[string]any{"messages": []provider.Message{message("model", "not a preview")}})
52 add("compaction", map[string]any{"messages": []provider.Message{message("compact", "not a preview either")}})
53 add("turn/end", map[string]any{"status": "completed"})
54 add("turn/end", map[string]any{"status": "completed"})
55 add("history/replace", map[string]any{"messages": []provider.Message{message("x", "first"), message("x", "duplicate allowed by replacement"), message("y", "last")}})
56 add("message/upsert", map[string]any{"message": message("x", "")})
57 add("message/upsert", map[string]any{"message": provider.Message{ID: "y", Role: provider.RoleUser, Origin: provider.MessageOriginHost, Content: "host"}})
58 add("history/replace", map[string]any{"messages": []provider.Message{}})
59 for i := range 100 {
60 add("message/complete", map[string]any{"message": message(fmt.Sprint(i), strings.Repeat("body", 100))})
61 add("message/upsert", map[string]any{"message": message(fmt.Sprint(i), "")})
62 }
63 }
64
65 func TestCatalogReducerKeepsFirstCompleteAndRejectsMalformedPayload(t *testing.T) {
66 for _, kind := range []string{"message/complete", "session/config", "turn/end", "tool/result"} {
67 r := catalogReducer{}
68 if err := r.apply(Commit{Events: []Event{{Kind: kind, Payload: json.RawMessage(`{}`)}}}); err == nil {
69 t.Fatalf("accepted malformed %s", kind)
70 }
71 }
72 r := catalogReducer{}
73 first := Commit{Events: []Event{{Kind: "message/complete", Payload: json.RawMessage(`{"message":{"id":"same","role":"user","content":"hello"}}`)}}}
74 repeat := Commit{Events: []Event{{Kind: "message/complete", Payload: json.RawMessage(`{"message":{"id":"same","role":"user","content":"repeat"}}`)}}}
75 for _, commit := range []Commit{first, repeat} {
76 if err := r.apply(commit); err != nil {
77 t.Fatalf("duplicate complete across commits: %v", err)
78 }
79 }
80 if got := r.metadata(Manifest{}).Preview; got != "hello" {
81 t.Fatalf("preview = %q, want the first occurrence", got)
82 }
83 }
84
85 func TestCatalogResultSequenceAdvancesOnlyForVisibleAssistantResults(t *testing.T) {
86 assistant, _ := json.Marshal(map[string]any{"message": provider.Message{ID: "answer", Role: provider.RoleAssistant, Content: "done"}})
87 commits := []Commit{
88 {TurnID: "turn-1", FirstSequence: 1, EventCount: 1, Events: []Event{{Kind: "turn/start", Sequence: 1, Payload: json.RawMessage(`{}`)}}},
89 {TurnID: "turn-1", FirstSequence: 2, EventCount: 1, Events: []Event{{Kind: "message/complete", Sequence: 2, Payload: assistant}}},
90 {TurnID: "turn-1", FirstSequence: 3, EventCount: 1, Events: []Event{{Kind: "turn/end", Sequence: 3, Payload: json.RawMessage(`{"status":"completed"}`)}}},
91 {FirstSequence: 4, EventCount: 1, Events: []Event{{Kind: "plan/state", Sequence: 4, Payload: json.RawMessage(`{"enabled":false}`)}}},
92 {FirstSequence: 5, EventCount: 1, Events: []Event{{Kind: "session/title", Sequence: 5, Payload: json.RawMessage(`{"title":"renamed"}`)}}},
93 }
94 r := catalogReducer{}
95 for _, commit := range commits {
96 if err := r.apply(commit); err != nil {
97 t.Fatal(err)
98 }
99 }
100 metadata := r.metadata(Manifest{})
101 if metadata.ResultSequence != 3 {
102 t.Fatalf("result sequence = %d, want completed answer boundary 3", metadata.ResultSequence)
103 }
104 if metadata.Sequence != 5 {
105 t.Fatalf("event sequence = %d, want all events through 5", metadata.Sequence)
106 }
107 }
108
109 type largeCatalogReader struct {
110 count int
111 baseline, peak uint64
112 allocatedPeak uint64
113 t *testing.T
114 }
115
116 func (h *largeCatalogReader) Read(ctx context.Context, cursor uint64, limit int) (EventPage, error) {
117 if err := ctx.Err(); err != nil {
118 return EventPage{}, err
119 }
120 var current runtime.MemStats
121 runtime.ReadMemStats(&current)
122 if current.HeapAlloc > h.baseline {
123 h.allocatedPeak = max(h.allocatedPeak, current.HeapAlloc-h.baseline)
124 }
125 if cursor%256 == 0 {
126 runtime.GC()
127 var stats runtime.MemStats
128 runtime.ReadMemStats(&stats)
129 if stats.HeapAlloc > h.baseline {
130 h.peak = max(h.peak, stats.HeapAlloc-h.baseline)
131 }
132 if h.peak > 32<<20 {
133 h.t.Fatalf("retained heap grew with message bodies: %.1f MiB", float64(h.peak)/(1<<20))
134 }
135 }
136 page := EventPage{}
137 for i := int(cursor); i < min(int(cursor)+limit, h.count); i++ {
138 role := provider.RoleAssistant
139 if i == 0 {
140 role = provider.RoleUser
141 }
142 payload, _ := json.Marshal(map[string]any{"message": provider.Message{ID: fmt.Sprint(i), Role: role, Content: strings.Repeat("a", 64<<10)}})
143 seq := uint64(i + 1)
144 page.Commits = append(page.Commits, Commit{FirstSequence: seq, EventCount: 1, Events: []Event{{Kind: "message/complete", Sequence: seq, Payload: payload}}})
145 page.Next = seq
146 }
147 page.Truncated = int(page.Next) < h.count
148 return page, nil
149 }
150
151 type streamingCatalogProbe struct {
152 pagedCatalogHandle
153 scans int
154 }
155
156 func (*streamingCatalogProbe) Read(context.Context, uint64, int) (EventPage, error) {
157 panic("streaming catalog unexpectedly used sparse pages")
158 }
159 func (p *streamingCatalogProbe) scanCatalog(_ context.Context, apply func(Commit) error) error {
160 p.scans++
161 return apply(Commit{Events: []Event{{Kind: "session/title", Sequence: 1, Payload: json.RawMessage(`{"title":"streamed"}`)}}})
162 }
163
164 func TestCatalogUsesSinglePassReaderWhenAvailable(t *testing.T) {
165 p := &streamingCatalogProbe{}
166 m, err := reduceCatalogMetadata(t.Context(), p, Manifest{})
167 if err != nil || m.Title != "streamed" || p.scans != 1 {
168 t.Fatalf("metadata=%+v scans=%d err=%v", m, p.scans, err)
169 }
170 }
171
172 func TestCatalogReducerLargeHistoryRetainedHeap(t *testing.T) {
173 for _, count := range []int{512, 8192, 8192} {
174 runtime.GC()
175 var stats runtime.MemStats
176 runtime.ReadMemStats(&stats)
177 h := &largeCatalogReader{count: count, baseline: stats.HeapAlloc, t: t}
178 m, err := reduceCatalogMetadata(t.Context(), h, Manifest{})
179 if err != nil || m.Sequence != uint64(count) || m.Preview == "" {
180 t.Fatalf("metadata=%+v err=%v", m, err)
181 }
182 t.Logf("%d messages / %d MiB text: sampled retained heap %.2f MiB, sampled heap peak %.2f MiB", count, count*64/1024, float64(h.peak)/(1<<20), float64(h.allocatedPeak)/(1<<20))
183 runtime.GC()
184 runtime.ReadMemStats(&stats)
185 if stats.HeapAlloc > h.baseline+8<<20 {
186 t.Fatalf("completed rebuild retained %.2f MiB", float64(stats.HeapAlloc-h.baseline)/(1<<20))
187 }
188 runtime.KeepAlive(m)
189 }
190 }
191
191 lines GO