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