package logstorage import ( "encoding/json" "errors" "os" "path/filepath" "strings" "testing" "time" ) func openTestStore(t *testing.T, options Options) *Store { t.Helper() if options.Root == "" { options.Root = t.TempDir() } if options.SegmentBytes == 0 { options.SegmentBytes = minimumSegmentSize } if options.FreeBytes == nil { options.FreeBytes = func(string) (int64, error) { return 1 << 40, nil } } store, err := Open(options) if err != nil { t.Fatalf("Open: %v", err) } return store } func TestStorePersistsChecksummedRecordsAndRebuildsIndex(t *testing.T) { root := t.TempDir() now := time.Date(2026, 7, 29, 1, 2, 3, 0, time.UTC) store := openTestStore(t, Options{Root: root, Clock: func() time.Time { return now }}) for _, message := range []string{"first", "second"} { if _, err := store.Append(ClassSystem, now, map[string]string{"message": message}); err != nil { t.Fatalf("Append: %v", err) } } indexPath := filepath.Join(root, "index", "log-index-v1.json") if err := os.WriteFile(indexPath, []byte("corrupt derived index"), 0o600); err != nil { t.Fatal(err) } reopened := openTestStore(t, Options{Root: root, Clock: func() time.Time { return now }}) payloads, err := reopened.Read(ClassSystem) if err != nil || len(payloads) != 2 { t.Fatalf("Read = %d, %v", len(payloads), err) } var event map[string]string if err := json.Unmarshal(payloads[1], &event); err != nil || event["message"] != "second" { t.Fatalf("second payload = %s, %v", payloads[1], err) } index, err := os.ReadFile(indexPath) if err != nil || !strings.Contains(string(index), `"next_sequence"`) { t.Fatalf("rebuilt index = %q, %v", index, err) } } func TestStoreRecoversOnlyTruncatedActiveTail(t *testing.T) { root := t.TempDir() now := time.Date(2026, 7, 29, 1, 2, 3, 0, time.UTC) store := openTestStore(t, Options{Root: root, Clock: func() time.Time { return now }}) if _, err := store.Append(ClassAudit, now, map[string]string{"message": "durable"}); err != nil { t.Fatal(err) } active := filepath.Join(root, "audit", "active-audit.jsonl") file, err := os.OpenFile(active, os.O_APPEND|os.O_WRONLY, 0) if err != nil { t.Fatal(err) } if _, err := file.WriteString(`{"format_version":1,"sequence":2`); err != nil { t.Fatal(err) } _ = file.Close() reopened := openTestStore(t, Options{Root: root, Clock: func() time.Time { return now }}) payloads, err := reopened.Read(ClassAudit) if err != nil || len(payloads) != 1 { t.Fatalf("Read after recovery = %d, %v", len(payloads), err) } data, _ := os.ReadFile(active) if bytes := strings.Count(string(data), "\n"); bytes != 1 { t.Fatalf("active newline count = %d, want 1", bytes) } } func TestStoreRejectsCorruptClosedSegment(t *testing.T) { root := t.TempDir() now := time.Date(2026, 7, 29, 1, 2, 3, 0, time.UTC) store := openTestStore(t, Options{Root: root, Clock: func() time.Time { return now }}) payload := map[string]string{"message": strings.Repeat("x", 60<<10)} for index := 0; index < 20; index++ { if _, err := store.Append(ClassPort, now.Add(time.Duration(index)*time.Second), payload); err != nil { t.Fatal(err) } } segments, err := filepath.Glob(filepath.Join(root, "port", "segment-port-*.jsonl")) if err != nil || len(segments) == 0 { t.Fatalf("closed segments = %v, %v", segments, err) } data, err := os.ReadFile(segments[0]) if err != nil { t.Fatal(err) } data[len(data)/2] ^= 0xff if err := os.WriteFile(segments[0], data, 0o600); err != nil { t.Fatal(err) } if _, err := Open(Options{Root: root, SegmentBytes: minimumSegmentSize, FreeBytes: func(string) (int64, error) { return 1 << 40, nil }}); !errors.Is(err, ErrCorruptSegment) { t.Fatalf("Open corrupt closed segment error = %v", err) } } func TestStorePagesAcrossIndexedSegments(t *testing.T) { root := t.TempDir() now := time.Date(2026, 7, 29, 1, 2, 3, 0, time.UTC) store := openTestStore(t, Options{Root: root, Clock: func() time.Time { return now }}) payload := map[string]string{"message": strings.Repeat("x", 60<<10)} for index := 0; index < 20; index++ { if _, err := store.Append(ClassPort, now.Add(time.Duration(index)*time.Second), payload); err != nil { t.Fatal(err) } } first, err := store.Page(ClassPort, 0, 7) if err != nil || len(first) != 7 || first[0].Sequence != 1 || first[6].Sequence != 7 { t.Fatalf("first page = (%d, %v), sequences %v..%v", len(first), err, first[0].Sequence, first[6].Sequence) } second, err := store.Page(ClassPort, first[len(first)-1].Sequence, 7) if err != nil || len(second) != 7 || second[0].Sequence != 8 || second[6].Sequence != 14 { t.Fatalf("second page = (%d, %v), sequences %v..%v", len(second), err, second[0].Sequence, second[6].Sequence) } last, err := store.Page(ClassPort, second[len(second)-1].Sequence, 7) if err != nil || len(last) != 6 || last[0].Sequence != 15 || last[5].Sequence != 20 { t.Fatalf("last page = (%d, %v), sequences %v..%v", len(last), err, last[0].Sequence, last[5].Sequence) } } func TestStoreAppliesAgeRetentionAndHardMaximum(t *testing.T) { root := t.TempDir() now := time.Date(2026, 7, 29, 0, 0, 0, 0, time.UTC) store := openTestStore(t, Options{Root: root, Clock: func() time.Time { return now }}) if _, err := store.Append(ClassLink, now.Add(-48*time.Hour), map[string]string{"state": "down"}); err != nil { t.Fatal(err) } if err := store.ApplyRetention(Retention{AuditDays: 365, SystemDays: 365, PortDays: 1}); err != nil { t.Fatal(err) } payloads, err := store.Read(ClassLink) if err != nil || len(payloads) != 0 { t.Fatalf("expired payloads = %d, %v", len(payloads), err) } if err := store.ApplyRetention(Retention{AuditDays: 366, SystemDays: 1, PortDays: 1}); err == nil { t.Fatal("ApplyRetention accepted more than 365 days") } } func TestStoreProtectsAuditReservationAtClassCap(t *testing.T) { store := openTestStore(t, Options{AuditCapBytes: 128}) _, err := store.Append(ClassAudit, time.Now(), map[string]string{"message": strings.Repeat("x", 256)}) if !errors.Is(err, ErrCapacityExhausted) { t.Fatalf("Append error = %v, want capacity exhausted", err) } } func TestStoreRefusesSymlinkedClassDirectory(t *testing.T) { root := t.TempDir() target := t.TempDir() if err := os.Symlink(target, filepath.Join(root, "audit")); err != nil { t.Skipf("symlink unavailable: %v", err) } _, err := Open(Options{Root: root, SegmentBytes: minimumSegmentSize}) if err == nil { t.Fatal("Open accepted symlinked audit directory") } }