17 "github.com/stretchr/testify/require"
20 func TestMemoryRestartLoadsTheSameView(t *testing.T) {
22 clock := newTestClock()
23 memory, err := openMemory(t.Context(), root, clock.Now, newFakeSummarizer())
24 require.NoError(t, err)
26 memory.chat.high = 200
28 memory.cview.high = 80
31 _, err = memory.Append("user", fmt.Sprint(n))
32 require.NoError(t, err)
36 context := memory.Context(0, 100)
37 saved, err := os.ReadFile(filepath.Join(root, "view.json"))
38 require.NoError(t, err)
39 require.Less(t, len(memory.chat.lines), 100)
40 require.NoError(t, memory.Close())
42 memory, err = openMemory(t.Context(), root, clock.Now, newFakeSummarizer())
43 require.NoError(t, err)
46 require.NoError(t, memory.Close())
49 require.Equal(t, view, memory.View())
50 require.Equal(t, context, memory.Context(0, 100))
52 loaded, err := os.ReadFile(filepath.Join(root, "view.json"))
53 require.NoError(t, err)
54 require.Equal(t, saved, loaded)
57 func TestMemoryKeepsTheCompactionViewCoarser(t *testing.T) {
58 memory, err := openMemory(t.Context(), t.TempDir(), newTestClock().Now, newFakeSummarizer())
59 require.NoError(t, err)
62 require.NoError(t, memory.Close())
65 memory.chat.high = 200
67 memory.cview.high = 80
70 _, err = memory.Append("user", fmt.Sprint(n%10))
71 require.NoError(t, err)
74 for _, line := range memory.chat.lines {
75 outer := memory.cview.lines[coarse]
76 for nodeEnd(outer.level, outer.index) <= line.index<<line.level {
78 outer = memory.cview.lines[coarse]
81 require.GreaterOrEqual(t, outer.level, line.level, "message %d", n)
86 func TestMemoryRunsAtMostEightBuilds(t *testing.T) {
87 summarizer := newFakeSummarizer()
88 summarizer.gate = make(chan struct{})
89 memory, err := openMemory(t.Context(), t.TempDir(), newTestClock().Now, summarizer)
90 require.NoError(t, err)
93 require.NoError(t, memory.Close())
97 _, err = memory.Append("echo", longText(n))
98 require.NoError(t, err)
102 running := slices.Collect(maps.Keys(memory.queue.running))
103 memory.mutex.Unlock()
104 slices.SortFunc(running, func(a, b nodeKey) int {
105 return a.index - b.index
107 require.Equal(t, []nodeKey{{0, 0}, {0, 1}, {0, 2}, {0, 3}, {0, 4}, {0, 5}, {0, 6}, {0, 7}}, running)
109 require.Eventually(t, func() bool {
110 return summarizer.Active() == maxBuilds
111 }, 10*time.Second, time.Millisecond)
113 close(summarizer.gate)
114 require.NoError(t, memory.Wait(t.Context()))
115 require.Equal(t, maxBuilds, summarizer.peak)
117 for level, count := range []int{12, 6, 3, 1} {
118 for index := range count {
119 require.True(t, memory.tree.Built(level, index), "node (%d, %d)", level, index)
124 func TestMemoryStartsALeafAfterFewerThanEightUnbuilt(t *testing.T) {
125 summarizer := newFakeSummarizer()
126 summarizer.SetFailing(true)
127 memory, err := openMemory(t.Context(), t.TempDir(), newTestClock().Now, summarizer)
128 require.NoError(t, err)
131 require.NoError(t, memory.Close())
135 _, err = memory.Append("echo", longText(n))
136 require.NoError(t, err)
139 require.NoError(t, memory.Wait(t.Context()))
140 for _, task := range summarizer.Tasks() {
141 require.Less(t, task.Index, 8)
144 require.False(t, memory.tree.Built(0, 8))
145 summarizer.SetFailing(false)
147 _, err = memory.Append("user", "retry")
148 require.NoError(t, err)
149 require.NoError(t, memory.Wait(t.Context()))
150 require.True(t, memory.tree.Built(0, 8))
151 require.True(t, memory.tree.Built(3, 0))
154 func TestMemoryMergesAfterBothHalves(t *testing.T) {
155 summarizer := newFakeSummarizer()
156 memory, err := openMemory(t.Context(), t.TempDir(), newTestClock().Now, summarizer)
157 require.NoError(t, err)
160 require.NoError(t, memory.Close())
163 _, err = memory.Append("echo", longText(0))
164 require.NoError(t, err)
165 require.NoError(t, memory.Wait(t.Context()))
167 _, err = memory.Append("echo", longText(1))
168 require.NoError(t, err)
169 require.NoError(t, memory.Wait(t.Context()))
170 require.Len(t, summarizer.Tasks(), 3)
172 left, _ := memory.tree.Node(0, 0)
173 right, _ := memory.tree.Node(0, 1)
174 merge := summarizer.Tasks()[2]
175 require.Equal(t, []string{left.Text, right.Text}, merge.Input)
176 require.Equal(t, "<chat>\n0+1|"+left.Text+"\n1+1|"+right.Text+"\n</chat>", merge.Context)
177 require.True(t, memory.tree.Built(1, 0))
180 func TestMemoryRestartRestoresTheQueue(t *testing.T) {
182 clock := newTestClock()
183 summarizer := newFakeSummarizer()
184 summarizer.SetFailing(true)
185 memory, err := openMemory(t.Context(), root, clock.Now, summarizer)
186 require.NoError(t, err)
189 _, err = memory.Append("echo", longText(n))
190 require.NoError(t, err)
193 require.NoError(t, memory.Wait(t.Context()))
194 require.NoError(t, memory.Close())
196 memory, err = openMemory(t.Context(), root, clock.Now, newFakeSummarizer())
197 require.NoError(t, err)
200 require.NoError(t, memory.Close())
203 require.NoError(t, memory.Wait(t.Context()))
204 require.True(t, memory.tree.Built(2, 0))
207 func TestMemoryBuildsFreeParents(t *testing.T) {
208 memory, err := openMemory(t.Context(), t.TempDir(), newTestClock().Now, newFakeSummarizer())
209 require.NoError(t, err)
212 require.NoError(t, memory.Close())
215 for _, text := range []string{"a", "b", "c", "d"} {
216 _, err = memory.Append("user", text)
217 require.NoError(t, err)
220 node, built := memory.tree.Node(2, 0)
221 require.True(t, built)
222 require.Equal(t, "user: a\nuser: b\nuser: c\nuser: d", node.Text)
225 func TestMemoryKeepsALongMessageOutOfTheView(t *testing.T) {
226 summarizer := newFakeSummarizer()
227 summarizer.SetFailing(true)
228 memory, err := openMemory(t.Context(), t.TempDir(), newTestClock().Now, summarizer)
229 require.NoError(t, err)
232 require.NoError(t, memory.Close())
235 _, err = memory.Append("echo", longText(0))
236 require.NoError(t, err)
237 require.NoError(t, memory.Wait(t.Context()))
238 require.Equal(t, "<chat>\n0+1|"+pending+"\n</chat>", memory.View())
240 summarizer.SetFailing(false)
242 _, err = memory.Append("user", "again")
243 require.NoError(t, err)
244 require.NoError(t, memory.Wait(t.Context()))
245 require.Equal(t, "<chat>\n0+1|"+fakeLine(memory.task(nodeKey{0, 0}))+"\n1+1|user: again\n</chat>", memory.View())
248 func TestMemoryCoversMessagesAfterTheSavedView(t *testing.T) {
250 clock := newTestClock()
251 memory, err := openMemory(t.Context(), root, clock.Now, newFakeSummarizer())
252 require.NoError(t, err)
254 _, err = memory.Append("user", "a")
255 require.NoError(t, err)
257 _, err = memory.log.Append("reply", "b")
258 require.NoError(t, err)
259 require.NoError(t, memory.Close())
261 memory, err = openMemory(t.Context(), root, clock.Now, newFakeSummarizer())
262 require.NoError(t, err)
265 require.NoError(t, memory.Close())
268 require.Equal(t, "<chat>\n0+1|user: a\n1+1|reply: b\n</chat>", memory.View())
271 func TestMemoryRefusesToRebuildAMissingView(t *testing.T) {
273 clock := newTestClock()
274 memory, err := openMemory(t.Context(), root, clock.Now, newFakeSummarizer())
275 require.NoError(t, err)
277 _, err = memory.Append("user", "a")
278 require.NoError(t, err)
279 require.NoError(t, memory.Close())
280 require.NoError(t, os.Remove(filepath.Join(root, "view.json")))
282 _, err = openMemory(t.Context(), root, clock.Now, newFakeSummarizer())
283 require.ErrorIs(t, err, errNoView)
286 // fakeSummarizer builds each node with a line of fakeLine. It fails each task
287 // while failing is true, and each task waits for gate when gate is not nil.
288 type fakeSummarizer struct {
297 func newFakeSummarizer() *fakeSummarizer {
298 return &fakeSummarizer{}
301 func (f *fakeSummarizer) Summarize(ctx context.Context, task Task) (string, error) {
303 f.tasks = append(f.tasks, task)
305 f.peak = max(f.peak, f.active)
324 return "", errors.New("fake failure")
327 return fakeLine(task), nil
330 // fakeLine returns a line of nodeBytes bytes or less that names the node and
331 // starts with the input. A real line of this size makes each merge a model
333 func fakeLine(task Task) string {
334 count := 1 << task.Level
335 line := fmt.Sprintf("summary %d+%d: %s", task.Index*count, count, strings.Join(task.Input, " "))
336 line = strings.ReplaceAll(line, "\n", " ")
337 for len(line) > nodeBytes-12 {
338 _, size := utf8.DecodeLastRuneInString(line)
339 line = line[:len(line)-size]
345 // longText returns a text that is too long for its own node.
346 func longText(n int) string {
347 return fmt.Sprintf("long %d ", n) + strings.Repeat("x", nodeBytes)
350 // SetFailing makes the next tasks fail or not.
351 func (f *fakeSummarizer) SetFailing(failing bool) {
353 defer f.mutex.Unlock()
358 // Active returns the number of tasks that run now.
359 func (f *fakeSummarizer) Active() int {
361 defer f.mutex.Unlock()
366 // Tasks returns the tasks so far.
367 func (f *fakeSummarizer) Tasks() []Task {
369 defer f.mutex.Unlock()
371 return slices.Clone(f.tasks)