9 "github.com/stretchr/testify/require"
12 func TestLogReopenLoadsTheSameMessages(t *testing.T) {
14 clock := newTestClock()
15 log, err := openLog(t.Context(), root, clock.Now)
16 require.NoError(t, err)
20 for _, kind := range []string{"user", "reply", "tool", "echo", "work", "note"} {
21 message, err := log.Append(kind, kind+" text ✓")
22 require.NoError(t, err)
24 written = append(written, message)
27 require.Equal(t, 5, written[5].ID)
28 require.Equal(t, len("note text ✓"), written[5].Size)
29 require.NoError(t, log.Close())
31 log, err = openLog(t.Context(), root, clock.Now)
32 require.NoError(t, err)
35 require.NoError(t, log.Close())
38 require.Equal(t, len(written), log.Len())
40 for _, message := range written {
41 read, err := log.Message(message.ID)
42 require.NoError(t, err)
43 require.Equal(t, message, read)
46 message, err := log.Append("user", "after the reopen")
47 require.NoError(t, err)
48 require.Equal(t, 6, message.ID)
51 func TestLogBuildsAMissingIndexAgain(t *testing.T) {
53 clock := newTestClock()
54 log, err := openLog(t.Context(), root, clock.Now)
55 require.NoError(t, err)
57 for _, text := range []string{"a", "b", "c"} {
58 _, err = log.Append("user", text)
59 require.NoError(t, err)
61 clock.now = clock.now.Add(24 * time.Hour)
64 require.NoError(t, log.Close())
66 // A missing index, and an index that a crash cut in a record.
67 for _, cut := range []func(string) error{os.Remove, logTruncateToOneAndAHalf} {
68 require.NoError(t, cut(filepath.Join(root, "index", "main.idx")))
70 log, err = openLog(t.Context(), root, clock.Now)
71 require.NoError(t, err)
72 require.Equal(t, 3, log.Len())
74 message, err := log.Message(2)
75 require.NoError(t, err)
76 require.Equal(t, "c", message.Text)
77 require.NoError(t, log.Close())
81 func TestLogWritesOneFilePerDay(t *testing.T) {
83 clock := newTestClock()
84 log, err := openLog(t.Context(), root, clock.Now)
85 require.NoError(t, err)
88 require.NoError(t, log.Close())
91 _, err = log.Append("user", "first day")
92 require.NoError(t, err)
94 clock.now = clock.now.Add(24 * time.Hour)
95 _, err = log.Append("user", "second day")
96 require.NoError(t, err)
98 first, err := os.ReadFile(filepath.Join(root, "main", "2026-01-01.jsonl"))
99 require.NoError(t, err)
100 require.JSONEq(t, `{"i":0,"kind":"user","text":"first day","size":9,"date":"2026-01-01T12:00:00Z"}`+"\n", string(first))
102 second, err := os.ReadFile(filepath.Join(root, "main", "2026-01-02.jsonl"))
103 require.NoError(t, err)
104 require.JSONEq(t, `{"i":1,"kind":"user","text":"second day","size":10,"date":"2026-01-02T12:00:00Z"}`+"\n", string(second))
107 func TestLogAllowsOneWriter(t *testing.T) {
109 clock := newTestClock()
110 log, err := openLog(t.Context(), root, clock.Now)
111 require.NoError(t, err)
113 _, err = openLog(t.Context(), root, clock.Now)
114 require.ErrorIs(t, err, errLocked)
115 require.NoError(t, log.Close())
117 log, err = openLog(t.Context(), root, clock.Now)
118 require.NoError(t, err)
119 require.NoError(t, log.Close())
122 func TestLogRejectsAnUnknownKind(t *testing.T) {
123 log, err := openLog(t.Context(), t.TempDir(), newTestClock().Now)
124 require.NoError(t, err)
127 require.NoError(t, log.Close())
130 _, err = log.Append("talk", "text")
131 require.ErrorContains(t, err, `unknown message kind "talk"`)
132 require.Equal(t, 0, log.Len())
135 func TestLogRejectsAGapInTheIDs(t *testing.T) {
137 require.NoError(t, os.MkdirAll(filepath.Join(root, "main"), 0o700))
139 line := `{"i":1,"kind":"user","text":"x","size":1,"date":"2026-01-01T12:00:00Z"}` + "\n"
140 require.NoError(t, os.WriteFile(filepath.Join(root, "main", "2026-01-01.jsonl"), []byte(line), 0o600))
142 _, err := openLog(t.Context(), root, newTestClock().Now)
143 require.ErrorContains(t, err, "expected message 0, found 1")
146 func TestLogCutsAnIncompleteLastLine(t *testing.T) {
148 clock := newTestClock()
149 log, err := openLog(t.Context(), root, clock.Now)
150 require.NoError(t, err)
152 _, err = log.Append("user", "whole")
153 require.NoError(t, err)
154 require.NoError(t, log.Close())
156 path := filepath.Join(root, "main", "2026-01-01.jsonl")
157 file, err := os.OpenFile(path, os.O_WRONLY|os.O_APPEND, 0)
158 require.NoError(t, err)
160 _, err = file.WriteString(`{"i":1,"kind":"user","te`)
161 require.NoError(t, err)
162 require.NoError(t, file.Close())
164 log, err = openLog(t.Context(), root, clock.Now)
165 require.NoError(t, err)
166 require.Equal(t, 1, log.Len())
168 message, err := log.Append("user", "after the cut")
169 require.NoError(t, err)
170 require.Equal(t, 1, message.ID)
171 require.NoError(t, log.Close())
173 log, err = openLog(t.Context(), root, clock.Now)
174 require.NoError(t, err)
177 require.NoError(t, log.Close())
180 message, err = log.Message(1)
181 require.NoError(t, err)
182 require.Equal(t, "after the cut", message.Text)
185 func TestLogRejectsAnIncompleteLineBeforeTheLastDay(t *testing.T) {
187 require.NoError(t, os.MkdirAll(filepath.Join(root, "main"), 0o700))
189 first := `{"i":0,"kind":"user","text":"x","size":1,"date":"2026-01-01T12:00:00Z"}` + "\n" + `{"i":1,"kin`
190 second := `{"i":1,"kind":"user","text":"y","size":1,"date":"2026-01-02T12:00:00Z"}` + "\n"
192 require.NoError(t, os.WriteFile(filepath.Join(root, "main", "2026-01-01.jsonl"), []byte(first), 0o600))
193 require.NoError(t, os.WriteFile(filepath.Join(root, "main", "2026-01-02.jsonl"), []byte(second), 0o600))
195 _, err := openLog(t.Context(), root, newTestClock().Now)
196 require.ErrorContains(t, err, "ends in an incomplete line")
199 // logTruncateToOneAndAHalf cuts the index at path in its second record.
200 func logTruncateToOneAndAHalf(path string) error {
201 return os.Truncate(path, positionBytes+positionBytes/2)