]> git.giacomolli.uk - onechat/blobdiff - memory_test.go
Keep the connection of a new WhatsApp link
[onechat] / memory_test.go
index 8c34149cb07b18997df2d1a950e93e6c41f0fb34..4dee8b2951e18d2cb6d47ca60a0414e02054545d 100644 (file)
@@ -2,17 +2,14 @@ package main
 
 import (
        "context"
 
 import (
        "context"
-       "errors"
        "fmt"
        "maps"
        "os"
        "path/filepath"
        "slices"
        "strings"
        "fmt"
        "maps"
        "os"
        "path/filepath"
        "slices"
        "strings"
-       "sync"
        "testing"
        "time"
        "testing"
        "time"
-       "unicode/utf8"
 
        "github.com/stretchr/testify/require"
 )
 
        "github.com/stretchr/testify/require"
 )
@@ -54,6 +51,33 @@ func TestMemoryRestartLoadsTheSameView(t *testing.T) {
        require.Equal(t, saved, loaded)
 }
 
        require.Equal(t, saved, loaded)
 }
 
+func TestMemoryOpensAfterACrashBeforeTheViewSave(t *testing.T) {
+       root := t.TempDir()
+       clock := newTestClock()
+       memory, err := openMemory(t.Context(), root, clock.Now, newFakeSummarizer())
+       require.NoError(t, err)
+
+       _, err = memory.Append("user", "first")
+       require.NoError(t, err)
+
+       saved, err := os.ReadFile(filepath.Join(root, "view.json"))
+       require.NoError(t, err)
+
+       _, err = memory.Append("user", "second")
+       require.NoError(t, err)
+
+       view := memory.View()
+       require.NoError(t, memory.Close())
+
+       // The log and the tree hold message 1, but the view does not.
+       require.NoError(t, os.WriteFile(filepath.Join(root, "view.json"), saved, 0o600))
+
+       memory, err = openMemory(t.Context(), root, clock.Now, newFakeSummarizer())
+       require.NoError(t, err)
+       require.Equal(t, view, memory.View())
+       require.NoError(t, memory.Close())
+}
+
 func TestMemoryKeepsTheCompactionViewCoarser(t *testing.T) {
        memory, err := openMemory(t.Context(), t.TempDir(), newTestClock().Now, newFakeSummarizer())
        require.NoError(t, err)
 func TestMemoryKeepsTheCompactionViewCoarser(t *testing.T) {
        memory, err := openMemory(t.Context(), t.TempDir(), newTestClock().Now, newFakeSummarizer())
        require.NoError(t, err)
@@ -94,33 +118,102 @@ func TestMemoryRunsAtMostEightBuilds(t *testing.T) {
        }()
 
        for n := range 12 {
        }()
 
        for n := range 12 {
-               _, err = memory.Append("echo", longText(n))
+               _, err = memory.Append("echo", memoryLongText(n))
                require.NoError(t, err)
        }
 
        memory.mutex.Lock()
        running := slices.Collect(maps.Keys(memory.queue.running))
        memory.mutex.Unlock()
                require.NoError(t, err)
        }
 
        memory.mutex.Lock()
        running := slices.Collect(maps.Keys(memory.queue.running))
        memory.mutex.Unlock()
-       slices.SortFunc(running, func(a, b nodeKey) int {
+       slices.SortFunc(running, func(a, b NodeKey) int {
                return a.index - b.index
        })
                return a.index - b.index
        })
-       require.Equal(t, []nodeKey{{0, 0}, {0, 1}, {0, 2}, {0, 3}, {0, 4}, {0, 5}, {0, 6}, {0, 7}}, running)
+       require.Equal(t, []NodeKey{{0, 0}, {0, 1}, {0, 2}, {0, 3}, {0, 4}, {0, 5}, {0, 6}, {0, 7}}, running)
 
        require.Eventually(t, func() bool {
 
        require.Eventually(t, func() bool {
-               return summarizer.Active() == maxBuilds
+               return summarizer.Active() == buildMax
        }, 10*time.Second, time.Millisecond)
 
        close(summarizer.gate)
        require.NoError(t, memory.Wait(t.Context()))
        }, 10*time.Second, time.Millisecond)
 
        close(summarizer.gate)
        require.NoError(t, memory.Wait(t.Context()))
-       require.Equal(t, maxBuilds, summarizer.peak)
+       require.Equal(t, buildMax, summarizer.peak)
 
        for level, count := range []int{12, 6, 3, 1} {
                for index := range count {
 
        for level, count := range []int{12, 6, 3, 1} {
                for index := range count {
-                       require.True(t, isBuilt(t, memory.tree, level, index), "node (%d, %d)", level, index)
+                       require.True(t, treeIsBuilt(t, memory.tree, level, index), "node (%d, %d)", level, index)
                }
        }
 }
 
                }
        }
 }
 
+func TestMemorySettleWaitsForThePendingLines(t *testing.T) {
+       summarizer := newFakeSummarizer()
+       summarizer.gate = make(chan struct{})
+       memory, err := openMemory(t.Context(), t.TempDir(), newTestClock().Now, summarizer)
+       require.NoError(t, err)
+
+       defer func() {
+               require.NoError(t, memory.Close())
+       }()
+
+       _, err = memory.Append("echo", memoryLongText(0))
+       require.NoError(t, err)
+       require.Contains(t, memory.View(), viewPending)
+
+       settled := make(chan error, 1)
+       go func() {
+               settled <- memory.Settle(t.Context())
+       }()
+
+       select {
+       case err = <-settled:
+               require.Fail(t, "Settle returned before the build", "error: %v", err)
+       case <-time.After(50 * time.Millisecond):
+       }
+
+       close(summarizer.gate)
+       require.NoError(t, <-settled)
+       require.NotContains(t, memory.View(), viewPending)
+}
+
+func TestMemorySettleFailsWhileABuildFailed(t *testing.T) {
+       summarizer := newFakeSummarizer()
+       summarizer.SetFailing(true)
+       memory, err := openMemory(t.Context(), t.TempDir(), newTestClock().Now, summarizer)
+       require.NoError(t, err)
+
+       defer func() {
+               require.NoError(t, memory.Close())
+       }()
+
+       _, err = memory.Append("echo", memoryLongText(0))
+       require.NoError(t, err)
+       require.ErrorIs(t, memory.Settle(t.Context()), errMemoryUnsettled)
+
+       summarizer.SetFailing(false)
+       _, err = memory.Append("user", "retry")
+       require.NoError(t, err)
+       require.NoError(t, memory.Settle(t.Context()))
+}
+
+func TestMemorySettleEndsWithItsContext(t *testing.T) {
+       summarizer := newFakeSummarizer()
+       summarizer.gate = make(chan struct{})
+       memory, err := openMemory(t.Context(), t.TempDir(), newTestClock().Now, summarizer)
+       require.NoError(t, err)
+
+       defer func() {
+               require.NoError(t, memory.Close())
+       }()
+
+       _, err = memory.Append("echo", memoryLongText(0))
+       require.NoError(t, err)
+
+       ctx, cancel := context.WithTimeout(t.Context(), 50*time.Millisecond)
+       defer cancel()
+
+       require.ErrorIs(t, memory.Settle(ctx), context.DeadlineExceeded)
+}
+
 func TestMemoryStartsALeafAfterFewerThanEightUnbuilt(t *testing.T) {
        summarizer := newFakeSummarizer()
        summarizer.SetFailing(true)
 func TestMemoryStartsALeafAfterFewerThanEightUnbuilt(t *testing.T) {
        summarizer := newFakeSummarizer()
        summarizer.SetFailing(true)
@@ -132,7 +225,7 @@ func TestMemoryStartsALeafAfterFewerThanEightUnbuilt(t *testing.T) {
        }()
 
        for n := range 9 {
        }()
 
        for n := range 9 {
-               _, err = memory.Append("echo", longText(n))
+               _, err = memory.Append("echo", memoryLongText(n))
                require.NoError(t, err)
        }
 
                require.NoError(t, err)
        }
 
@@ -141,14 +234,14 @@ func TestMemoryStartsALeafAfterFewerThanEightUnbuilt(t *testing.T) {
                require.Less(t, task.Index, 8)
        }
 
                require.Less(t, task.Index, 8)
        }
 
-       require.False(t, isBuilt(t, memory.tree, 0, 8))
+       require.False(t, treeIsBuilt(t, memory.tree, 0, 8))
        summarizer.SetFailing(false)
 
        _, err = memory.Append("user", "retry")
        require.NoError(t, err)
        require.NoError(t, memory.Wait(t.Context()))
        summarizer.SetFailing(false)
 
        _, err = memory.Append("user", "retry")
        require.NoError(t, err)
        require.NoError(t, memory.Wait(t.Context()))
-       require.True(t, isBuilt(t, memory.tree, 0, 8))
-       require.True(t, isBuilt(t, memory.tree, 3, 0))
+       require.True(t, treeIsBuilt(t, memory.tree, 0, 8))
+       require.True(t, treeIsBuilt(t, memory.tree, 3, 0))
 }
 
 func TestMemoryMergesAfterBothHalves(t *testing.T) {
 }
 
 func TestMemoryMergesAfterBothHalves(t *testing.T) {
@@ -160,21 +253,21 @@ func TestMemoryMergesAfterBothHalves(t *testing.T) {
                require.NoError(t, memory.Close())
        }()
 
                require.NoError(t, memory.Close())
        }()
 
-       _, err = memory.Append("echo", longText(0))
+       _, err = memory.Append("echo", memoryLongText(0))
        require.NoError(t, err)
        require.NoError(t, memory.Wait(t.Context()))
 
        require.NoError(t, err)
        require.NoError(t, memory.Wait(t.Context()))
 
-       _, err = memory.Append("echo", longText(1))
+       _, err = memory.Append("echo", memoryLongText(1))
        require.NoError(t, err)
        require.NoError(t, memory.Wait(t.Context()))
        require.Len(t, summarizer.Tasks(), 3)
 
        require.NoError(t, err)
        require.NoError(t, memory.Wait(t.Context()))
        require.Len(t, summarizer.Tasks(), 3)
 
-       left := readNode(t, memory.tree, 0, 0)
-       right := readNode(t, memory.tree, 0, 1)
+       left := treeReadNode(t, memory.tree, 0, 0)
+       right := treeReadNode(t, memory.tree, 0, 1)
        merge := summarizer.Tasks()[2]
        require.Equal(t, []string{left.Text, right.Text}, merge.Input)
        require.Equal(t, "<chat>\n0+1|"+left.Text+"\n1+1|"+right.Text+"\n</chat>", merge.Context)
        merge := summarizer.Tasks()[2]
        require.Equal(t, []string{left.Text, right.Text}, merge.Input)
        require.Equal(t, "<chat>\n0+1|"+left.Text+"\n1+1|"+right.Text+"\n</chat>", merge.Context)
-       require.True(t, isBuilt(t, memory.tree, 1, 0))
+       require.True(t, treeIsBuilt(t, memory.tree, 1, 0))
 }
 
 func TestMemoryRestartRestoresTheQueue(t *testing.T) {
 }
 
 func TestMemoryRestartRestoresTheQueue(t *testing.T) {
@@ -186,7 +279,7 @@ func TestMemoryRestartRestoresTheQueue(t *testing.T) {
        require.NoError(t, err)
 
        for n := range 4 {
        require.NoError(t, err)
 
        for n := range 4 {
-               _, err = memory.Append("echo", longText(n))
+               _, err = memory.Append("echo", memoryLongText(n))
                require.NoError(t, err)
        }
 
                require.NoError(t, err)
        }
 
@@ -201,7 +294,7 @@ func TestMemoryRestartRestoresTheQueue(t *testing.T) {
        }()
 
        require.NoError(t, memory.Wait(t.Context()))
        }()
 
        require.NoError(t, memory.Wait(t.Context()))
-       require.True(t, isBuilt(t, memory.tree, 2, 0))
+       require.True(t, treeIsBuilt(t, memory.tree, 2, 0))
 }
 
 func TestMemoryBuildsFreeParents(t *testing.T) {
 }
 
 func TestMemoryBuildsFreeParents(t *testing.T) {
@@ -217,7 +310,7 @@ func TestMemoryBuildsFreeParents(t *testing.T) {
                require.NoError(t, err)
        }
 
                require.NoError(t, err)
        }
 
-       node := readNode(t, memory.tree, 2, 0)
+       node := treeReadNode(t, memory.tree, 2, 0)
        require.Equal(t, "user: a\nuser: b\nuser: c\nuser: d", node.Text)
 }
 
        require.Equal(t, "user: a\nuser: b\nuser: c\nuser: d", node.Text)
 }
 
@@ -231,10 +324,10 @@ func TestMemoryKeepsALongMessageOutOfTheView(t *testing.T) {
                require.NoError(t, memory.Close())
        }()
 
                require.NoError(t, memory.Close())
        }()
 
-       _, err = memory.Append("echo", longText(0))
+       _, err = memory.Append("echo", memoryLongText(0))
        require.NoError(t, err)
        require.NoError(t, memory.Wait(t.Context()))
        require.NoError(t, err)
        require.NoError(t, memory.Wait(t.Context()))
-       require.Equal(t, "<chat>\n0+1|"+pending+"\n</chat>", memory.View())
+       require.Equal(t, "<chat>\n0+1|"+viewPending+"\n</chat>", memory.View())
 
        summarizer.SetFailing(false)
 
 
        summarizer.SetFailing(false)
 
@@ -242,7 +335,7 @@ func TestMemoryKeepsALongMessageOutOfTheView(t *testing.T) {
        require.NoError(t, err)
        require.NoError(t, memory.Wait(t.Context()))
 
        require.NoError(t, err)
        require.NoError(t, memory.Wait(t.Context()))
 
-       task, err := memory.task(nodeKey{0, 0})
+       task, err := memory.task(NodeKey{0, 0})
        require.NoError(t, err)
        require.Equal(t, "<chat>\n0+1|"+fakeLine(task)+"\n1+1|user: again\n</chat>", memory.View())
 }
        require.NoError(t, err)
        require.Equal(t, "<chat>\n0+1|"+fakeLine(task)+"\n1+1|user: again\n</chat>", memory.View())
 }
@@ -282,93 +375,10 @@ func TestMemoryRefusesToRebuildAMissingView(t *testing.T) {
        require.NoError(t, os.Remove(filepath.Join(root, "view.json")))
 
        _, err = openMemory(t.Context(), root, clock.Now, newFakeSummarizer())
        require.NoError(t, os.Remove(filepath.Join(root, "view.json")))
 
        _, err = openMemory(t.Context(), root, clock.Now, newFakeSummarizer())
-       require.ErrorIs(t, err, errNoView)
-}
-
-// fakeSummarizer builds each node with a line of fakeLine. It fails each task
-// while failing is true, and each task waits for gate when gate is not nil.
-type fakeSummarizer struct {
-       mutex   sync.Mutex
-       gate    chan struct{}
-       failing bool
-       active  int
-       peak    int
-       tasks   []Task
-}
-
-func newFakeSummarizer() *fakeSummarizer {
-       return &fakeSummarizer{}
+       require.ErrorIs(t, err, errMemoryNoView)
 }
 
 }
 
-func (f *fakeSummarizer) Summarize(ctx context.Context, task Task) (string, error) {
-       f.mutex.Lock()
-       f.tasks = append(f.tasks, task)
-       f.active++
-       f.peak = max(f.peak, f.active)
-       fail := f.failing
-       f.mutex.Unlock()
-
-       defer func() {
-               f.mutex.Lock()
-               f.active--
-               f.mutex.Unlock()
-       }()
-
-       if f.gate != nil {
-               select {
-               case <-f.gate:
-               case <-ctx.Done():
-                       return "", ctx.Err()
-               }
-       }
-
-       if fail {
-               return "", errors.New("fake failure")
-       }
-
-       return fakeLine(task), nil
-}
-
-// fakeLine returns a line of nodeBytes bytes or less that names the node and
-// starts with the input. A real line of this size makes each merge a model
-// call.
-func fakeLine(task Task) string {
-       count := 1 << task.Level
-       line := fmt.Sprintf("summary %d+%d: %s", task.Index*count, count, strings.Join(task.Input, " "))
-       line = strings.ReplaceAll(line, "\n", " ")
-       for len(line) > nodeBytes-12 {
-               _, size := utf8.DecodeLastRuneInString(line)
-               line = line[:len(line)-size]
-       }
-
-       return line
-}
-
-// longText returns a text that is too long for its own node.
-func longText(n int) string {
+// memoryLongText returns a text that is too long for its own node.
+func memoryLongText(n int) string {
        return fmt.Sprintf("long %d ", n) + strings.Repeat("x", nodeBytes)
 }
        return fmt.Sprintf("long %d ", n) + strings.Repeat("x", nodeBytes)
 }
-
-// SetFailing makes the next tasks fail or not.
-func (f *fakeSummarizer) SetFailing(failing bool) {
-       f.mutex.Lock()
-       defer f.mutex.Unlock()
-
-       f.failing = failing
-}
-
-// Active returns the number of tasks that run now.
-func (f *fakeSummarizer) Active() int {
-       f.mutex.Lock()
-       defer f.mutex.Unlock()
-
-       return f.active
-}
-
-// Tasks returns the tasks so far.
-func (f *fakeSummarizer) Tasks() []Task {
-       f.mutex.Lock()
-       defer f.mutex.Unlock()
-
-       return slices.Clone(f.tasks)
-}