package history import ( "errors" "io" "os" "strings" "testing" "time" "github.com/klauspost/compress/zstd" ) func TestAppendBoundsUntrustedEventText(t *testing.T) { store := Store{DataDir: t.TempDir()} matrix := map[string]string{} for i := range 64 { matrix[string(rune('A'+i%26))+strings.Repeat("k", 1100)] = strings.Repeat("v", 5000) } if _, err := store.Append(Event{RunID: "large", ChildID: "run", Repo: "repo", Job: strings.Repeat("j", 70000), Status: "failed", Detail: strings.Repeat("x", 70000), Matrix: matrix}); err != nil { t.Fatal(err) } events, err := ReadAll(store.DataDir) if err != nil || len(events) != 1 || len(events[0].Job) > 4100 || len(events[0].Detail) > 8200 { t.Fatalf("events=%d err=%v", len(events), err) } } func TestReadAllEmptyAndInvalidFiles(t *testing.T) { dir := t.TempDir() events, err := ReadAll(dir) if err != nil { t.Fatalf("ReadAll empty error = %v", err) } if len(events) != 0 { t.Fatalf("empty events = %#v", events) } if err := os.MkdirAll(dir+"/events", 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(dir+"/events/bad.jsonl.zst", []byte("not zstd"), 0o644); err != nil { t.Fatal(err) } if _, err := ReadAll(dir); err == nil { t.Fatal("invalid zstd accepted") } } func TestReadAllRejectsBrokenInputs(t *testing.T) { dir := t.TempDir() if err := os.MkdirAll(dir+"/events", 0o755); err != nil { t.Fatal(err) } if err := os.Symlink("missing", dir+"/events/missing.jsonl.zst"); err != nil { t.Fatal(err) } if _, err := ReadAll(dir); err == nil { t.Fatal("broken symlink accepted") } if err := os.Remove(dir + "/events/missing.jsonl.zst"); err != nil { t.Fatal(err) } writeZstd(t, dir+"/events/bad-json.jsonl.zst", []byte("{\n")) if _, err := ReadAll(dir); err == nil { t.Fatal("bad JSON accepted") } if err := os.Remove(dir + "/events/bad-json.jsonl.zst"); err != nil { t.Fatal(err) } writeZstd(t, dir+"/events/huge.jsonl.zst", []byte(strings.Repeat("x", 70*1024))) if _, err := ReadAll(dir); err == nil { t.Fatal("huge scanner token accepted") } } func writeZstd(t *testing.T, path string, data []byte) { t.Helper() file, err := os.Create(path) if err != nil { t.Fatal(err) } encoder, err := zstd.NewWriter(file) if err != nil { t.Fatal(err) } if _, err := encoder.Write(data); err != nil { t.Fatal(err) } if err := encoder.Close(); err != nil { t.Fatal(err) } if err := file.Close(); err != nil { t.Fatal(err) } } func TestAppendRejectsBadDataDir(t *testing.T) { root := t.TempDir() bad := root + "/file" if err := os.WriteFile(bad, []byte("x"), 0o644); err != nil { t.Fatal(err) } _, err := Store{DataDir: bad, Now: func() time.Time { return time.Date(2026, 6, 18, 1, 2, 3, 0, time.UTC) }}.Append(Event{RunID: "run"}) if err == nil { t.Fatal("append under file path accepted") } } func TestAppendRejectsExistingDirectoryLogPath(t *testing.T) { store := Store{DataDir: t.TempDir(), Now: func() time.Time { return time.Date(2026, 6, 18, 1, 2, 3, 0, time.UTC) }} if err := os.MkdirAll(store.DataDir+"/events/2026-06.jsonl.zst", 0o755); err != nil { t.Fatal(err) } if _, err := store.Append(Event{RunID: "run"}); err == nil { t.Fatal("append accepted directory log path") } } func TestReadAllInjectedReaderFailure(t *testing.T) { dir := t.TempDir() if err := os.MkdirAll(dir+"/events", 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(dir+"/events/x.jsonl.zst", []byte("x"), 0o644); err != nil { t.Fatal(err) } old := newReader newReader = func(io.Reader) (zstdReader, error) { return nil, errors.New("reader boom") } defer func() { newReader = old }() if _, err := ReadAll(dir); err == nil { t.Fatal("reader failure hidden") } } func TestAppendInjectedFailures(t *testing.T) { store := Store{DataDir: t.TempDir(), Now: func() time.Time { return time.Date(2026, 6, 18, 1, 2, 3, 0, time.UTC) }} boom := errors.New("boom") oldWriter := newWriter newWriter = func(io.Writer) (zstdWriter, error) { return nil, boom } if _, err := store.Append(Event{RunID: "run"}); !errors.Is(err, boom) { t.Fatalf("writer err = %v", err) } newWriter = oldWriter oldMarshal := marshal marshal = func(any) ([]byte, error) { return nil, boom } if _, err := store.Append(Event{RunID: "run"}); !errors.Is(err, boom) { t.Fatalf("marshal err = %v", err) } marshal = oldMarshal oldWriter = newWriter newWriter = func(io.Writer) (zstdWriter, error) { return failWriter{writeErr: boom}, nil } if _, err := store.Append(Event{RunID: "run"}); !errors.Is(err, boom) { t.Fatalf("write err = %v", err) } newWriter = oldWriter newWriter = func(io.Writer) (zstdWriter, error) { return failWriter{closeErr: boom}, nil } if _, err := store.Append(Event{RunID: "run"}); !errors.Is(err, boom) { t.Fatalf("close err = %v", err) } newWriter = oldWriter oldOpen := openAppend openAppend = func(string) (appendFile, error) { return failFile{closeErr: boom}, nil } if _, err := store.Append(Event{RunID: "run"}); !errors.Is(err, boom) { t.Fatalf("file close err = %v", err) } openAppend = oldOpen } type failWriter struct{ writeErr, closeErr error } func (w failWriter) Write([]byte) (int, error) { return 0, w.writeErr } func (w failWriter) Close() error { return w.closeErr } type failFile struct{ closeErr error } type partialFile struct { *os.File err error } func (f partialFile) Write(p []byte) (int, error) { if len(p) == 0 { return 0, f.err } n, _ := f.File.Write(p[:max(1, len(p)/2)]) return n, f.err } func (f failFile) Write(p []byte) (int, error) { return len(p), nil } func (f failFile) Close() error { return f.closeErr } func TestFailedAppendLeavesPreviousHistoryReadable(t *testing.T) { store := Store{DataDir: t.TempDir(), Now: func() time.Time { return time.Date(2026, 6, 18, 1, 2, 3, 0, time.UTC) }} if _, err := store.Append(Event{RunID: "first", ChildID: "run", Repo: "repo", Job: "check", Status: "success"}); err != nil { t.Fatal(err) } boom := errors.New("disk full") oldOpen := openAppend t.Cleanup(func() { openAppend = oldOpen }) openAppend = func(path string) (appendFile, error) { file, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644) if err != nil { return nil, err } return partialFile{File: file, err: boom}, nil } if _, err := store.Append(Event{RunID: "second", ChildID: "run", Repo: "repo", Job: "check", Status: "failed"}); !errors.Is(err, boom) { t.Fatalf("append err=%v", err) } openAppend = oldOpen events, err := ReadAll(store.DataDir) if err != nil || len(events) != 1 || events[0].RunID != "first" { t.Fatalf("events=%#v err=%v", events, err) } } func TestAppendAndReadAllCompressedJSONL(t *testing.T) { store := Store{DataDir: t.TempDir(), Now: func() time.Time { return time.Date(2026, 6, 18, 1, 2, 3, 0, time.UTC) }} path, err := store.Append(Event{RunID: "run1", Repo: "repo", Job: "check", Rev: "abc", Status: "success"}) if err != nil { t.Fatalf("Append() error = %v", err) } if _, err := store.Append(Event{RunID: "run2", Repo: "repo", Job: "test", Rev: "def", Status: "failed"}); err != nil { t.Fatalf("Append() second error = %v", err) } file, err := os.Open(path) if err != nil { t.Fatal(err) } decoder, err := zstd.NewReader(file) if err != nil { t.Fatal(err) } data, err := io.ReadAll(decoder) decoder.Close() file.Close() if err != nil { t.Fatal(err) } text := string(data) if !strings.Contains(text, `"run_id":"run1"`) || !strings.Contains(text, `"status":"success"`) || !strings.HasSuffix(text, "\n") { t.Fatalf("jsonl = %q", text) } events, err := ReadAll(store.DataDir) if err != nil { t.Fatalf("ReadAll() error = %v", err) } if len(events) != 2 || events[0].RunID != "run1" || events[1].RunID != "run2" { t.Fatalf("events = %#v", events) } } func TestAppendOnceUsesRunAndChildIdentity(t *testing.T) { now := time.Date(2026, 8, 5, 1, 2, 3, 4, time.UTC) store := Store{DataDir: t.TempDir(), Now: func() time.Time { return now }} event := Event{RunID: "42", ChildID: "run", Repo: "smith", Job: "push", Rev: "abc", Ref: "refs/heads/trunk", Trigger: "push", Status: "success", StartedAt: now.Add(-time.Second)} appended, err := store.AppendOnce(event) if err != nil || !appended { t.Fatalf("first append=%v err=%v", appended, err) } appended, err = store.AppendOnce(event) if err != nil || appended { t.Fatalf("second append=%v err=%v", appended, err) } events, err := ReadAll(store.DataDir) if err != nil || len(events) != 1 || events[0].ChildID != "run" || events[0].Ref != "refs/heads/trunk" || events[0].Trigger != "push" || !events[0].StartedAt.Equal(now.Add(-time.Second)) { t.Fatalf("events=%#v err=%v", events, err) } found, err := store.Contains("42", "run") if err != nil || !found { t.Fatalf("found=%v err=%v", found, err) } found, err = store.Contains("42", "001") if err != nil || found { t.Fatalf("absent=%v err=%v", found, err) } } func TestLegacyEventRemainsReadable(t *testing.T) { dir := t.TempDir() if err := os.MkdirAll(dir+"/events", 0o755); err != nil { t.Fatal(err) } writeZstd(t, dir+"/events/legacy.jsonl.zst", []byte(`{"run_id":"old","repo":"repo","job":"check","rev":"abc","status":"success","time":"2026-08-05T00:00:00Z"}`+"\n")) events, err := ReadAll(dir) if err != nil || len(events) != 1 { t.Fatalf("events=%#v err=%v", events, err) } got := events[0] if got.ChildID != "" || got.Ref != "" || got.Trigger != "" || !got.StartedAt.IsZero() { t.Fatalf("legacy fields=%#v", got) } } func TestReadAllRejectsEventsFile(t *testing.T) { dir := t.TempDir() if err := os.WriteFile(dir+"/events", []byte("x"), 0o644); err != nil { t.Fatal(err) } if _, err := ReadAll(dir); err == nil { t.Fatal("events file accepted as empty history") } }