Something went wrong. Try again.
Monorepo for Tangled tangled.org
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189package engine
import ( "context" "errors" "slices" "testing" "time"
"tangled.org/core/spindle/db")
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)) } }) }}
func TestPruneCachesRetriesQueuedObjectDeletion(t *testing.T) { ctx := context.Background() d := newCacheTestDB(t) now := time.Now() key := "objects/did:web:example.com/orphan" if err := d.ApplyEventBatch(nil, func(tx *db.EventBatchTx) error { return tx.QueueCacheObjectDeletion(ctx, key, now) }); err != nil { t.Fatal(err) } deleteErr := errors.New("delete failed") store := &fakeStorage{ objects: map[string][]byte{key: []byte("archive")}, deleteErr: deleteErr, } if _, err := PruneCaches(ctx, d, store, now, time.Hour, time.Hour, 10); !errors.Is(err, deleteErr) { t.Fatalf("first prune error = %v, want %v", err, deleteErr) } pending, err := d.PendingCacheObjectDeletions(ctx, 10) if err != nil { t.Fatal(err) } if len(pending) != 1 || pending[0] != key { t.Fatalf("pending object deletions = %v", pending) } pruned, err := PruneCaches(ctx, d, store, now, time.Hour, time.Hour, 10) if err != nil { t.Fatal(err) } if pruned != 1 || store.has(key) { t.Fatalf("retry pruned %d, object remains %t", pruned, store.has(key)) }}