]> git.giacomolli.uk - onechat/blob - log_test.go
Keep a built leaf when the view lags the tree
[onechat] / log_test.go
1 package main
2
3 import (
4         "os"
5         "path/filepath"
6         "testing"
7         "time"
8
9         "github.com/stretchr/testify/require"
10 )
11
12 func TestLogReopenLoadsTheSameMessages(t *testing.T) {
13         root := t.TempDir()
14         clock := newTestClock()
15         log, err := openLog(t.Context(), root, clock.Now)
16         require.NoError(t, err)
17
18         var written []Message
19         for _, kind := range []string{"user", "reply", "tool", "echo", "work", "note"} {
20                 message, err := log.Append(kind, kind+" text ✓")
21                 require.NoError(t, err)
22
23                 written = append(written, message)
24         }
25
26         require.Equal(t, 5, written[5].ID)
27         require.Equal(t, len("note text ✓"), written[5].Size)
28         require.NoError(t, log.Close())
29
30         log, err = openLog(t.Context(), root, clock.Now)
31         require.NoError(t, err)
32
33         defer func() {
34                 require.NoError(t, log.Close())
35         }()
36
37         require.Equal(t, len(written), log.Len())
38         for _, message := range written {
39                 read, err := log.Message(message.ID)
40                 require.NoError(t, err)
41                 require.Equal(t, message, read)
42         }
43
44         message, err := log.Append("user", "after the reopen")
45         require.NoError(t, err)
46         require.Equal(t, 6, message.ID)
47 }
48
49 func TestLogBuildsAMissingIndexAgain(t *testing.T) {
50         root := t.TempDir()
51         clock := newTestClock()
52         log, err := openLog(t.Context(), root, clock.Now)
53         require.NoError(t, err)
54
55         for _, text := range []string{"a", "b", "c"} {
56                 _, err = log.Append("user", text)
57                 require.NoError(t, err)
58
59                 clock.now = clock.now.Add(24 * time.Hour)
60         }
61
62         require.NoError(t, log.Close())
63
64         // A missing index, and an index that a crash cut in a record.
65         for _, cut := range []func(string) error{os.Remove, truncateToOneAndAHalf} {
66                 require.NoError(t, cut(filepath.Join(root, "index", "main.idx")))
67
68                 log, err = openLog(t.Context(), root, clock.Now)
69                 require.NoError(t, err)
70                 require.Equal(t, 3, log.Len())
71
72                 message, err := log.Message(2)
73                 require.NoError(t, err)
74                 require.Equal(t, "c", message.Text)
75                 require.NoError(t, log.Close())
76         }
77 }
78
79 func TestLogWritesOneFilePerDay(t *testing.T) {
80         root := t.TempDir()
81         clock := newTestClock()
82         log, err := openLog(t.Context(), root, clock.Now)
83         require.NoError(t, err)
84
85         defer func() {
86                 require.NoError(t, log.Close())
87         }()
88
89         _, err = log.Append("user", "first day")
90         require.NoError(t, err)
91
92         clock.now = clock.now.Add(24 * time.Hour)
93         _, err = log.Append("user", "second day")
94         require.NoError(t, err)
95
96         first, err := os.ReadFile(filepath.Join(root, "main", "2026-01-01.jsonl"))
97         require.NoError(t, err)
98         require.Equal(t, `{"i":0,"kind":"user","text":"first day","size":9,"date":"2026-01-01T12:00:00Z"}`+"\n", string(first))
99
100         second, err := os.ReadFile(filepath.Join(root, "main", "2026-01-02.jsonl"))
101         require.NoError(t, err)
102         require.Equal(t, `{"i":1,"kind":"user","text":"second day","size":10,"date":"2026-01-02T12:00:00Z"}`+"\n", string(second))
103 }
104
105 func TestLogAllowsOneWriter(t *testing.T) {
106         root := t.TempDir()
107         clock := newTestClock()
108         log, err := openLog(t.Context(), root, clock.Now)
109         require.NoError(t, err)
110
111         _, err = openLog(t.Context(), root, clock.Now)
112         require.ErrorIs(t, err, errLocked)
113         require.NoError(t, log.Close())
114
115         log, err = openLog(t.Context(), root, clock.Now)
116         require.NoError(t, err)
117         require.NoError(t, log.Close())
118 }
119
120 func TestLogRejectsAnUnknownKind(t *testing.T) {
121         log, err := openLog(t.Context(), t.TempDir(), newTestClock().Now)
122         require.NoError(t, err)
123
124         defer func() {
125                 require.NoError(t, log.Close())
126         }()
127
128         _, err = log.Append("talk", "text")
129         require.ErrorContains(t, err, `unknown message kind "talk"`)
130         require.Equal(t, 0, log.Len())
131 }
132
133 func TestLogRejectsAGapInTheIDs(t *testing.T) {
134         root := t.TempDir()
135         require.NoError(t, os.MkdirAll(filepath.Join(root, "main"), 0o700))
136
137         line := `{"i":1,"kind":"user","text":"x","size":1,"date":"2026-01-01T12:00:00Z"}` + "\n"
138         require.NoError(t, os.WriteFile(filepath.Join(root, "main", "2026-01-01.jsonl"), []byte(line), 0o600))
139
140         _, err := openLog(t.Context(), root, newTestClock().Now)
141         require.ErrorContains(t, err, "expected message 0, found 1")
142 }
143
144 // truncateToOneAndAHalf cuts the index at path in its second record.
145 func truncateToOneAndAHalf(path string) error {
146         return os.Truncate(path, positionBytes+positionBytes/2)
147 }