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