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)
19 for _, kind := range []string{"user", "reply", "tool", "echo", "work", "note"} {
20 message, err := log.Append(kind, kind+" text ✓")
21 require.NoError(t, err)
23 written = append(written, message)
26 require.Equal(t, 5, written[5].ID)
27 require.Equal(t, len("note text ✓"), written[5].Size)
28 require.NoError(t, log.Close())
30 log, err = openLog(t.Context(), root, clock.Now)
31 require.NoError(t, err)
34 require.NoError(t, log.Close())
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)
44 message, err := log.Append("user", "after the reopen")
45 require.NoError(t, err)
46 require.Equal(t, 6, message.ID)
49 func TestLogBuildsAMissingIndexAgain(t *testing.T) {
51 clock := newTestClock()
52 log, err := openLog(t.Context(), root, clock.Now)
53 require.NoError(t, err)
55 for _, text := range []string{"a", "b", "c"} {
56 _, err = log.Append("user", text)
57 require.NoError(t, err)
59 clock.now = clock.now.Add(24 * time.Hour)
62 require.NoError(t, log.Close())
64 // A missing index, and an index that a crash cut in a record.
65 for _, cut := range []func(string) error{os.Remove, logTruncateToOneAndAHalf} {
66 require.NoError(t, cut(filepath.Join(root, "index", "main.idx")))
68 log, err = openLog(t.Context(), root, clock.Now)
69 require.NoError(t, err)
70 require.Equal(t, 3, log.Len())
72 message, err := log.Message(2)
73 require.NoError(t, err)
74 require.Equal(t, "c", message.Text)
75 require.NoError(t, log.Close())
79 func TestLogWritesOneFilePerDay(t *testing.T) {
81 clock := newTestClock()
82 log, err := openLog(t.Context(), root, clock.Now)
83 require.NoError(t, err)
86 require.NoError(t, log.Close())
89 _, err = log.Append("user", "first day")
90 require.NoError(t, err)
92 clock.now = clock.now.Add(24 * time.Hour)
93 _, err = log.Append("user", "second day")
94 require.NoError(t, err)
96 first, err := os.ReadFile(filepath.Join(root, "main", "2026-01-01.jsonl"))
97 require.NoError(t, err)
98 require.JSONEq(t, `{"i":0,"kind":"user","text":"first day","size":9,"date":"2026-01-01T12:00:00Z"}`+"\n", string(first))
100 second, err := os.ReadFile(filepath.Join(root, "main", "2026-01-02.jsonl"))
101 require.NoError(t, err)
102 require.JSONEq(t, `{"i":1,"kind":"user","text":"second day","size":10,"date":"2026-01-02T12:00:00Z"}`+"\n", string(second))
105 func TestLogAllowsOneWriter(t *testing.T) {
107 clock := newTestClock()
108 log, err := openLog(t.Context(), root, clock.Now)
109 require.NoError(t, err)
111 _, err = openLog(t.Context(), root, clock.Now)
112 require.ErrorIs(t, err, errLocked)
113 require.NoError(t, log.Close())
115 log, err = openLog(t.Context(), root, clock.Now)
116 require.NoError(t, err)
117 require.NoError(t, log.Close())
120 func TestLogRejectsAnUnknownKind(t *testing.T) {
121 log, err := openLog(t.Context(), t.TempDir(), newTestClock().Now)
122 require.NoError(t, err)
125 require.NoError(t, log.Close())
128 _, err = log.Append("talk", "text")
129 require.ErrorContains(t, err, `unknown message kind "talk"`)
130 require.Equal(t, 0, log.Len())
133 func TestLogRejectsAGapInTheIDs(t *testing.T) {
135 require.NoError(t, os.MkdirAll(filepath.Join(root, "main"), 0o700))
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))
140 _, err := openLog(t.Context(), root, newTestClock().Now)
141 require.ErrorContains(t, err, "expected message 0, found 1")
144 func TestLogCutsAnIncompleteLastLine(t *testing.T) {
146 clock := newTestClock()
147 log, err := openLog(t.Context(), root, clock.Now)
148 require.NoError(t, err)
150 _, err = log.Append("user", "whole")
151 require.NoError(t, err)
152 require.NoError(t, log.Close())
154 path := filepath.Join(root, "main", "2026-01-01.jsonl")
155 file, err := os.OpenFile(path, os.O_WRONLY|os.O_APPEND, 0)
156 require.NoError(t, err)
158 _, err = file.WriteString(`{"i":1,"kind":"user","te`)
159 require.NoError(t, err)
160 require.NoError(t, file.Close())
162 log, err = openLog(t.Context(), root, clock.Now)
163 require.NoError(t, err)
164 require.Equal(t, 1, log.Len())
166 message, err := log.Append("user", "after the cut")
167 require.NoError(t, err)
168 require.Equal(t, 1, message.ID)
169 require.NoError(t, log.Close())
171 log, err = openLog(t.Context(), root, clock.Now)
172 require.NoError(t, err)
175 require.NoError(t, log.Close())
178 message, err = log.Message(1)
179 require.NoError(t, err)
180 require.Equal(t, "after the cut", message.Text)
183 func TestLogRejectsAnIncompleteLineBeforeTheLastDay(t *testing.T) {
185 require.NoError(t, os.MkdirAll(filepath.Join(root, "main"), 0o700))
187 first := `{"i":0,"kind":"user","text":"x","size":1,"date":"2026-01-01T12:00:00Z"}` + "\n" + `{"i":1,"kin`
188 second := `{"i":1,"kind":"user","text":"y","size":1,"date":"2026-01-02T12:00:00Z"}` + "\n"
189 require.NoError(t, os.WriteFile(filepath.Join(root, "main", "2026-01-01.jsonl"), []byte(first), 0o600))
190 require.NoError(t, os.WriteFile(filepath.Join(root, "main", "2026-01-02.jsonl"), []byte(second), 0o600))
192 _, err := openLog(t.Context(), root, newTestClock().Now)
193 require.ErrorContains(t, err, "ends in an incomplete line")
196 // logTruncateToOneAndAHalf cuts the index at path in its second record.
197 func logTruncateToOneAndAHalf(path string) error {
198 return os.Truncate(path, positionBytes+positionBytes/2)