carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package release
2
3import (
4 "context"
5 "encoding/json"
6 "os"
7 "path/filepath"
8 "strings"
9 "testing"
10 "time"
11)
12
13// pruneFixture seeds n versions v01..vNN with ascending hour-apart stamps
14// ending 30 days before now, so age protection (72h) never applies unless a
15// test asks for it. Each manifest is a real, parseable one (not just a
16// stamp): TestPruneEligibleProtectsPointerVersionAndHistory promotes fixture
17// versions, and Promote's GetManifest call parses the body — a version's
18// SHA256 is a fixed placeholder, never checked against the artifact bytes
19// (validateManifest only checks the hex shape). Returns the signing pair for
20// pointer/production writes.
21func pruneFixture(t *testing.T, n int) (dir string, s *DirStore, now time.Time) {
22 t.Helper()
23 dir = t.TempDir()
24 s = NewDirStore(dir)
25 now = time.Date(2026, 8, 10, 12, 0, 0, 0, time.UTC)
26 base := now.Add(-30 * 24 * time.Hour)
27 ctx := context.Background()
28 for i := 1; i <= n; i++ {
29 v := version(i)
30 mod := base.Add(time.Duration(i) * time.Hour)
31 m := Manifest{
32 Schema: 1, App: "kass", Version: v, Kind: "binary",
33 Artifacts: map[string]Artifact{"linux-arm64": {File: "linux-arm64", SHA256: strings.Repeat("ab", 32), Size: 1}},
34 }
35 raw, err := json.Marshal(&m)
36 if err != nil {
37 t.Fatal(err)
38 }
39 manifestKey := ManifestKey("bab", "kass", v)
40 if _, err := s.Put(ctx, manifestKey, raw, ""); err != nil {
41 t.Fatal(err)
42 }
43 if err := os.Chtimes(filepath.Join(dir, filepath.FromSlash(manifestKey)), mod, mod); err != nil {
44 t.Fatal(err)
45 }
46 modStamp(t, s, dir, ArtifactKey("bab", "kass", v, "linux-arm64"), mod)
47 }
48 return dir, s, now
49}
50
51func version(i int) string { return "v" + string(rune('0'+i/10)) + string(rune('0'+i%10)) }
52
53// restamp updates an already-written key's mod time in place. modStamp's Put
54// is create-only (it exists to seed a fresh key + stamp in one call), so
55// re-stamping a key pruneFixture already wrote needs the bare Chtimes half
56// of that pattern on its own.
57func restamp(t *testing.T, dir, key string, mod time.Time) {
58 t.Helper()
59 if err := os.Chtimes(filepath.Join(dir, filepath.FromSlash(key)), mod, mod); err != nil {
60 t.Fatal(err)
61 }
62}
63
64func TestPruneEligibleKeepsNewestNAndReturnsOldestFirst(t *testing.T) {
65 _, s, now := pruneFixture(t, 25)
66 seedB64, pubB64, err := GenerateKey()
67 if err != nil {
68 t.Fatal(err)
69 }
70 _ = seedB64
71 pub, err := ParsePublicKey(pubB64, "test")
72 if err != nil {
73 t.Fatal(err)
74 }
75 got, err := PruneEligible(context.Background(), s, pub, "bab", "kass", 20, now, nil)
76 if err != nil {
77 t.Fatal(err)
78 }
79 // 25 versions, keep 20 → the 5 oldest are eligible, oldest first.
80 want := []string{"v01", "v02", "v03", "v04", "v05"}
81 if strings.Join(got, ",") != strings.Join(want, ",") {
82 t.Fatalf("eligible = %v, want %v", got, want)
83 }
84}
85
86func TestPruneEligibleProtectsTheBakeCeiling(t *testing.T) {
87 dir, s, now := pruneFixture(t, 25)
88 // Restamp the OLDEST version to 1h ago: inside the 72h ceiling, so even
89 // though it ranks outside keep-20 it must not be eligible.
90 restamp(t, dir, ManifestKey("bab", "kass", "v01"), now.Add(-time.Hour))
91 _, pubB64, _ := GenerateKey()
92 pub, _ := ParsePublicKey(pubB64, "test")
93 got, err := PruneEligible(context.Background(), s, pub, "bab", "kass", 20, now, nil)
94 if err != nil {
95 t.Fatal(err)
96 }
97 for _, v := range got {
98 if v == "v01" {
99 t.Fatal("a version inside the 72h bake ceiling was eligible")
100 }
101 }
102 // v01 now ranks NEWEST, so keep-20 shifts: v02..v06 become the eligible five.
103 want := []string{"v02", "v03", "v04", "v05", "v06"}
104 if strings.Join(got, ",") != strings.Join(want, ",") {
105 t.Fatalf("eligible = %v, want %v", got, want)
106 }
107}
108
109func TestPruneEligibleProtectsPointerVersionAndHistory(t *testing.T) {
110 _, s, now := pruneFixture(t, 25)
111 seedB64, pubB64, _ := GenerateKey()
112 priv, _ := ParseSigningKey(seedB64, "test")
113 pub, _ := ParsePublicKey(pubB64, "test")
114 ctx := context.Background()
115 // Promote v01 then v02 onto edge: pointer names v02, History carries v01.
116 if err := Promote(ctx, s, priv, "bab", "kass", "edge", "v01"); err != nil {
117 t.Fatal(err)
118 }
119 if err := Promote(ctx, s, priv, "bab", "kass", "edge", "v02"); err != nil {
120 t.Fatal(err)
121 }
122 got, err := PruneEligible(ctx, s, pub, "bab", "kass", 20, now, nil)
123 if err != nil {
124 t.Fatal(err)
125 }
126 want := []string{"v03", "v04", "v05"}
127 if strings.Join(got, ",") != strings.Join(want, ",") {
128 t.Fatalf("eligible = %v, want %v (pointer + history protected)", got, want)
129 }
130}
131
132func TestPruneEligibleProtectsCanaryBranchPointerAndHistory(t *testing.T) {
133 _, s, now := pruneFixture(t, 25)
134 seedB64, pubB64, _ := GenerateKey()
135 priv, _ := ParseSigningKey(seedB64, "test")
136 pub, _ := ParsePublicKey(pubB64, "test")
137 ctx := context.Background()
138 // Promote v01 then v02 onto the nested channel name canary/x: pointer
139 // names v02, History carries v01. Pins the TrimPrefix+CutSuffix parsing
140 // in PruneEligible against a channel name that itself contains a slash —
141 // production fleets run on canary/* channels today.
142 if err := Promote(ctx, s, priv, "bab", "kass", "canary/x", "v01"); err != nil {
143 t.Fatal(err)
144 }
145 if err := Promote(ctx, s, priv, "bab", "kass", "canary/x", "v02"); err != nil {
146 t.Fatal(err)
147 }
148 got, err := PruneEligible(ctx, s, pub, "bab", "kass", 20, now, nil)
149 if err != nil {
150 t.Fatal(err)
151 }
152 for _, v := range got {
153 if v == "v01" || v == "v02" {
154 t.Fatalf("eligible = %v, want v01 and v02 protected (canary/x pointer + history)", got)
155 }
156 }
157 want := []string{"v03", "v04", "v05"}
158 if strings.Join(got, ",") != strings.Join(want, ",") {
159 t.Fatalf("eligible = %v, want %v (canary/x pointer + history protected)", got, want)
160 }
161}
162
163func TestPruneEligibleProtectsTagReleases(t *testing.T) {
164 _, s, now := pruneFixture(t, 25)
165 _, pubB64, _ := GenerateKey()
166 pub, _ := ParsePublicKey(pubB64, "test")
167 ctx := context.Background()
168 // A tag whose Release names v03 — Tag.Release is the protected field,
169 // Tag.Version is just the semver name.
170 raw := []byte(`{"schema":1,"version":"1.2.0","release":"v03","tagged_at":"2026-08-01T00:00:00Z"}`)
171 if _, err := s.Put(ctx, TagKey("bab", "kass", "1.2.0"), raw, ""); err != nil {
172 t.Fatal(err)
173 }
174 got, err := PruneEligible(ctx, s, pub, "bab", "kass", 20, now, nil)
175 if err != nil {
176 t.Fatal(err)
177 }
178 want := []string{"v01", "v02", "v04", "v05"}
179 if strings.Join(got, ",") != strings.Join(want, ",") {
180 t.Fatalf("eligible = %v, want %v (tagged release protected)", got, want)
181 }
182}
183
184func TestPruneEligibleProtectsCallerExtras(t *testing.T) {
185 _, s, now := pruneFixture(t, 25)
186 _, pubB64, _ := GenerateKey()
187 pub, _ := ParsePublicKey(pubB64, "test")
188 got, err := PruneEligible(context.Background(), s, pub, "bab", "kass", 20, now,
189 map[string]bool{"v02": true, "v04": true})
190 if err != nil {
191 t.Fatal(err)
192 }
193 want := []string{"v01", "v03", "v05"}
194 if strings.Join(got, ",") != strings.Join(want, ",") {
195 t.Fatalf("eligible = %v, want %v (box pins protected)", got, want)
196 }
197}
198
199func TestPruneEligibleAbortsOnUnverifiablePointer(t *testing.T) {
200 _, s, now := pruneFixture(t, 25)
201 _, pubB64, _ := GenerateKey()
202 pub, _ := ParsePublicKey(pubB64, "test")
203 ctx := context.Background()
204 // Garbage where a signed pointer should be: the whole app's prune must
205 // abort — never "prune what we could verify".
206 if _, err := s.Put(ctx, ChannelKey("bab", "kass", "edge"), []byte(`{"not":"signed"}`), ""); err != nil {
207 t.Fatal(err)
208 }
209 if _, err := PruneEligible(ctx, s, pub, "bab", "kass", 20, now, nil); err == nil {
210 t.Fatal("an unverifiable pointer did not abort the prune")
211 }
212}
213
214func TestPruneEligibleFailClosedGates(t *testing.T) {
215 _, s, now := pruneFixture(t, 5)
216 _, pubB64, _ := GenerateKey()
217 pub, _ := ParsePublicKey(pubB64, "test")
218 ctx := context.Background()
219 if _, err := PruneEligible(ctx, s, nil, "bab", "kass", 20, now, nil); err == nil {
220 t.Fatal("nil pubkey accepted")
221 }
222 if _, err := PruneEligible(ctx, s, pub, "bab", "kass", KeepFloor-1, now, nil); err == nil {
223 t.Fatal("keep below floor accepted")
224 }
225 if _, err := PruneEligible(ctx, plainStore{s}, pub, "bab", "kass", 20, now, nil); err == nil {
226 t.Fatal("a store that cannot rank by age was accepted")
227 }
228}
229
230func TestPruneEligibleManifestlessAgedShipIsEligible(t *testing.T) {
231 dir, s, now := pruneFixture(t, 25)
232 // An aborted ship (artifact only, no manifest), OLDER than every shipped
233 // version (the fixture's stamps start 30 days back): junk, outside
234 // keep-N, eligible. (Stamped newer it would occupy a keep slot instead —
235 // aborted ships surface in listings by design, PR #134.)
236 modStamp(t, s, dir, ArtifactKey("bab", "kass", "dead1", "linux-arm64"), now.Add(-31*24*time.Hour))
237 _, pubB64, _ := GenerateKey()
238 pub, _ := ParsePublicKey(pubB64, "test")
239 got, err := PruneEligible(context.Background(), s, pub, "bab", "kass", 20, now, nil)
240 if err != nil {
241 t.Fatal(err)
242 }
243 found := false
244 for _, v := range got {
245 found = found || v == "dead1"
246 }
247 if !found {
248 t.Fatalf("eligible = %v, want dead1 included (aged aborted ship)", got)
249 }
250}
251
252// deleteRecorder pins the manifest-first rule: the order of Deletes is the
253// difference between a half-deleted version that reads as an aborted ship
254// and one that reads as a complete version with missing artifacts.
255type deleteRecorder struct {
256 Store
257 order []string
258}
259
260func (d *deleteRecorder) Delete(ctx context.Context, key string) error {
261 d.order = append(d.order, key)
262 return d.Store.Delete(ctx, key)
263}
264
265func TestDeleteVersionManifestFirstThenIdempotent(t *testing.T) {
266 dir, s, _ := pruneFixture(t, 1)
267 _ = dir
268 rec := &deleteRecorder{Store: s}
269 ctx := context.Background()
270 if err := DeleteVersion(ctx, rec, "bab", "kass", "v01"); err != nil {
271 t.Fatal(err)
272 }
273 if len(rec.order) < 2 || rec.order[0] != ManifestKey("bab", "kass", "v01") {
274 t.Fatalf("delete order = %v, want manifest first", rec.order)
275 }
276 keys, err := s.List(ctx, ReleasesPrefix("bab", "kass")+"v01/")
277 if err != nil {
278 t.Fatal(err)
279 }
280 if len(keys) != 0 {
281 t.Fatalf("v01 keys after delete = %v, want none", keys)
282 }
283 // Deleting again is a no-op, not an error.
284 if err := DeleteVersion(ctx, s, "bab", "kass", "v01"); err != nil {
285 t.Fatal(err)
286 }
287}
288