package engine import ( "context" "errors" "slices" "testing" "time" ) func TestPruneCachesExpiryPolicies(t *testing.T) { ctx := context.Background() now := time.Date(2026, 5, 6, 7, 8, 9, 0, time.UTC) type spec struct { id string state string age time.Duration } tests := []struct { name string retention time.Duration pendingMax time.Duration entries []spec wantPruned int wantSurvive []string }{ { name: "ready entries expire by last use", retention: time.Hour, pendingMax: 15 * time.Minute, entries: []spec{{"old", "ready", 2 * time.Hour}, {"fresh", "ready", 30 * time.Minute}}, wantPruned: 1, wantSurvive: []string{"fresh"}, }, { name: "zero retention keeps ready entries", retention: 0, pendingMax: time.Hour, entries: []spec{{"ready", "ready", 24 * time.Hour}, {"pending", "pending", 2 * time.Hour}}, wantPruned: 1, wantSurvive: []string{"ready"}, }, { name: "pending entries expire by age", retention: time.Hour, pendingMax: time.Hour, entries: []spec{{"old", "pending", 2 * time.Hour}, {"fresh", "pending", 10 * time.Minute}}, wantPruned: 1, wantSurvive: []string{"fresh"}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { d := newCacheTestDB(t) store := &fakeStorage{objects: map[string][]byte{}} keys := map[string]string{} for _, e := range tt.entries { entry := cacheTestEntry(e.id, "did:plc:repo", "microvm", "deps", "hash", e.state, now.Add(-e.age)) insertCacheTestEntry(t, d, entry) store.objects[entry.StorageKey] = []byte(e.id) keys[e.id] = entry.StorageKey } pruned, err := PruneCaches(ctx, d, store, now, tt.retention, tt.pendingMax, 10) if err != nil { t.Fatalf("PruneCaches: %v", err) } if pruned != tt.wantPruned { t.Fatalf("pruned %d entries, want %d", pruned, tt.wantPruned) } for _, e := range tt.entries { survives := slices.Contains(tt.wantSurvive, e.id) if store.has(keys[e.id]) != survives || cacheTestEntryExists(t, d, e.id) != survives { t.Fatalf("entry %q survived = %v, want %v", e.id, !survives, survives) } } }) } } func TestPruneCachesSkipsEntryRefreshedAfterScan(t *testing.T) { ctx := context.Background() d := newCacheTestDB(t) now := time.Date(2026, 5, 6, 7, 8, 9, 0, time.UTC) first := cacheTestEntry("first", "did:plc:repo", "microvm", "deps", "first", "ready", now.Add(-3*time.Hour)) refreshed := cacheTestEntry("refreshed", "did:plc:repo", "microvm", "deps", "refreshed", "ready", now.Add(-2*time.Hour)) insertCacheTestEntry(t, d, first) insertCacheTestEntry(t, d, refreshed) store := &fakeStorage{objects: map[string][]byte{ first.StorageKey: []byte("first"), refreshed.StorageKey: []byte("refreshed"), }} store.onDelete = func(key string) { if key != first.StorageKey { return } if err := d.TouchCacheEntry(ctx, refreshed.ID, now); err != nil { t.Fatalf("TouchCacheEntry: %v", err) } } pruned, err := PruneCaches(ctx, d, store, now, time.Hour, time.Hour, 10) if err != nil { t.Fatalf("PruneCaches: %v", err) } if pruned != 1 { t.Fatalf("pruned %d entries, want 1", pruned) } if !store.has(refreshed.StorageKey) || !cacheTestEntryExists(t, d, refreshed.ID) { t.Fatal("entry refreshed after expiry scan was pruned") } } func TestPruneCachesDeleteFailureRemainsRetryable(t *testing.T) { for _, initialState := range []string{"ready", "deleting"} { t.Run(initialState, func(t *testing.T) { ctx := context.Background() d := newCacheTestDB(t) now := time.Date(2026, 5, 6, 7, 8, 9, 0, time.UTC) entry := cacheTestEntry("retry", "did:plc:repo", "microvm", "deps", "hash", initialState, now.Add(-2*time.Hour)) insertCacheTestEntry(t, d, entry) deleteErr := errors.New("delete failed") store := &fakeStorage{ objects: map[string][]byte{entry.StorageKey: []byte("archive")}, deleteErr: deleteErr, } pruned, err := PruneCaches(ctx, d, store, now, time.Hour, time.Hour, 10) if !errors.Is(err, deleteErr) { t.Fatalf("first PruneCaches error = %v, want %v", err, deleteErr) } if pruned != 0 { t.Fatalf("first prune count = %d, want 0", pruned) } state, _, _ := cacheTestEntryState(t, d, entry.ID) if state != initialState { t.Fatalf("state after delete failure = %q, want %q", state, initialState) } if !store.has(entry.StorageKey) { t.Fatal("failed delete removed object") } pruned, err = PruneCaches(ctx, d, store, now, time.Hour, time.Hour, 10) if err != nil { t.Fatalf("retry PruneCaches: %v", err) } if pruned != 1 || store.has(entry.StorageKey) || cacheTestEntryExists(t, d, entry.ID) { t.Fatalf("retry result = pruned %d, object %v, metadata %v", pruned, store.has(entry.StorageKey), cacheTestEntryExists(t, d, entry.ID)) } }) } }