package main import ( "os" "path/filepath" "testing" "time" "github.com/stretchr/testify/require" ) func TestLogReopenLoadsTheSameMessages(t *testing.T) { root := t.TempDir() clock := newTestClock() log, err := openLog(t.Context(), root, clock.Now) require.NoError(t, err) var written []Message for _, kind := range []string{"user", "reply", "tool", "echo", "work", "note"} { message, err := log.Append(kind, kind+" text ✓") require.NoError(t, err) written = append(written, message) } require.Equal(t, 5, written[5].ID) require.Equal(t, len("note text ✓"), written[5].Size) require.NoError(t, log.Close()) log, err = openLog(t.Context(), root, clock.Now) require.NoError(t, err) defer func() { require.NoError(t, log.Close()) }() require.Equal(t, len(written), log.Len()) for _, message := range written { read, err := log.Message(message.ID) require.NoError(t, err) require.Equal(t, message, read) } message, err := log.Append("user", "after the reopen") require.NoError(t, err) require.Equal(t, 6, message.ID) } func TestLogBuildsAMissingIndexAgain(t *testing.T) { root := t.TempDir() clock := newTestClock() log, err := openLog(t.Context(), root, clock.Now) require.NoError(t, err) for _, text := range []string{"a", "b", "c"} { _, err = log.Append("user", text) require.NoError(t, err) clock.now = clock.now.Add(24 * time.Hour) } require.NoError(t, log.Close()) // A missing index, and an index that a crash cut in a record. for _, cut := range []func(string) error{os.Remove, truncateToOneAndAHalf} { require.NoError(t, cut(filepath.Join(root, "index", "main.idx"))) log, err = openLog(t.Context(), root, clock.Now) require.NoError(t, err) require.Equal(t, 3, log.Len()) message, err := log.Message(2) require.NoError(t, err) require.Equal(t, "c", message.Text) require.NoError(t, log.Close()) } } func TestLogWritesOneFilePerDay(t *testing.T) { root := t.TempDir() clock := newTestClock() log, err := openLog(t.Context(), root, clock.Now) require.NoError(t, err) defer func() { require.NoError(t, log.Close()) }() _, err = log.Append("user", "first day") require.NoError(t, err) clock.now = clock.now.Add(24 * time.Hour) _, err = log.Append("user", "second day") require.NoError(t, err) first, err := os.ReadFile(filepath.Join(root, "main", "2026-01-01.jsonl")) require.NoError(t, err) require.Equal(t, `{"i":0,"kind":"user","text":"first day","size":9,"date":"2026-01-01T12:00:00Z"}`+"\n", string(first)) second, err := os.ReadFile(filepath.Join(root, "main", "2026-01-02.jsonl")) require.NoError(t, err) require.Equal(t, `{"i":1,"kind":"user","text":"second day","size":10,"date":"2026-01-02T12:00:00Z"}`+"\n", string(second)) } func TestLogAllowsOneWriter(t *testing.T) { root := t.TempDir() clock := newTestClock() log, err := openLog(t.Context(), root, clock.Now) require.NoError(t, err) _, err = openLog(t.Context(), root, clock.Now) require.ErrorIs(t, err, errLocked) require.NoError(t, log.Close()) log, err = openLog(t.Context(), root, clock.Now) require.NoError(t, err) require.NoError(t, log.Close()) } func TestLogRejectsAnUnknownKind(t *testing.T) { log, err := openLog(t.Context(), t.TempDir(), newTestClock().Now) require.NoError(t, err) defer func() { require.NoError(t, log.Close()) }() _, err = log.Append("talk", "text") require.ErrorContains(t, err, `unknown message kind "talk"`) require.Equal(t, 0, log.Len()) } func TestLogRejectsAGapInTheIDs(t *testing.T) { root := t.TempDir() require.NoError(t, os.MkdirAll(filepath.Join(root, "main"), 0o700)) line := `{"i":1,"kind":"user","text":"x","size":1,"date":"2026-01-01T12:00:00Z"}` + "\n" require.NoError(t, os.WriteFile(filepath.Join(root, "main", "2026-01-01.jsonl"), []byte(line), 0o600)) _, err := openLog(t.Context(), root, newTestClock().Now) require.ErrorContains(t, err, "expected message 0, found 1") } func TestLogCutsAnIncompleteLastLine(t *testing.T) { root := t.TempDir() clock := newTestClock() log, err := openLog(t.Context(), root, clock.Now) require.NoError(t, err) _, err = log.Append("user", "whole") require.NoError(t, err) require.NoError(t, log.Close()) path := filepath.Join(root, "main", "2026-01-01.jsonl") file, err := os.OpenFile(path, os.O_WRONLY|os.O_APPEND, 0) require.NoError(t, err) _, err = file.WriteString(`{"i":1,"kind":"user","te`) require.NoError(t, err) require.NoError(t, file.Close()) log, err = openLog(t.Context(), root, clock.Now) require.NoError(t, err) require.Equal(t, 1, log.Len()) message, err := log.Append("user", "after the cut") require.NoError(t, err) require.Equal(t, 1, message.ID) require.NoError(t, log.Close()) log, err = openLog(t.Context(), root, clock.Now) require.NoError(t, err) defer func() { require.NoError(t, log.Close()) }() message, err = log.Message(1) require.NoError(t, err) require.Equal(t, "after the cut", message.Text) } func TestLogRejectsAnIncompleteLineBeforeTheLastDay(t *testing.T) { root := t.TempDir() require.NoError(t, os.MkdirAll(filepath.Join(root, "main"), 0o700)) first := `{"i":0,"kind":"user","text":"x","size":1,"date":"2026-01-01T12:00:00Z"}` + "\n" + `{"i":1,"kin` second := `{"i":1,"kind":"user","text":"y","size":1,"date":"2026-01-02T12:00:00Z"}` + "\n" require.NoError(t, os.WriteFile(filepath.Join(root, "main", "2026-01-01.jsonl"), []byte(first), 0o600)) require.NoError(t, os.WriteFile(filepath.Join(root, "main", "2026-01-02.jsonl"), []byte(second), 0o600)) _, err := openLog(t.Context(), root, newTestClock().Now) require.ErrorContains(t, err, "ends in an incomplete line") } // truncateToOneAndAHalf cuts the index at path in its second record. func truncateToOneAndAHalf(path string) error { return os.Truncate(path, positionBytes+positionBytes/2) }