+func TestQueuedAppendResumesWithoutADuplicate(t *testing.T) {
+ dir := t.TempDir()
+ memory, err := openMemory(t.Context(), dir, newTestClock().Now, newFakeSummarizer(), discardLog())
+ require.NoError(t, err)
+
+ var saved *int
+
+ first, err := memory.AppendOnce("user", "Record the gate size.", nil, func(id int) error {
+ saved = &id
+
+ require.Equal(t, 0, memory.log.Len())
+
+ return nil
+ })
+ require.NoError(t, err)
+ require.NoError(t, memory.Close())
+
+ memory, err = openMemory(t.Context(), dir, newTestClock().Now, newFakeSummarizer(), discardLog())
+ require.NoError(t, err)
+
+ defer func() {
+ require.NoError(t, memory.Close())
+ }()
+
+ second, err := memory.AppendOnce("user", first.Text, saved, func(int) error {
+ require.FailNow(t, "a recorded message must not be prepared again")
+
+ return nil
+ })
+ require.NoError(t, err)
+ require.Equal(t, first, second)
+ require.Equal(t, 1, memory.Len())
+}
+
+func TestQueuedAppendRetriesAnUnwrittenPreparedMessage(t *testing.T) {
+ memory, err := openMemory(t.Context(), t.TempDir(), newTestClock().Now, newFakeSummarizer(), discardLog())
+ require.NoError(t, err)
+
+ defer func() {
+ require.NoError(t, memory.Close())
+ }()
+
+ failure := errors.New("fake queue write failure")
+
+ var saved *int
+
+ _, err = memory.AppendOnce("work", "[Garden] Check the gate.", nil, func(id int) error {
+ saved = &id
+
+ return failure
+ })
+ require.ErrorIs(t, err, failure)
+ require.Equal(t, 0, memory.Len())
+
+ message, err := memory.AppendOnce("work", "[Garden] Check the gate.", saved, func(id int) error {
+ require.Equal(t, *saved, id)
+
+ return nil
+ })
+ require.NoError(t, err)
+ require.Equal(t, 0, message.ID)
+ require.Equal(t, 1, memory.Len())
+}
+
+func TestQueuedAppendRefusesAnInvalidReceipt(t *testing.T) {
+ memory, err := openMemory(t.Context(), t.TempDir(), newTestClock().Now, newFakeSummarizer(), discardLog())
+ require.NoError(t, err)
+
+ defer func() {
+ require.NoError(t, memory.Close())
+ }()
+
+ _, err = memory.Append("user", "Keep the original.")
+ require.NoError(t, err)
+
+ testCases := []struct {
+ name string
+ id int
+ cause string
+ }{
+ {
+ name: "receipt points to different text",
+ id: 0,
+ cause: "does not match",
+ },
+ {
+ name: "receipt skips a log position",
+ id: 2,
+ cause: "beyond the log",
+ },
+ {
+ name: "receipt has a negative position",
+ id: -1,
+ cause: "read queued message",
+ },
+ }
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ _, err := memory.AppendOnce("user", "Other text.", &tc.id, func(int) error {
+ require.FailNow(t, "an invalid receipt must not be prepared")
+
+ return nil
+ })
+ require.ErrorContains(t, err, tc.cause)
+ require.Equal(t, 1, memory.Len())
+ })
+ }
+}
+
+// TestReplaySyntheticLog runs a synthetic log through the memory core with the
+// fake summarizer. After each message it waits for the builds, as a turn does.
+// It checks that each view stays within its sawtooth, that no view line holds a
+// whole long message or a part of an unbuilt one, and that a restart in the
+// middle and at the end loads the same views byte for byte. With -short, the
+// log is shorter, and the test still requires a batch of the chat view.
+func TestReplaySyntheticLog(t *testing.T) {
+ start := time.Now()
+ messages := replayMessages
+ if testing.Short() {
+ messages = replayShortMessages
+ }
+
+ root := t.TempDir()
+ clock := newTestClock()
+ random := rand.New(rand.NewPCG(1, 2))
+ memory, err := openMemory(t.Context(), root, clock.Now, newFakeSummarizer(), discardLog())
+ require.NoError(t, err)
+
+ defer func() {
+ require.NoError(t, memory.Close())
+ }()
+
+ batches := 0
+
+ low := viewChatHigh
+ long := make([]bool, messages)
+ for id := range messages {
+ if id == messages/2 {
+ memory = replayRequireRestart(t, memory, root, clock)
+ }
+
+ clock.now = clock.now.Add(time.Minute)
+ kind, text := replayMessage(random, id)
+ long[id] = len(kind)+len(": ")+len(text) > nodeBytes
+ lines := replayViewLines(memory)
+ _, err = memory.Append(kind, text)
+ require.NoError(t, err)
+
+ size, merged := replayRequireViews(t, memory, lines, long, id%1000 == 0)
+ if merged {
+ batches++
+ low = min(low, size)
+ }
+
+ require.NoError(t, memory.Wait(t.Context()))
+ }
+
+ memory = replayRequireRestart(t, memory, root, clock)
+ require.Equal(t, messages, memory.log.Len())
+ require.Positive(t, batches)
+
+ require.NoError(t, memory.Wait(t.Context()))
+ t.Logf("%d messages, %d nodes, %d batches in %s", messages, memory.tree.Count(), batches, time.Since(start).Round(time.Second))
+ t.Logf("chat view: %d lines, %d bytes, lowest after a batch %d bytes", len(memory.chat.lines), memory.chat.Size(), low)
+ t.Logf("compaction view: %d lines, %d bytes", len(memory.cview.lines), memory.cview.Size())
+
+ runtime.GC()
+
+ var stats runtime.MemStats
+
+ runtime.ReadMemStats(&stats)
+ t.Logf("heap in use: %d KB", stats.HeapInuse/1024)
+}
+