16 "github.com/stretchr/testify/require"
19 func TestMemoryRestartLoadsTheSameView(t *testing.T) {
21 clock := newTestClock()
22 memory, err := openMemory(t.Context(), root, clock.Now, newFakeSummarizer())
23 require.NoError(t, err)
25 memory.chat.high = 200
27 memory.cview.high = 80
30 _, err = memory.Append("user", fmt.Sprint(n))
31 require.NoError(t, err)
35 context := memory.Context(0, 100)
36 saved, err := os.ReadFile(filepath.Join(root, "view.json"))
37 require.NoError(t, err)
38 require.Less(t, len(memory.chat.lines), 100)
39 require.NoError(t, memory.Close())
41 memory, err = openMemory(t.Context(), root, clock.Now, newFakeSummarizer())
42 require.NoError(t, err)
45 require.NoError(t, memory.Close())
48 require.Equal(t, view, memory.View())
49 require.Equal(t, context, memory.Context(0, 100))
51 loaded, err := os.ReadFile(filepath.Join(root, "view.json"))
52 require.NoError(t, err)
53 require.Equal(t, saved, loaded)
56 func TestMemoryKeepsTheCompactionViewCoarser(t *testing.T) {
57 memory, err := openMemory(t.Context(), t.TempDir(), newTestClock().Now, newFakeSummarizer())
58 require.NoError(t, err)
61 require.NoError(t, memory.Close())
64 memory.chat.high = 200
66 memory.cview.high = 80
69 _, err = memory.Append("user", fmt.Sprint(n%10))
70 require.NoError(t, err)
73 for _, line := range memory.chat.lines {
74 outer := memory.cview.lines[coarse]
75 for nodeEnd(outer.level, outer.index) <= line.index<<line.level {
77 outer = memory.cview.lines[coarse]
80 require.GreaterOrEqual(t, outer.level, line.level, "message %d", n)
85 func TestMemoryRunsAtMostEightBuilds(t *testing.T) {
86 summarizer := newFakeSummarizer()
87 summarizer.gate = make(chan struct{})
88 memory, err := openMemory(t.Context(), t.TempDir(), newTestClock().Now, summarizer)
89 require.NoError(t, err)
92 require.NoError(t, memory.Close())
96 _, err = memory.Append("echo", longText(n))
97 require.NoError(t, err)
101 running := slices.Collect(maps.Keys(memory.queue.running))
102 memory.mutex.Unlock()
103 slices.SortFunc(running, func(a, b nodeKey) int {
104 return a.index - b.index
106 require.Equal(t, []nodeKey{{0, 0}, {0, 1}, {0, 2}, {0, 3}, {0, 4}, {0, 5}, {0, 6}, {0, 7}}, running)
108 close(summarizer.gate)
109 require.NoError(t, memory.Wait(t.Context()))
110 require.Equal(t, maxBuilds, summarizer.peak)
112 for level, count := range []int{12, 6, 3, 1} {
113 for index := range count {
114 require.True(t, memory.tree.Built(level, index), "node (%d, %d)", level, index)
119 func TestMemoryStartsALeafAfterFewerThanEightUnbuilt(t *testing.T) {
120 summarizer := newFakeSummarizer()
121 summarizer.SetFailing(true)
122 memory, err := openMemory(t.Context(), t.TempDir(), newTestClock().Now, summarizer)
123 require.NoError(t, err)
126 require.NoError(t, memory.Close())
130 _, err = memory.Append("echo", longText(n))
131 require.NoError(t, err)
134 require.NoError(t, memory.Wait(t.Context()))
135 for _, task := range summarizer.Tasks() {
136 require.Less(t, task.Index, 8)
139 require.False(t, memory.tree.Built(0, 8))
140 summarizer.SetFailing(false)
142 _, err = memory.Append("user", "retry")
143 require.NoError(t, err)
144 require.NoError(t, memory.Wait(t.Context()))
145 require.True(t, memory.tree.Built(0, 8))
146 require.True(t, memory.tree.Built(3, 0))
149 func TestMemoryMergesAfterBothHalves(t *testing.T) {
150 summarizer := newFakeSummarizer()
151 memory, err := openMemory(t.Context(), t.TempDir(), newTestClock().Now, summarizer)
152 require.NoError(t, err)
155 require.NoError(t, memory.Close())
158 _, err = memory.Append("echo", longText(0))
159 require.NoError(t, err)
160 require.NoError(t, memory.Wait(t.Context()))
162 _, err = memory.Append("echo", longText(1))
163 require.NoError(t, err)
164 require.NoError(t, memory.Wait(t.Context()))
165 require.Len(t, summarizer.Tasks(), 3)
167 left, _ := memory.tree.Node(0, 0)
168 right, _ := memory.tree.Node(0, 1)
169 merge := summarizer.Tasks()[2]
170 require.Equal(t, []string{left.Text, right.Text}, merge.Input)
171 require.Equal(t, "<chat>\n0+1|"+left.Text+"\n1+1|"+right.Text+"\n</chat>", merge.Context)
172 require.True(t, memory.tree.Built(1, 0))
175 func TestMemoryRestartRestoresTheQueue(t *testing.T) {
177 clock := newTestClock()
178 summarizer := newFakeSummarizer()
179 summarizer.SetFailing(true)
180 memory, err := openMemory(t.Context(), root, clock.Now, summarizer)
181 require.NoError(t, err)
184 _, err = memory.Append("echo", longText(n))
185 require.NoError(t, err)
188 require.NoError(t, memory.Wait(t.Context()))
189 require.NoError(t, memory.Close())
191 memory, err = openMemory(t.Context(), root, clock.Now, newFakeSummarizer())
192 require.NoError(t, err)
195 require.NoError(t, memory.Close())
198 require.NoError(t, memory.Wait(t.Context()))
199 require.True(t, memory.tree.Built(2, 0))
202 func TestMemoryBuildsFreeParents(t *testing.T) {
203 memory, err := openMemory(t.Context(), t.TempDir(), newTestClock().Now, newFakeSummarizer())
204 require.NoError(t, err)
207 require.NoError(t, memory.Close())
210 for _, text := range []string{"a", "b", "c", "d"} {
211 _, err = memory.Append("user", text)
212 require.NoError(t, err)
215 node, built := memory.tree.Node(2, 0)
216 require.True(t, built)
217 require.Equal(t, "user: a\nuser: b\nuser: c\nuser: d", node.Text)
220 func TestMemoryKeepsALongMessageOutOfTheView(t *testing.T) {
221 summarizer := newFakeSummarizer()
222 summarizer.SetFailing(true)
223 memory, err := openMemory(t.Context(), t.TempDir(), newTestClock().Now, summarizer)
224 require.NoError(t, err)
227 require.NoError(t, memory.Close())
230 _, err = memory.Append("echo", longText(0))
231 require.NoError(t, err)
232 require.NoError(t, memory.Wait(t.Context()))
233 require.Equal(t, "<chat>\n0+1|"+pending+"\n</chat>", memory.View())
235 summarizer.SetFailing(false)
237 _, err = memory.Append("user", "again")
238 require.NoError(t, err)
239 require.NoError(t, memory.Wait(t.Context()))
240 require.Equal(t, "<chat>\n0+1|"+fakeLine(memory.task(nodeKey{0, 0}))+"\n1+1|user: again\n</chat>", memory.View())
243 func TestMemoryCoversMessagesAfterTheSavedView(t *testing.T) {
245 clock := newTestClock()
246 memory, err := openMemory(t.Context(), root, clock.Now, newFakeSummarizer())
247 require.NoError(t, err)
249 _, err = memory.Append("user", "a")
250 require.NoError(t, err)
252 _, err = memory.log.Append("reply", "b")
253 require.NoError(t, err)
254 require.NoError(t, memory.Close())
256 memory, err = openMemory(t.Context(), root, clock.Now, newFakeSummarizer())
257 require.NoError(t, err)
260 require.NoError(t, memory.Close())
263 require.Equal(t, "<chat>\n0+1|user: a\n1+1|reply: b\n</chat>", memory.View())
266 func TestMemoryRefusesToRebuildAMissingView(t *testing.T) {
268 clock := newTestClock()
269 memory, err := openMemory(t.Context(), root, clock.Now, newFakeSummarizer())
270 require.NoError(t, err)
272 _, err = memory.Append("user", "a")
273 require.NoError(t, err)
274 require.NoError(t, memory.Close())
275 require.NoError(t, os.Remove(filepath.Join(root, "view.json")))
277 _, err = openMemory(t.Context(), root, clock.Now, newFakeSummarizer())
278 require.ErrorIs(t, err, errNoView)
281 // fakeSummarizer builds each node with a line of fakeLine. It fails each task
282 // while failing is true, and each task waits for gate when gate is not nil.
283 type fakeSummarizer struct {
292 func newFakeSummarizer() *fakeSummarizer {
293 return &fakeSummarizer{}
296 func (f *fakeSummarizer) Summarize(ctx context.Context, task Task) (string, error) {
298 f.tasks = append(f.tasks, task)
300 f.peak = max(f.peak, f.active)
319 return "", errors.New("fake failure")
322 return fakeLine(task), nil
325 // fakeLine returns a line of nodeBytes bytes or less that names the node and
326 // starts with the input. A real line of this size makes each merge a model
328 func fakeLine(task Task) string {
329 count := 1 << task.Level
330 line := fmt.Sprintf("summary %d+%d: %s", task.Index*count, count, strings.Join(task.Input, " "))
331 line = strings.ReplaceAll(line, "\n", " ")
332 for len(line) > nodeBytes-12 {
333 _, size := utf8.DecodeLastRuneInString(line)
334 line = line[:len(line)-size]
340 // longText returns a text that is too long for its own node.
341 func longText(n int) string {
342 return fmt.Sprintf("long %d ", n) + strings.Repeat("x", nodeBytes)
345 // SetFailing makes the next tasks fail or not.
346 func (f *fakeSummarizer) SetFailing(failing bool) {
348 defer f.mutex.Unlock()
353 // Tasks returns the tasks so far.
354 func (f *fakeSummarizer) Tasks() []Task {
356 defer f.mutex.Unlock()
358 return slices.Clone(f.tasks)