package janitor import ( "context" "os" "path/filepath" "testing" "time" "vetting/internal/store" ) // fakeStores is a test double that records what the janitor asked for // and hands back canned runs/artifacts. It lets us verify both the // cleanup contract (files deleted, rows deleted) and that the janitor // honours a zero retention as a no-op. type fakeStores struct { cutoffSeen time.Time runsOlder []int64 artifactsByID map[int64][]store.Artifact deleted map[int64]bool logs map[int64]string } func (f *fakeStores) CompletedOlderThan(_ context.Context, cutoff time.Time) ([]int64, error) { f.cutoffSeen = cutoff return f.runsOlder, nil } func (f *fakeStores) DeleteArtifactsForRun(_ context.Context, runID int64) ([]store.Artifact, error) { if f.deleted == nil { f.deleted = map[int64]bool{} } f.deleted[runID] = true return f.artifactsByID[runID], nil } func (f *fakeStores) LogPathFor(runID int64) string { return f.logs[runID] } func writeTempFile(t *testing.T, dir, name string) string { t.Helper() p := filepath.Join(dir, name) if err := os.WriteFile(p, []byte("x"), 0o644); err != nil { t.Fatalf("write %s: %v", p, err) } return p } func TestSweepDeletesArtifactsAndLogs(t *testing.T) { dir := t.TempDir() p1 := writeTempFile(t, dir, "artifact-1.bin") p2 := writeTempFile(t, dir, "artifact-2.json") log1 := writeTempFile(t, dir, "run-1.log") s := &fakeStores{ runsOlder: []int64{1}, artifactsByID: map[int64][]store.Artifact{ 1: {{ID: 10, RunID: 1, Path: p1}, {ID: 11, RunID: 1, Path: p2}}, }, logs: map[int64]string{1: log1}, } j := New(Config{ ArtifactRetention: 24 * time.Hour, LogRetention: 24 * time.Hour, Interval: time.Minute, }, s) if err := j.Sweep(context.Background(), time.Now().UTC()); err != nil { t.Fatalf("sweep: %v", err) } if !s.deleted[1] { t.Fatalf("run 1 not passed to DeleteArtifactsForRun") } for _, p := range []string{p1, p2, log1} { if _, err := os.Stat(p); !os.IsNotExist(err) { t.Errorf("file %s still exists (err=%v)", p, err) } } } func TestSweepIsNoopWhenRetentionsAreZero(t *testing.T) { dir := t.TempDir() p := writeTempFile(t, dir, "keep.bin") s := &fakeStores{ runsOlder: []int64{1}, artifactsByID: map[int64][]store.Artifact{ 1: {{ID: 10, RunID: 1, Path: p}}, }, logs: map[int64]string{1: p}, } j := New(Config{}, s) // all zero if err := j.Sweep(context.Background(), time.Now().UTC()); err != nil { t.Fatalf("sweep: %v", err) } if s.deleted[1] { t.Fatalf("expected no deletion for zero retention") } if _, err := os.Stat(p); err != nil { t.Fatalf("file should still exist: %v", err) } } func TestSweepSkipsMissingFilesGracefully(t *testing.T) { s := &fakeStores{ runsOlder: []int64{7}, artifactsByID: map[int64][]store.Artifact{ 7: {{ID: 99, RunID: 7, Path: "/nonexistent/path.bin"}}, }, logs: map[int64]string{7: "/nonexistent/run-7.log"}, } j := New(Config{ArtifactRetention: time.Hour, LogRetention: time.Hour}, s) if err := j.Sweep(context.Background(), time.Now().UTC()); err != nil { t.Fatalf("sweep: %v", err) } if !s.deleted[7] { t.Fatalf("run 7 should have been processed") } } func TestSweepUsesTheLongerCutoff(t *testing.T) { s := &fakeStores{} j := New(Config{ ArtifactRetention: 72 * time.Hour, LogRetention: 24 * time.Hour, }, s) now := time.Date(2026, 4, 17, 12, 0, 0, 0, time.UTC) if err := j.Sweep(context.Background(), now); err != nil { t.Fatalf("sweep: %v", err) } want := now.Add(-72 * time.Hour) if !s.cutoffSeen.Equal(want) { t.Fatalf("cutoff = %v, want %v (the longer of the two retentions)", s.cutoffSeen, want) } }