]> git.giacomolli.uk - onechat/blob - memory_test.go
4dee8b2951e18d2cb6d47ca60a0414e02054545d
[onechat] / memory_test.go
1 package main
2
3 import (
4 "context"
5 "fmt"
6 "maps"
7 "os"
8 "path/filepath"
9 "slices"
10 "strings"
11 "testing"
12 "time"
13
14 "github.com/stretchr/testify/require"
15 )
16
17 func TestMemoryRestartLoadsTheSameView(t *testing.T) {
18 root := t.TempDir()
19 clock := newTestClock()
20 memory, err := openMemory(t.Context(), root, clock.Now, newFakeSummarizer())
21 require.NoError(t, err)
22
23 memory.chat.high = 200
24 memory.chat.low = 100
25 memory.cview.high = 80
26 memory.cview.low = 40
27 for n := range 100 {
28 _, err = memory.Append("user", fmt.Sprint(n))
29 require.NoError(t, err)
30 }
31
32 view := memory.View()
33 context := memory.Context(0, 100)
34 saved, err := os.ReadFile(filepath.Join(root, "view.json"))
35 require.NoError(t, err)
36 require.Less(t, len(memory.chat.lines), 100)
37 require.NoError(t, memory.Close())
38
39 memory, err = openMemory(t.Context(), root, clock.Now, newFakeSummarizer())
40 require.NoError(t, err)
41
42 defer func() {
43 require.NoError(t, memory.Close())
44 }()
45
46 require.Equal(t, view, memory.View())
47 require.Equal(t, context, memory.Context(0, 100))
48
49 loaded, err := os.ReadFile(filepath.Join(root, "view.json"))
50 require.NoError(t, err)
51 require.Equal(t, saved, loaded)
52 }
53
54 func TestMemoryOpensAfterACrashBeforeTheViewSave(t *testing.T) {
55 root := t.TempDir()
56 clock := newTestClock()
57 memory, err := openMemory(t.Context(), root, clock.Now, newFakeSummarizer())
58 require.NoError(t, err)
59
60 _, err = memory.Append("user", "first")
61 require.NoError(t, err)
62
63 saved, err := os.ReadFile(filepath.Join(root, "view.json"))
64 require.NoError(t, err)
65
66 _, err = memory.Append("user", "second")
67 require.NoError(t, err)
68
69 view := memory.View()
70 require.NoError(t, memory.Close())
71
72 // The log and the tree hold message 1, but the view does not.
73 require.NoError(t, os.WriteFile(filepath.Join(root, "view.json"), saved, 0o600))
74
75 memory, err = openMemory(t.Context(), root, clock.Now, newFakeSummarizer())
76 require.NoError(t, err)
77 require.Equal(t, view, memory.View())
78 require.NoError(t, memory.Close())
79 }
80
81 func TestMemoryKeepsTheCompactionViewCoarser(t *testing.T) {
82 memory, err := openMemory(t.Context(), t.TempDir(), newTestClock().Now, newFakeSummarizer())
83 require.NoError(t, err)
84
85 defer func() {
86 require.NoError(t, memory.Close())
87 }()
88
89 memory.chat.high = 200
90 memory.chat.low = 100
91 memory.cview.high = 80
92 memory.cview.low = 40
93 for n := range 64 {
94 _, err = memory.Append("user", fmt.Sprint(n%10))
95 require.NoError(t, err)
96
97 coarse := 0
98 for _, line := range memory.chat.lines {
99 outer := memory.cview.lines[coarse]
100 for nodeEnd(outer.level, outer.index) <= line.index<<line.level {
101 coarse++
102 outer = memory.cview.lines[coarse]
103 }
104
105 require.GreaterOrEqual(t, outer.level, line.level, "message %d", n)
106 }
107 }
108 }
109
110 func TestMemoryRunsAtMostEightBuilds(t *testing.T) {
111 summarizer := newFakeSummarizer()
112 summarizer.gate = make(chan struct{})
113 memory, err := openMemory(t.Context(), t.TempDir(), newTestClock().Now, summarizer)
114 require.NoError(t, err)
115
116 defer func() {
117 require.NoError(t, memory.Close())
118 }()
119
120 for n := range 12 {
121 _, err = memory.Append("echo", memoryLongText(n))
122 require.NoError(t, err)
123 }
124
125 memory.mutex.Lock()
126 running := slices.Collect(maps.Keys(memory.queue.running))
127 memory.mutex.Unlock()
128 slices.SortFunc(running, func(a, b NodeKey) int {
129 return a.index - b.index
130 })
131 require.Equal(t, []NodeKey{{0, 0}, {0, 1}, {0, 2}, {0, 3}, {0, 4}, {0, 5}, {0, 6}, {0, 7}}, running)
132
133 require.Eventually(t, func() bool {
134 return summarizer.Active() == buildMax
135 }, 10*time.Second, time.Millisecond)
136
137 close(summarizer.gate)
138 require.NoError(t, memory.Wait(t.Context()))
139 require.Equal(t, buildMax, summarizer.peak)
140
141 for level, count := range []int{12, 6, 3, 1} {
142 for index := range count {
143 require.True(t, treeIsBuilt(t, memory.tree, level, index), "node (%d, %d)", level, index)
144 }
145 }
146 }
147
148 func TestMemorySettleWaitsForThePendingLines(t *testing.T) {
149 summarizer := newFakeSummarizer()
150 summarizer.gate = make(chan struct{})
151 memory, err := openMemory(t.Context(), t.TempDir(), newTestClock().Now, summarizer)
152 require.NoError(t, err)
153
154 defer func() {
155 require.NoError(t, memory.Close())
156 }()
157
158 _, err = memory.Append("echo", memoryLongText(0))
159 require.NoError(t, err)
160 require.Contains(t, memory.View(), viewPending)
161
162 settled := make(chan error, 1)
163 go func() {
164 settled <- memory.Settle(t.Context())
165 }()
166
167 select {
168 case err = <-settled:
169 require.Fail(t, "Settle returned before the build", "error: %v", err)
170 case <-time.After(50 * time.Millisecond):
171 }
172
173 close(summarizer.gate)
174 require.NoError(t, <-settled)
175 require.NotContains(t, memory.View(), viewPending)
176 }
177
178 func TestMemorySettleFailsWhileABuildFailed(t *testing.T) {
179 summarizer := newFakeSummarizer()
180 summarizer.SetFailing(true)
181 memory, err := openMemory(t.Context(), t.TempDir(), newTestClock().Now, summarizer)
182 require.NoError(t, err)
183
184 defer func() {
185 require.NoError(t, memory.Close())
186 }()
187
188 _, err = memory.Append("echo", memoryLongText(0))
189 require.NoError(t, err)
190 require.ErrorIs(t, memory.Settle(t.Context()), errMemoryUnsettled)
191
192 summarizer.SetFailing(false)
193 _, err = memory.Append("user", "retry")
194 require.NoError(t, err)
195 require.NoError(t, memory.Settle(t.Context()))
196 }
197
198 func TestMemorySettleEndsWithItsContext(t *testing.T) {
199 summarizer := newFakeSummarizer()
200 summarizer.gate = make(chan struct{})
201 memory, err := openMemory(t.Context(), t.TempDir(), newTestClock().Now, summarizer)
202 require.NoError(t, err)
203
204 defer func() {
205 require.NoError(t, memory.Close())
206 }()
207
208 _, err = memory.Append("echo", memoryLongText(0))
209 require.NoError(t, err)
210
211 ctx, cancel := context.WithTimeout(t.Context(), 50*time.Millisecond)
212 defer cancel()
213
214 require.ErrorIs(t, memory.Settle(ctx), context.DeadlineExceeded)
215 }
216
217 func TestMemoryStartsALeafAfterFewerThanEightUnbuilt(t *testing.T) {
218 summarizer := newFakeSummarizer()
219 summarizer.SetFailing(true)
220 memory, err := openMemory(t.Context(), t.TempDir(), newTestClock().Now, summarizer)
221 require.NoError(t, err)
222
223 defer func() {
224 require.NoError(t, memory.Close())
225 }()
226
227 for n := range 9 {
228 _, err = memory.Append("echo", memoryLongText(n))
229 require.NoError(t, err)
230 }
231
232 require.NoError(t, memory.Wait(t.Context()))
233 for _, task := range summarizer.Tasks() {
234 require.Less(t, task.Index, 8)
235 }
236
237 require.False(t, treeIsBuilt(t, memory.tree, 0, 8))
238 summarizer.SetFailing(false)
239
240 _, err = memory.Append("user", "retry")
241 require.NoError(t, err)
242 require.NoError(t, memory.Wait(t.Context()))
243 require.True(t, treeIsBuilt(t, memory.tree, 0, 8))
244 require.True(t, treeIsBuilt(t, memory.tree, 3, 0))
245 }
246
247 func TestMemoryMergesAfterBothHalves(t *testing.T) {
248 summarizer := newFakeSummarizer()
249 memory, err := openMemory(t.Context(), t.TempDir(), newTestClock().Now, summarizer)
250 require.NoError(t, err)
251
252 defer func() {
253 require.NoError(t, memory.Close())
254 }()
255
256 _, err = memory.Append("echo", memoryLongText(0))
257 require.NoError(t, err)
258 require.NoError(t, memory.Wait(t.Context()))
259
260 _, err = memory.Append("echo", memoryLongText(1))
261 require.NoError(t, err)
262 require.NoError(t, memory.Wait(t.Context()))
263 require.Len(t, summarizer.Tasks(), 3)
264
265 left := treeReadNode(t, memory.tree, 0, 0)
266 right := treeReadNode(t, memory.tree, 0, 1)
267 merge := summarizer.Tasks()[2]
268 require.Equal(t, []string{left.Text, right.Text}, merge.Input)
269 require.Equal(t, "<chat>\n0+1|"+left.Text+"\n1+1|"+right.Text+"\n</chat>", merge.Context)
270 require.True(t, treeIsBuilt(t, memory.tree, 1, 0))
271 }
272
273 func TestMemoryRestartRestoresTheQueue(t *testing.T) {
274 root := t.TempDir()
275 clock := newTestClock()
276 summarizer := newFakeSummarizer()
277 summarizer.SetFailing(true)
278 memory, err := openMemory(t.Context(), root, clock.Now, summarizer)
279 require.NoError(t, err)
280
281 for n := range 4 {
282 _, err = memory.Append("echo", memoryLongText(n))
283 require.NoError(t, err)
284 }
285
286 require.NoError(t, memory.Wait(t.Context()))
287 require.NoError(t, memory.Close())
288
289 memory, err = openMemory(t.Context(), root, clock.Now, newFakeSummarizer())
290 require.NoError(t, err)
291
292 defer func() {
293 require.NoError(t, memory.Close())
294 }()
295
296 require.NoError(t, memory.Wait(t.Context()))
297 require.True(t, treeIsBuilt(t, memory.tree, 2, 0))
298 }
299
300 func TestMemoryBuildsFreeParents(t *testing.T) {
301 memory, err := openMemory(t.Context(), t.TempDir(), newTestClock().Now, newFakeSummarizer())
302 require.NoError(t, err)
303
304 defer func() {
305 require.NoError(t, memory.Close())
306 }()
307
308 for _, text := range []string{"a", "b", "c", "d"} {
309 _, err = memory.Append("user", text)
310 require.NoError(t, err)
311 }
312
313 node := treeReadNode(t, memory.tree, 2, 0)
314 require.Equal(t, "user: a\nuser: b\nuser: c\nuser: d", node.Text)
315 }
316
317 func TestMemoryKeepsALongMessageOutOfTheView(t *testing.T) {
318 summarizer := newFakeSummarizer()
319 summarizer.SetFailing(true)
320 memory, err := openMemory(t.Context(), t.TempDir(), newTestClock().Now, summarizer)
321 require.NoError(t, err)
322
323 defer func() {
324 require.NoError(t, memory.Close())
325 }()
326
327 _, err = memory.Append("echo", memoryLongText(0))
328 require.NoError(t, err)
329 require.NoError(t, memory.Wait(t.Context()))
330 require.Equal(t, "<chat>\n0+1|"+viewPending+"\n</chat>", memory.View())
331
332 summarizer.SetFailing(false)
333
334 _, err = memory.Append("user", "again")
335 require.NoError(t, err)
336 require.NoError(t, memory.Wait(t.Context()))
337
338 task, err := memory.task(NodeKey{0, 0})
339 require.NoError(t, err)
340 require.Equal(t, "<chat>\n0+1|"+fakeLine(task)+"\n1+1|user: again\n</chat>", memory.View())
341 }
342
343 func TestMemoryCoversMessagesAfterTheSavedView(t *testing.T) {
344 root := t.TempDir()
345 clock := newTestClock()
346 memory, err := openMemory(t.Context(), root, clock.Now, newFakeSummarizer())
347 require.NoError(t, err)
348
349 _, err = memory.Append("user", "a")
350 require.NoError(t, err)
351
352 _, err = memory.log.Append("reply", "b")
353 require.NoError(t, err)
354 require.NoError(t, memory.Close())
355
356 memory, err = openMemory(t.Context(), root, clock.Now, newFakeSummarizer())
357 require.NoError(t, err)
358
359 defer func() {
360 require.NoError(t, memory.Close())
361 }()
362
363 require.Equal(t, "<chat>\n0+1|user: a\n1+1|reply: b\n</chat>", memory.View())
364 }
365
366 func TestMemoryRefusesToRebuildAMissingView(t *testing.T) {
367 root := t.TempDir()
368 clock := newTestClock()
369 memory, err := openMemory(t.Context(), root, clock.Now, newFakeSummarizer())
370 require.NoError(t, err)
371
372 _, err = memory.Append("user", "a")
373 require.NoError(t, err)
374 require.NoError(t, memory.Close())
375 require.NoError(t, os.Remove(filepath.Join(root, "view.json")))
376
377 _, err = openMemory(t.Context(), root, clock.Now, newFakeSummarizer())
378 require.ErrorIs(t, err, errMemoryNoView)
379 }
380
381 // memoryLongText returns a text that is too long for its own node.
382 func memoryLongText(n int) string {
383 return fmt.Sprintf("long %d ", n) + strings.Repeat("x", nodeBytes)
384 }