This repository has no description
1package engine
2
3import (
4 "context"
5 "errors"
6 "slices"
7 "testing"
8 "time"
9)
10
11func TestPruneCachesExpiryPolicies(t *testing.T) {
12 ctx := context.Background()
13 now := time.Date(2026, 5, 6, 7, 8, 9, 0, time.UTC)
14 type spec struct {
15 id string
16 state string
17 age time.Duration
18 }
19 tests := []struct {
20 name string
21 retention time.Duration
22 pendingMax time.Duration
23 entries []spec
24 wantPruned int
25 wantSurvive []string
26 }{
27 {
28 name: "ready entries expire by last use",
29 retention: time.Hour,
30 pendingMax: 15 * time.Minute,
31 entries: []spec{{"old", "ready", 2 * time.Hour}, {"fresh", "ready", 30 * time.Minute}},
32 wantPruned: 1,
33 wantSurvive: []string{"fresh"},
34 },
35 {
36 name: "zero retention keeps ready entries",
37 retention: 0,
38 pendingMax: time.Hour,
39 entries: []spec{{"ready", "ready", 24 * time.Hour}, {"pending", "pending", 2 * time.Hour}},
40 wantPruned: 1,
41 wantSurvive: []string{"ready"},
42 },
43 {
44 name: "pending entries expire by age",
45 retention: time.Hour,
46 pendingMax: time.Hour,
47 entries: []spec{{"old", "pending", 2 * time.Hour}, {"fresh", "pending", 10 * time.Minute}},
48 wantPruned: 1,
49 wantSurvive: []string{"fresh"},
50 },
51 }
52 for _, tt := range tests {
53 t.Run(tt.name, func(t *testing.T) {
54 d := newCacheTestDB(t)
55 store := &fakeStorage{objects: map[string][]byte{}}
56 keys := map[string]string{}
57 for _, e := range tt.entries {
58 entry := cacheTestEntry(e.id, "did:plc:repo", "microvm", "deps", "hash", e.state, now.Add(-e.age))
59 insertCacheTestEntry(t, d, entry)
60 store.objects[entry.StorageKey] = []byte(e.id)
61 keys[e.id] = entry.StorageKey
62 }
63
64 pruned, err := PruneCaches(ctx, d, store, now, tt.retention, tt.pendingMax, 10)
65 if err != nil {
66 t.Fatalf("PruneCaches: %v", err)
67 }
68 if pruned != tt.wantPruned {
69 t.Fatalf("pruned %d entries, want %d", pruned, tt.wantPruned)
70 }
71 for _, e := range tt.entries {
72 survives := slices.Contains(tt.wantSurvive, e.id)
73 if store.has(keys[e.id]) != survives || cacheTestEntryExists(t, d, e.id) != survives {
74 t.Fatalf("entry %q survived = %v, want %v", e.id, !survives, survives)
75 }
76 }
77 })
78 }
79}
80
81func TestPruneCachesSkipsEntryRefreshedAfterScan(t *testing.T) {
82 ctx := context.Background()
83 d := newCacheTestDB(t)
84 now := time.Date(2026, 5, 6, 7, 8, 9, 0, time.UTC)
85 first := cacheTestEntry("first", "did:plc:repo", "microvm", "deps", "first", "ready", now.Add(-3*time.Hour))
86 refreshed := cacheTestEntry("refreshed", "did:plc:repo", "microvm", "deps", "refreshed", "ready", now.Add(-2*time.Hour))
87 insertCacheTestEntry(t, d, first)
88 insertCacheTestEntry(t, d, refreshed)
89 store := &fakeStorage{objects: map[string][]byte{
90 first.StorageKey: []byte("first"),
91 refreshed.StorageKey: []byte("refreshed"),
92 }}
93 store.onDelete = func(key string) {
94 if key != first.StorageKey {
95 return
96 }
97 if err := d.TouchCacheEntry(ctx, refreshed.ID, now); err != nil {
98 t.Fatalf("TouchCacheEntry: %v", err)
99 }
100 }
101
102 pruned, err := PruneCaches(ctx, d, store, now, time.Hour, time.Hour, 10)
103 if err != nil {
104 t.Fatalf("PruneCaches: %v", err)
105 }
106 if pruned != 1 {
107 t.Fatalf("pruned %d entries, want 1", pruned)
108 }
109 if !store.has(refreshed.StorageKey) || !cacheTestEntryExists(t, d, refreshed.ID) {
110 t.Fatal("entry refreshed after expiry scan was pruned")
111 }
112}
113
114func TestPruneCachesDeleteFailureRemainsRetryable(t *testing.T) {
115 for _, initialState := range []string{"ready", "deleting"} {
116 t.Run(initialState, func(t *testing.T) {
117 ctx := context.Background()
118 d := newCacheTestDB(t)
119 now := time.Date(2026, 5, 6, 7, 8, 9, 0, time.UTC)
120 entry := cacheTestEntry("retry", "did:plc:repo", "microvm", "deps", "hash", initialState, now.Add(-2*time.Hour))
121 insertCacheTestEntry(t, d, entry)
122 deleteErr := errors.New("delete failed")
123 store := &fakeStorage{
124 objects: map[string][]byte{entry.StorageKey: []byte("archive")},
125 deleteErr: deleteErr,
126 }
127
128 pruned, err := PruneCaches(ctx, d, store, now, time.Hour, time.Hour, 10)
129 if !errors.Is(err, deleteErr) {
130 t.Fatalf("first PruneCaches error = %v, want %v", err, deleteErr)
131 }
132 if pruned != 0 {
133 t.Fatalf("first prune count = %d, want 0", pruned)
134 }
135 state, _, _ := cacheTestEntryState(t, d, entry.ID)
136 if state != initialState {
137 t.Fatalf("state after delete failure = %q, want %q", state, initialState)
138 }
139 if !store.has(entry.StorageKey) {
140 t.Fatal("failed delete removed object")
141 }
142
143 pruned, err = PruneCaches(ctx, d, store, now, time.Hour, time.Hour, 10)
144 if err != nil {
145 t.Fatalf("retry PruneCaches: %v", err)
146 }
147 if pruned != 1 || store.has(entry.StorageKey) || cacheTestEntryExists(t, d, entry.ID) {
148 t.Fatalf("retry result = pruned %d, object %v, metadata %v", pruned, store.has(entry.StorageKey), cacheTestEntryExists(t, d, entry.ID))
149 }
150 })
151 }
152}