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 "bytes"
5 "context"
6 "crypto/ed25519"
7 "encoding/base64"
8 "encoding/json"
9 "errors"
10 "fmt"
11 "os"
12 "strings"
13 "testing"
14 "time"
15
16 "github.com/carlosframework/platform/internal/caps"
17)
18
19// testKeys mints a signing pair through the exported key helpers, so the
20// round trip seed -> base64 -> key is exercised too.
21func testKeys(t *testing.T) (ed25519.PrivateKey, ed25519.PublicKey) {
22 t.Helper()
23 seedB64, pubB64, err := GenerateKey()
24 if err != nil {
25 t.Fatalf("GenerateKey: %v", err)
26 }
27 priv, err := ParseSigningKey(seedB64, "test")
28 if err != nil {
29 t.Fatalf("ParseSigningKey: %v", err)
30 }
31 pub, err := ParsePublicKey(pubB64, "test")
32 if err != nil {
33 t.Fatalf("ParsePublicKey: %v", err)
34 }
35 return priv, pub
36}
37
38// shipRelease ships a one-artifact binary release keyed by plat.
39func shipRelease(t *testing.T, s Store, acct, app, version, plat string, body []byte) {
40 t.Helper()
41 m := Manifest{
42 App: app,
43 Version: version,
44 Kind: "binary",
45 Artifacts: map[string]Artifact{plat: {}},
46 }
47 if err := Ship(context.Background(), s, acct, m, map[string][]byte{plat: body}); err != nil {
48 t.Fatalf("Ship %s/%s: %v", app, version, err)
49 }
50}
51
52func TestShipThenPromoteThenChannelRoundTrip(t *testing.T) {
53 s := NewDirStore(t.TempDir())
54 ctx := context.Background()
55 priv, pub := testKeys(t)
56
57 shipRelease(t, s, "bab", "myapp", "v1abc", "linux-amd64", []byte("binary bytes v1"))
58 if err := Promote(ctx, s, priv, "bab", "myapp", "canary/x", "v1abc"); err != nil {
59 t.Fatalf("Promote: %v", err)
60 }
61 ptr, err := Channel(ctx, s, pub, "bab", "myapp", "canary/x")
62 if err != nil {
63 t.Fatalf("Channel: %v", err)
64 }
65 if ptr.App != "myapp" || ptr.Channel != "canary/x" || ptr.Version != "v1abc" {
66 t.Fatalf("pointer = %+v", ptr)
67 }
68 if ptr.PromotedAt.IsZero() || len(ptr.History) != 0 {
69 t.Fatalf("pointer = %+v, want stamped PromotedAt and empty history", ptr)
70 }
71 // The manifest round-trips with the sha256 Ship computed.
72 m, err := GetManifest(ctx, s, "bab", "myapp", "v1abc")
73 if err != nil {
74 t.Fatalf("GetManifest: %v", err)
75 }
76 art := m.Artifacts["linux-amd64"]
77 if art.SHA256 != etagOf([]byte("binary bytes v1")) || art.Size != int64(len("binary bytes v1")) {
78 t.Fatalf("artifact = %+v", art)
79 }
80 body, _, err := s.Get(ctx, ArtifactKey("bab", "myapp", "v1abc", art.File))
81 if err != nil || string(body) != "binary bytes v1" {
82 t.Fatalf("artifact bytes = %q, %v", body, err)
83 }
84}
85
86func TestShipImmutable(t *testing.T) {
87 s := NewDirStore(t.TempDir())
88 // Re-shipping identical bytes is an idempotent no-op...
89 shipRelease(t, s, "bab", "myapp", "v1abc", "linux-amd64", []byte("same"))
90 shipRelease(t, s, "bab", "myapp", "v1abc", "linux-amd64", []byte("same"))
91 // ...but the same version with DIFFERENT content is refused.
92 m := Manifest{App: "myapp", Version: "v1abc", Kind: "binary",
93 Artifacts: map[string]Artifact{"linux-amd64": {}}}
94 err := Ship(context.Background(), s, "bab", m, map[string][]byte{"linux-amd64": []byte("different")})
95 if err == nil {
96 t.Fatalf("Ship accepted different content under an existing version")
97 }
98}
99
100func TestPromoteRequiresShippedVersion(t *testing.T) {
101 s := NewDirStore(t.TempDir())
102 priv, _ := testKeys(t)
103 err := Promote(context.Background(), s, priv, "bab", "myapp", "stable", "ghost1")
104 if err == nil || !errors.Is(err, os.ErrNotExist) {
105 t.Fatalf("Promote of unshipped version: err = %v, want os.ErrNotExist-wrapped", err)
106 }
107}
108
109func TestTamperedPointerRejected(t *testing.T) {
110 s := NewDirStore(t.TempDir())
111 ctx := context.Background()
112 priv, pub := testKeys(t)
113 shipRelease(t, s, "bab", "myapp", "v1abc", "linux-amd64", []byte("bin"))
114 if err := Promote(ctx, s, priv, "bab", "myapp", "stable", "v1abc"); err != nil {
115 t.Fatalf("Promote: %v", err)
116 }
117
118 key := ChannelKey("bab", "myapp", "stable")
119 raw, etag, err := s.Get(ctx, key)
120 if err != nil {
121 t.Fatalf("Get pointer: %v", err)
122 }
123 var sw signedWrapper
124 if err := json.Unmarshal(raw, &sw); err != nil {
125 t.Fatalf("unmarshal wrapper: %v", err)
126 }
127
128 // Flip a byte in the signature.
129 sig := []byte(sw.Sig)
130 if sig[10] == 'A' {
131 sig[10] = 'B'
132 } else {
133 sig[10] = 'A'
134 }
135 tampered, _ := json.Marshal(signedWrapper{Payload: sw.Payload, Sig: string(sig)})
136 if _, err := s.Put(ctx, key, tampered, etag); err != nil {
137 t.Fatalf("plant tampered pointer: %v", err)
138 }
139 if _, err := Channel(ctx, s, pub, "bab", "myapp", "stable"); err == nil {
140 t.Fatalf("Channel accepted a pointer with a flipped signature byte")
141 }
142
143 // Flip a byte in the payload instead (signature intact) — the signature
144 // covers the exact base64 payload bytes, so this must fail too.
145 payload := []byte(sw.Payload)
146 if payload[10] == 'A' {
147 payload[10] = 'B'
148 } else {
149 payload[10] = 'A'
150 }
151 raw2, etag2, _ := s.Get(ctx, key)
152 _ = raw2
153 tampered2, _ := json.Marshal(signedWrapper{Payload: string(payload), Sig: sw.Sig})
154 if _, err := s.Put(ctx, key, tampered2, etag2); err != nil {
155 t.Fatalf("plant tampered payload: %v", err)
156 }
157 if _, err := Channel(ctx, s, pub, "bab", "myapp", "stable"); err == nil {
158 t.Fatalf("Channel accepted a pointer with a flipped payload byte")
159 }
160
161 // And a validly-signed pointer copied from ANOTHER channel's key is a
162 // cross-channel replay — rejected because app/channel live under the
163 // signature.
164 if err := Promote(ctx, s, priv, "bab", "myapp", "canary/evil", "v1abc"); err != nil {
165 t.Fatalf("Promote canary: %v", err)
166 }
167 canaryRaw, _, _ := s.Get(ctx, ChannelKey("bab", "myapp", "canary/evil"))
168 _, curEtag, _ := s.Get(ctx, key)
169 if _, err := s.Put(ctx, key, canaryRaw, curEtag); err != nil {
170 t.Fatalf("plant replayed pointer: %v", err)
171 }
172 if _, err := Channel(ctx, s, pub, "bab", "myapp", "stable"); err == nil {
173 t.Fatalf("Channel accepted a pointer replayed from another channel")
174 }
175}
176
177func TestPromoteHistoryCapTen(t *testing.T) {
178 s := NewDirStore(t.TempDir())
179 ctx := context.Background()
180 priv, pub := testKeys(t)
181 for i := 1; i <= 12; i++ {
182 v := fmt.Sprintf("v%02d", i)
183 shipRelease(t, s, "bab", "myapp", v, "linux-amd64", []byte("bin "+v))
184 if err := Promote(ctx, s, priv, "bab", "myapp", "stable", v); err != nil {
185 t.Fatalf("Promote %s: %v", v, err)
186 }
187 }
188 ptr, err := Channel(ctx, s, pub, "bab", "myapp", "stable")
189 if err != nil {
190 t.Fatalf("Channel: %v", err)
191 }
192 if ptr.Version != "v12" {
193 t.Fatalf("Version = %s, want v12", ptr.Version)
194 }
195 if len(ptr.History) != 10 {
196 t.Fatalf("History has %d entries, want 10: %v", len(ptr.History), ptr.History)
197 }
198 if ptr.History[0] != "v11" || ptr.History[9] != "v02" {
199 t.Fatalf("History = %v, want newest-first v11..v02", ptr.History)
200 }
201}
202
203func TestRollbackSwapsAndReturns(t *testing.T) {
204 s := NewDirStore(t.TempDir())
205 ctx := context.Background()
206 priv, pub := testKeys(t)
207 shipRelease(t, s, "bab", "myapp", "v1", "linux-amd64", []byte("one"))
208 shipRelease(t, s, "bab", "myapp", "v2", "linux-amd64", []byte("two"))
209 if err := Promote(ctx, s, priv, "bab", "myapp", "stable", "v1"); err != nil {
210 t.Fatalf("Promote v1: %v", err)
211 }
212
213 // Nothing to roll back to yet.
214 if err := Rollback(ctx, s, priv, "bab", "myapp", "stable"); err == nil {
215 t.Fatalf("Rollback with empty history succeeded")
216 }
217
218 if err := Promote(ctx, s, priv, "bab", "myapp", "stable", "v2"); err != nil {
219 t.Fatalf("Promote v2: %v", err)
220 }
221 if err := Rollback(ctx, s, priv, "bab", "myapp", "stable"); err != nil {
222 t.Fatalf("Rollback: %v", err)
223 }
224 ptr, err := Channel(ctx, s, pub, "bab", "myapp", "stable")
225 if err != nil {
226 t.Fatalf("Channel: %v", err)
227 }
228 if ptr.Version != "v1" || len(ptr.History) != 1 || ptr.History[0] != "v2" {
229 t.Fatalf("after rollback: %+v, want v1 with history [v2]", ptr)
230 }
231 // A second rollback returns to where you were (the swap semantics).
232 if err := Rollback(ctx, s, priv, "bab", "myapp", "stable"); err != nil {
233 t.Fatalf("second Rollback: %v", err)
234 }
235 ptr, _ = Channel(ctx, s, pub, "bab", "myapp", "stable")
236 if ptr.Version != "v2" || len(ptr.History) != 1 || ptr.History[0] != "v1" {
237 t.Fatalf("after second rollback: %+v, want v2 with history [v1]", ptr)
238 }
239}
240
241func TestChannelWrongKeyRejected(t *testing.T) {
242 s := NewDirStore(t.TempDir())
243 ctx := context.Background()
244 priv, _ := testKeys(t)
245 _, otherPub := testKeys(t)
246 shipRelease(t, s, "bab", "myapp", "v1", "linux-amd64", []byte("one"))
247 if err := Promote(ctx, s, priv, "bab", "myapp", "stable", "v1"); err != nil {
248 t.Fatalf("Promote: %v", err)
249 }
250 if _, err := Channel(ctx, s, otherPub, "bab", "myapp", "stable"); err == nil {
251 t.Fatalf("Channel verified against the wrong public key")
252 }
253}
254
255func TestValidChannel(t *testing.T) {
256 for c, want := range map[string]bool{
257 "stable": true, "beta": true, "canary/feature-x": true,
258 "canary/..": false, "a/b/c": false, "": false, "sta ble": false,
259 } {
260 if got := ValidChannel(c); got != want {
261 t.Errorf("ValidChannel(%q) = %v, want %v", c, got, want)
262 }
263 }
264}
265
266// Guard against the PromotedAt stamp being mistaken for a trust input
267// anywhere: the bake clock must run off the box's own sightings, and the
268// stamp is informational. This just pins that signPointer stamps it.
269func TestSignStampsPromotedAt(t *testing.T) {
270 priv, pub := testKeys(t)
271 p := &ChannelPointer{Account: "bab", App: "myapp", Channel: "stable", Version: "v1"}
272 before := time.Now().Add(-time.Second)
273 raw, err := signPointer(RootSigner(priv), p)
274 if err != nil {
275 t.Fatalf("signPointer: %v", err)
276 }
277 got, err := verifyPointer(pub, raw, "bab", "myapp", "stable")
278 if err != nil {
279 t.Fatalf("verifyPointer: %v", err)
280 }
281 if got.PromotedAt.Before(before) {
282 t.Fatalf("PromotedAt = %v, want stamped at signing time", got.PromotedAt)
283 }
284}
285
286// The ladder, end to end through the store: a build walks edge -> beta ->
287// stable, a skip is refused with the bypass named, -hotfix places it anyway,
288// canary is never refused, and repeating a promotion is a no-op success.
289func TestPromoteOnLadderEnforcesOrdering(t *testing.T) {
290 s := NewDirStore(t.TempDir())
291 ctx := context.Background()
292 priv, pub := testKeys(t)
293 shipRelease(t, s, "bab", "myapp", "v1", "linux-arm64", []byte("bin v1"))
294 shipRelease(t, s, "bab", "myapp", "v2", "linux-arm64", []byte("bin v2"))
295
296 promote := func(version, channel string, opt PromoteOptions) (PromoteResult, error) {
297 return PromoteOnLadder(ctx, s, priv, "bab", "myapp", channel, version, opt)
298 }
299
300 // A fresh build enters at edge, then climbs one rung at a time.
301 if _, err := promote("v1", "edge", PromoteOptions{}); err != nil {
302 t.Fatalf("v1 -> edge: %v", err)
303 }
304 res, err := promote("v1", "beta", PromoteOptions{})
305 if err != nil {
306 t.Fatalf("v1 edge -> beta: %v", err)
307 }
308 if res.From != RungEdge || res.To != RungBeta || res.NoOp {
309 t.Fatalf("v1 edge -> beta: result = %+v", res)
310 }
311 if _, err := promote("v1", "stable", PromoteOptions{}); err != nil {
312 t.Fatalf("v1 beta -> stable: %v", err)
313 }
314 if p, err := Channel(ctx, s, pub, "bab", "myapp", "stable"); err != nil || p.Version != "v1" {
315 t.Fatalf("stable = %+v, %v; want v1", p, err)
316 }
317
318 // A build that has been nowhere cannot land on stable, and the refusal
319 // says how to override it.
320 _, err = promote("v2", "stable", PromoteOptions{})
321 if err == nil {
322 t.Fatalf("v2 (on no rung) -> stable was allowed")
323 }
324 if !strings.Contains(err.Error(), "-hotfix") {
325 t.Fatalf("refusal = %q, want it to name --hotfix", err)
326 }
327 if p, _ := Channel(ctx, s, pub, "bab", "myapp", "stable"); p.Version != "v1" {
328 t.Fatalf("stable moved to %s on a refused promotion", p.Version)
329 }
330 // The skip is refused too — that is what the ladder is for.
331 if _, err := promote("v2", "edge", PromoteOptions{}); err != nil {
332 t.Fatalf("v2 -> edge: %v", err)
333 }
334 if _, err := promote("v2", "stable", PromoteOptions{}); err == nil {
335 t.Fatalf("v2 edge -> stable (a skip) was allowed")
336 }
337
338 // -hotfix places it anyway, and says so.
339 res, err = promote("v2", "stable", PromoteOptions{Hotfix: true})
340 if err != nil {
341 t.Fatalf("v2 -> stable --hotfix: %v", err)
342 }
343 if !res.Hotfix || res.To != RungStable {
344 t.Fatalf("hotfix result = %+v", res)
345 }
346 if p, _ := Channel(ctx, s, pub, "bab", "myapp", "stable"); p.Version != "v2" {
347 t.Fatalf("stable = %s after hotfix, want v2", p.Version)
348 }
349
350 // Repeating a promotion is a no-op success, never an error — even though
351 // v1 now sits on beta while stable has moved past it.
352 res, err = promote("v1", "beta", PromoteOptions{})
353 if err != nil {
354 t.Fatalf("re-promoting v1 to the beta it already holds: %v", err)
355 }
356 if !res.NoOp {
357 t.Fatalf("re-promotion result = %+v, want NoOp", res)
358 }
359}
360
361// Production runs on canary/* channels right now: ordering must never refuse
362// one, whatever the build is already on.
363func TestPromoteOnLadderNeverRefusesCanary(t *testing.T) {
364 s := NewDirStore(t.TempDir())
365 ctx := context.Background()
366 priv, pub := testKeys(t)
367 shipRelease(t, s, "bab", "myapp", "v1", "linux-arm64", []byte("bin v1"))
368
369 for _, channel := range []string{"canary/keymail", "canary/platform-adoption", "canary/rehearsal"} {
370 if _, err := PromoteOnLadder(ctx, s, priv, "bab", "myapp", channel, "v1", PromoteOptions{}); err != nil {
371 t.Fatalf("promote v1 -> %s: %v", channel, err)
372 }
373 if p, err := Channel(ctx, s, pub, "bab", "myapp", channel); err != nil || p.Version != "v1" {
374 t.Fatalf("%s = %+v, %v", channel, p, err)
375 }
376 }
377 // ...including a build that has already reached stable.
378 if _, err := PromoteOnLadder(ctx, s, priv, "bab", "myapp", "edge", "v1", PromoteOptions{Hotfix: true}); err != nil {
379 t.Fatalf("hotfix v1 -> edge: %v", err)
380 }
381 if _, err := PromoteOnLadder(ctx, s, priv, "bab", "myapp", "canary/late", "v1", PromoteOptions{}); err != nil {
382 t.Fatalf("promote a build that is on edge back onto a canary: %v", err)
383 }
384}
385
386// VersionRung answers "how mature is this build" from the channel pointers
387// that exist, and reports the HIGHEST rung when several point at it.
388func TestVersionRungReportsHighestRung(t *testing.T) {
389 s := NewDirStore(t.TempDir())
390 ctx := context.Background()
391 priv, pub := testKeys(t)
392 shipRelease(t, s, "bab", "myapp", "v1", "linux-arm64", []byte("bin v1"))
393
394 if rung, err := VersionRung(ctx, s, pub, "bab", "myapp", "v1"); err != nil || rung != "" {
395 t.Fatalf("VersionRung with no pointers = %q, %v; want \"\"", rung, err)
396 }
397 if err := Promote(ctx, s, priv, "bab", "myapp", "canary/x", "v1"); err != nil {
398 t.Fatalf("Promote: %v", err)
399 }
400 if rung, err := VersionRung(ctx, s, pub, "bab", "myapp", "v1"); err != nil || rung != RungCanary {
401 t.Fatalf("VersionRung on canary = %q, %v", rung, err)
402 }
403 if err := Promote(ctx, s, priv, "bab", "myapp", "beta", "v1"); err != nil {
404 t.Fatalf("Promote: %v", err)
405 }
406 if rung, err := VersionRung(ctx, s, pub, "bab", "myapp", "v1"); err != nil || rung != RungBeta {
407 t.Fatalf("VersionRung on canary+beta = %q, %v; want the highest", rung, err)
408 }
409 // A channel that is not a rung says nothing about maturity.
410 shipRelease(t, s, "bab", "myapp", "v2", "linux-arm64", []byte("bin v2"))
411 if err := Promote(ctx, s, priv, "bab", "myapp", "wip", "v2"); err != nil {
412 t.Fatalf("Promote: %v", err)
413 }
414 if rung, err := VersionRung(ctx, s, pub, "bab", "myapp", "v2"); err != nil || rung != "" {
415 t.Fatalf("VersionRung of a build on a non-rung channel = %q, %v; want \"\"", rung, err)
416 }
417}
418
419// Every promotion is recorded on the pointer it moves: what moved, from which
420// rung to which, who did it, whether it bypassed the ladder, and when.
421func TestPromotionRecordsAnAuditEntry(t *testing.T) {
422 s := NewDirStore(t.TempDir())
423 ctx := context.Background()
424 priv, pub := testKeys(t)
425 shipRelease(t, s, "bab", "myapp", "v1", "linux-arm64", []byte("bin v1"))
426 shipRelease(t, s, "bab", "myapp", "v2", "linux-arm64", []byte("bin v2"))
427
428 if _, err := PromoteOnLadder(ctx, s, priv, "bab", "myapp", "edge", "v1",
429 PromoteOptions{Actor: "paul@keymail.dev"}); err != nil {
430 t.Fatalf("v1 -> edge: %v", err)
431 }
432 if _, err := PromoteOnLadder(ctx, s, priv, "bab", "myapp", "beta", "v1",
433 PromoteOptions{Actor: "paul@keymail.dev"}); err != nil {
434 t.Fatalf("v1 -> beta: %v", err)
435 }
436 p, err := Channel(ctx, s, pub, "bab", "myapp", "beta")
437 if err != nil {
438 t.Fatalf("Channel beta: %v", err)
439 }
440 if len(p.Audit) != 1 {
441 t.Fatalf("beta audit = %+v, want one entry", p.Audit)
442 }
443 got := p.Audit[0]
444 if got.Version != "v1" || got.From != RungEdge || got.To != RungBeta ||
445 got.Actor != "paul@keymail.dev" || got.Hotfix || got.At.IsZero() {
446 t.Fatalf("audit entry = %+v", got)
447 }
448
449 // A hotfix says so, and the trail keeps what came before it, newest first.
450 if _, err := PromoteOnLadder(ctx, s, priv, "bab", "myapp", "beta", "v2",
451 PromoteOptions{Actor: "james@keymail.dev", Hotfix: true}); err != nil {
452 t.Fatalf("v2 -> beta --hotfix: %v", err)
453 }
454 p, err = Channel(ctx, s, pub, "bab", "myapp", "beta")
455 if err != nil {
456 t.Fatalf("Channel beta: %v", err)
457 }
458 if len(p.Audit) != 2 {
459 t.Fatalf("beta audit = %+v, want two entries", p.Audit)
460 }
461 if !p.Audit[0].Hotfix || p.Audit[0].Version != "v2" || p.Audit[0].Actor != "james@keymail.dev" {
462 t.Fatalf("newest audit entry = %+v, want the hotfix", p.Audit[0])
463 }
464 if p.Audit[1].Version != "v1" || p.Audit[1].Hotfix {
465 t.Fatalf("older audit entry = %+v, want v1's ordinary promotion", p.Audit[1])
466 }
467 // A no-op repeat records nothing: nothing happened.
468 if _, err := PromoteOnLadder(ctx, s, priv, "bab", "myapp", "beta", "v2",
469 PromoteOptions{Actor: "james@keymail.dev"}); err != nil {
470 t.Fatalf("repeat promotion: %v", err)
471 }
472 if p, _ := Channel(ctx, s, pub, "bab", "myapp", "beta"); len(p.Audit) != 2 {
473 t.Fatalf("a no-op repeat appended an audit entry: %+v", p.Audit)
474 }
475 // Rollback is not a promotion, but it must not lose the trail either.
476 if err := Rollback(ctx, s, priv, "bab", "myapp", "beta"); err != nil {
477 t.Fatalf("Rollback: %v", err)
478 }
479 if p, _ := Channel(ctx, s, pub, "bab", "myapp", "beta"); len(p.Audit) != 2 {
480 t.Fatalf("rollback dropped the audit trail: %+v", p.Audit)
481 }
482}
483
484// The audit trail is additive: pointers written before it existed — and there
485// are live ones in production buckets — must still verify and parse, with no
486// audit data and no error. This is the exact payload the pre-audit signPointer
487// produced, signed the same way.
488func TestPointerWrittenBeforeAuditStillParses(t *testing.T) {
489 s := NewDirStore(t.TempDir())
490 ctx := context.Background()
491 priv, pub := testKeys(t)
492 shipRelease(t, s, "bab", "myapp", "v1", "linux-arm64", []byte("bin v1"))
493
494 old := `{
495 "schema": 1,
496 "app": "myapp",
497 "channel": "beta",
498 "version": "v1",
499 "promoted_at": "2026-07-01T10:00:00Z",
500 "history": [
501 "v0"
502 ]
503}`
504 b64 := base64.StdEncoding.EncodeToString([]byte(old))
505 wrapper, err := json.MarshalIndent(signedWrapper{
506 Payload: b64,
507 Sig: base64.StdEncoding.EncodeToString(ed25519.Sign(priv, []byte(b64))),
508 }, "", " ")
509 if err != nil {
510 t.Fatalf("marshal wrapper: %v", err)
511 }
512 if _, err := s.Put(ctx, ChannelKey("bab", "myapp", "beta"), wrapper, ""); err != nil {
513 t.Fatalf("plant pre-audit pointer: %v", err)
514 }
515
516 p, err := Channel(ctx, s, pub, "bab", "myapp", "beta")
517 if err != nil {
518 t.Fatalf("Channel on a pre-audit pointer: %v", err)
519 }
520 if p.Version != "v1" || len(p.History) != 1 || p.History[0] != "v0" {
521 t.Fatalf("pre-audit pointer = %+v", p)
522 }
523 if p.Audit != nil {
524 t.Fatalf("pre-audit pointer has audit data: %+v", p.Audit)
525 }
526 // It still answers "which rung is this build on", and still promotes —
527 // the trail simply starts at the next promotion.
528 if rung, err := VersionRung(ctx, s, pub, "bab", "myapp", "v1"); err != nil || rung != RungBeta {
529 t.Fatalf("VersionRung on a pre-audit pointer = %q, %v", rung, err)
530 }
531 if _, err := PromoteOnLadder(ctx, s, priv, "bab", "myapp", "stable", "v1",
532 PromoteOptions{Actor: "paul@keymail.dev"}); err != nil {
533 t.Fatalf("promote a pre-audit build: %v", err)
534 }
535 sp, err := Channel(ctx, s, pub, "bab", "myapp", "stable")
536 if err != nil {
537 t.Fatalf("Channel stable: %v", err)
538 }
539 if len(sp.Audit) != 1 || sp.Audit[0].From != RungBeta || sp.Audit[0].To != RungStable {
540 t.Fatalf("stable audit = %+v", sp.Audit)
541 }
542}
543
544// TestShipRefusesAnUnknownCapability: validateManifest's capability gate runs
545// at SHIP, where a human is standing at a terminal to read the refusal — not
546// only on the box hours later. The refusal has to name the offending value,
547// because "invalid capabilities" tells an operator nothing about which line
548// of their block to fix.
549func TestShipRefusesAnUnknownCapability(t *testing.T) {
550 s := NewDirStore(t.TempDir())
551 m := Manifest{
552 App: "myapp",
553 Version: "v1abc",
554 Kind: "binary",
555 Artifacts: map[string]Artifact{"linux-arm64": {}},
556 // 8080 is not a capability: only privileged ports are, and a high
557 // port needs no help from the platform.
558 Capabilities: &caps.Set{V: 1, Ports: []caps.Port{{Port: 8080, Name: "http"}}},
559 }
560 err := Ship(context.Background(), s, "bab", m, map[string][]byte{"linux-arm64": []byte("bin")})
561 if err == nil {
562 t.Fatal("Ship accepted a capability block the box would refuse — the ship and adopt gates have diverged")
563 }
564 if !strings.Contains(err.Error(), "8080") {
565 t.Fatalf("refusal %q does not name the offending port", err)
566 }
567 // And nothing was written: the gate runs before a single byte moves.
568 if _, err := GetManifest(context.Background(), s, "bab", "myapp", "v1abc"); err == nil {
569 t.Fatal("a refused ship still wrote a manifest")
570 }
571}
572
573// TestShipRefusesAnUnsupportedCapabilityVersion pins the other half of §3b's
574// fail-closed direction at ship time: a block this binary does not speak is
575// refused with a message naming the fix, rather than shipped for a box to
576// choke on later.
577func TestShipRefusesAnUnsupportedCapabilityVersion(t *testing.T) {
578 s := NewDirStore(t.TempDir())
579 m := Manifest{
580 App: "myapp",
581 Version: "v1abc",
582 Kind: "binary",
583 Artifacts: map[string]Artifact{"linux-arm64": {}},
584 Capabilities: &caps.Set{V: caps.Version + 1, Daemon: true},
585 }
586 err := Ship(context.Background(), s, "bab", m, map[string][]byte{"linux-arm64": []byte("bin")})
587 if err == nil {
588 t.Fatal("Ship accepted a capability block version this binary does not speak")
589 }
590 if !strings.Contains(err.Error(), "update the platform binary") {
591 t.Fatalf("refusal %q does not name the fix", err)
592 }
593}
594
595// TestShipRoundTripsACapabilityBlock: a valid declaration survives the whole
596// immutable-manifest round trip — marshalled by Ship, re-decoded through
597// caps.Set's strict door by GetManifest — with every field intact. This is
598// the trust chain the design turns on: the declaration rides the same signed
599// pointer -> immutable manifest -> sha256 path as the bytes it describes.
600func TestShipRoundTripsACapabilityBlock(t *testing.T) {
601 s := NewDirStore(t.TempDir())
602 ctx := context.Background()
603 want := &caps.Set{
604 V: 1,
605 Command: "relay",
606 Daemon: true,
607 Children: true,
608 RuntimeDir: "keymail",
609 Ports: []caps.Port{{Port: 25, Name: "mx"}},
610 Data: &caps.Data{Replicate: caps.DataReplicateApp},
611 HostRole: true,
612 }
613 m := Manifest{
614 App: "keymail", Version: "v1abc", Kind: "binary",
615 Artifacts: map[string]Artifact{"linux-arm64": {}},
616 Capabilities: want,
617 }
618 if err := Ship(ctx, s, "bab", m, map[string][]byte{"linux-arm64": []byte("bin")}); err != nil {
619 t.Fatalf("Ship: %v", err)
620 }
621 got, err := GetManifest(ctx, s, "bab", "keymail", "v1abc")
622 if err != nil {
623 t.Fatalf("GetManifest: %v", err)
624 }
625 if got.Capabilities == nil {
626 t.Fatal("the capability block did not survive the round trip")
627 }
628 // Compared through the canonical JSON encoding, not reflect.DeepEqual:
629 // caps.Set carries an unexported notes slice, so a decoded Set is not
630 // DeepEqual-safe against a literal once those notes are non-empty.
631 a, _ := json.Marshal(want)
632 b, _ := json.Marshal(got.Capabilities)
633 if string(a) != string(b) {
634 t.Fatalf("capabilities round-tripped as %s, want %s", b, a)
635 }
636}
637
638// Fast-track (2026-08-04 console bake controls spec §5): re-sign the head
639// with the bake waiver — same version, same history, one new audit entry —
640// and never from a scoped key, at mint or at verification.
641func TestFastTrackWaivesTheHeadInPlace(t *testing.T) {
642 s := NewDirStore(t.TempDir())
643 ctx := context.Background()
644 priv, pub := testKeys(t)
645 shipRelease(t, s, "bab", "kass", "v1", "linux-amd64", []byte("b1"))
646 shipRelease(t, s, "bab", "kass", "v2", "linux-amd64", []byte("b2"))
647 if err := Promote(ctx, s, priv, "bab", "kass", "stable", "v1"); err != nil {
648 t.Fatal(err)
649 }
650 if err := Promote(ctx, s, priv, "bab", "kass", "stable", "v2"); err != nil {
651 t.Fatal(err)
652 }
653
654 ver, err := FastTrack(ctx, s, priv, "bab", "kass", "stable", "paul@keymail.dev")
655 if err != nil {
656 t.Fatalf("FastTrack: %v", err)
657 }
658 if ver != "v2" {
659 t.Fatalf("FastTrack waived %q, want the head v2", ver)
660 }
661 p, err := Channel(ctx, s, pub, "bab", "kass", "stable")
662 if err != nil {
663 t.Fatal(err)
664 }
665 if !p.FastTrack || p.Version != "v2" {
666 t.Fatalf("pointer = version %s fast_track %v, want v2 waived", p.Version, p.FastTrack)
667 }
668 // History survives — a fast-tracked head must stay rollbackable.
669 if len(p.History) == 0 || p.History[0] != "v1" {
670 t.Fatalf("history = %v, want v1 preserved", p.History)
671 }
672 // The waiver is recorded on the trail it waives.
673 if len(p.Audit) == 0 || !p.Audit[0].FastTrack || p.Audit[0].Actor != "paul@keymail.dev" {
674 t.Fatalf("audit[0] = %+v, want a fast_track entry naming the actor", p.Audit)
675 }
676 // Idempotent: a second fast-track is a no-op success.
677 if _, err := FastTrack(ctx, s, priv, "bab", "kass", "stable", "paul@keymail.dev"); err != nil {
678 t.Fatalf("repeated FastTrack: %v", err)
679 }
680 // The NEXT promotion starts clean — fast_track is per write, never
681 // inherited (spec §5).
682 shipRelease(t, s, "bab", "kass", "v3", "linux-amd64", []byte("b3"))
683 if err := Promote(ctx, s, priv, "bab", "kass", "stable", "v3"); err != nil {
684 t.Fatal(err)
685 }
686 if p, _ := Channel(ctx, s, pub, "bab", "kass", "stable"); p.FastTrack {
687 t.Fatal("fast_track survived onto the next promotion")
688 }
689}
690
691// Root only, both directions (mint AND verification, like the grant cap):
692// a scoped signer cannot SIGN a fast-tracked pointer, and a hand-built
693// grant-bearing wrapper whose payload carries fast_track does not VERIFY.
694func TestFastTrackIsRootOnly(t *testing.T) {
695 s := NewDirStore(t.TempDir())
696 ctx := context.Background()
697 priv, pub := testKeys(t)
698 shipRelease(t, s, "bab", "kass", "v1", "linux-amd64", []byte("b1"))
699 if err := Promote(ctx, s, priv, "bab", "kass", "edge", "v1"); err != nil {
700 t.Fatal(err)
701 }
702 seed, grantB64, err := MintKeyGrant(priv, "builder", []string{RungEdge}, maxGrantTTL)
703 if err != nil {
704 t.Fatal(err)
705 }
706 key, err := ParseSigningKey(seed, "test")
707 if err != nil {
708 t.Fatal(err)
709 }
710 scoped, err := ScopedSigner(key, grantB64, pub)
711 if err != nil {
712 t.Fatal(err)
713 }
714 // Mint side: the scoped signer refuses the payload.
715 if _, err := signPointer(scoped, &ChannelPointer{
716 Account: "bab", App: "kass", Channel: "edge", Version: "v1", FastTrack: true,
717 }); err == nil || !strings.Contains(err.Error(), "root") {
718 t.Fatalf("a scoped signer signed a fast-tracked pointer (err=%v)", err)
719 }
720 // Verify side: build the wrapper by hand, exactly as a patched-out mint
721 // check would, and watch verification refuse it.
722 payload, _ := json.Marshal(ChannelPointer{Schema: 1, App: "kass", Channel: "edge",
723 Version: "v1", FastTrack: true, PromotedAt: timeNowTestHelper()})
724 b64 := base64.StdEncoding.EncodeToString(payload)
725 sg, err := parseGrantBlob(grantB64, "test")
726 if err != nil {
727 t.Fatal(err)
728 }
729 wrapper, _ := json.Marshal(signedWrapper{
730 Payload: b64,
731 Sig: base64.StdEncoding.EncodeToString(ed25519.Sign(key, []byte(b64))),
732 Grant: sg,
733 })
734 if _, err := verifyPointer(pub, wrapper, "bab", "kass", "edge"); err == nil ||
735 !strings.Contains(err.Error(), "root") {
736 t.Fatalf("a scoped-signed fast_track pointer verified (err=%v)", err)
737 }
738}
739
740// The wire change is additive both ways: a pre-change pointer still parses
741// (no fast_track key at all), and a fast-tracked pointer parses in a struct
742// that predates the field — the old binary simply keeps baking.
743func TestFastTrackIsAdditiveOnTheWire(t *testing.T) {
744 s := NewDirStore(t.TempDir())
745 ctx := context.Background()
746 priv, _ := testKeys(t)
747 shipRelease(t, s, "bab", "kass", "v1", "linux-amd64", []byte("b1"))
748 if err := Promote(ctx, s, priv, "bab", "kass", "stable", "v1"); err != nil {
749 t.Fatal(err)
750 }
751 if _, err := FastTrack(ctx, s, priv, "bab", "kass", "stable", ""); err != nil {
752 t.Fatal(err)
753 }
754 raw, _, err := GetQualified(ctx, s, ChannelKey("bab", "kass", "stable"))
755 if err != nil {
756 t.Fatal(err)
757 }
758 var sw signedWrapper
759 if err := json.Unmarshal(raw, &sw); err != nil {
760 t.Fatal(err)
761 }
762 payload, err := base64.StdEncoding.DecodeString(sw.Payload)
763 if err != nil {
764 t.Fatal(err)
765 }
766 // The old binary's shape: every pre-fast_track field, nothing else.
767 var old struct {
768 Schema int `json:"schema"`
769 App string `json:"app"`
770 Channel string `json:"channel"`
771 Version string `json:"version"`
772 }
773 if err := json.Unmarshal(payload, &old); err != nil {
774 t.Fatalf("an old struct shape failed to parse a fast-tracked payload: %v", err)
775 }
776 if old.Version != "v1" {
777 t.Fatalf("old shape read version %q", old.Version)
778 }
779}
780
781func TestManifestLabelNotesAdditive(t *testing.T) {
782 // A manifest written before label/notes existed must still parse (the
783 // production buckets are full of them) …
784 old := []byte(`{"schema":1,"app":"km","version":"de2766f8","kind":"binary","artifacts":{"linux-arm64":{"file":"linux-arm64","sha256":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","size":1}}}`)
785 var m Manifest
786 if err := json.Unmarshal(old, &m); err != nil {
787 t.Fatalf("old manifest no longer parses: %v", err)
788 }
789 if m.Label != "" || m.Notes != "" {
790 t.Fatalf("absent fields decoded non-empty: %q %q", m.Label, m.Notes)
791 }
792 // … and a label-free manifest must MARSHAL byte-identically to before,
793 // or idempotent re-ship of an old release breaks on its own metadata.
794 out, err := json.Marshal(m)
795 if err != nil {
796 t.Fatal(err)
797 }
798 if bytes.Contains(out, []byte("label")) || bytes.Contains(out, []byte("notes")) {
799 t.Fatalf("empty label/notes leaked into marshal: %s", out)
800 }
801}
802
803func TestValidateManifestLabelNotesLimits(t *testing.T) {
804 base := func() *Manifest {
805 return &Manifest{Schema: 1, App: "km", Version: "0.5.0-1", Kind: "binary",
806 Artifacts: map[string]Artifact{"linux-arm64": {File: "linux-arm64", SHA256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", Size: 1}}}
807 }
808 m := base()
809 m.Label = "fixing modals"
810 m.Notes = "- rounded the corners\n- fixed the focus trap"
811 if err := validateManifest(m); err != nil {
812 t.Fatalf("valid label/notes refused: %v", err)
813 }
814 m = base()
815 m.Label = strings.Repeat("x", MaxLabelLen+1)
816 if err := validateManifest(m); err == nil {
817 t.Fatal("overlong label accepted")
818 }
819 m = base()
820 m.Label = "two\nlines"
821 if err := validateManifest(m); err == nil {
822 t.Fatal("multi-line label accepted")
823 }
824 m = base()
825 m.Notes = strings.Repeat("x", MaxNotesLen+1)
826 if err := validateManifest(m); err == nil {
827 t.Fatal("overlong notes accepted")
828 }
829}
830
831func TestPromoteRetryRenewsPromotedAt(t *testing.T) {
832 s := NewDirStore(t.TempDir())
833 ctx := context.Background()
834 priv, pub := testKeys(t)
835 shipRelease(t, s, "bab", "console", "v1", adoptPlatform, []byte("b"))
836 if _, err := PromoteOnLadder(ctx, s, priv, "bab", "console", "canary/x", "v1", PromoteOptions{}); err != nil {
837 t.Fatal(err)
838 }
839 before, _ := Channel(ctx, s, pub, "bab", "console", "canary/x")
840 time.Sleep(1100 * time.Millisecond) // PromotedAt is truncated to the second
841 res, err := PromoteOnLadder(ctx, s, priv, "bab", "console", "canary/x", "v1", PromoteOptions{})
842 if err != nil || !res.NoOp {
843 t.Fatalf("plain repeat = %+v, %v; want the existing no-op", res, err)
844 }
845 same, _ := Channel(ctx, s, pub, "bab", "console", "canary/x")
846 if !same.PromotedAt.Equal(before.PromotedAt) {
847 t.Fatal("a plain repeat rewrote the pointer")
848 }
849 if _, err := PromoteOnLadder(ctx, s, priv, "bab", "console", "canary/x", "v1", PromoteOptions{Retry: true}); err != nil {
850 t.Fatalf("retry: %v", err)
851 }
852 after, _ := Channel(ctx, s, pub, "bab", "console", "canary/x")
853 if after.Version != "v1" || !after.PromotedAt.After(before.PromotedAt) {
854 t.Fatalf("after retry = %+v, want v1 with a later PromotedAt", after)
855 }
856}
857
858// A retry that decided on v1, then finds the channel moved to v2 before its
859// write, must refuse and leave v2 in place. The guarded write is called
860// directly with the version the retry saw, so the window is deterministic.
861func TestPromoteRetryWriteRefusesWhenTheChannelMoved(t *testing.T) {
862 s := NewDirStore(t.TempDir())
863 ctx := context.Background()
864 priv, pub := testKeys(t)
865 shipRelease(t, s, "bab", "console", "v1", adoptPlatform, []byte("a"))
866 shipRelease(t, s, "bab", "console", "v2", adoptPlatform, []byte("b"))
867 sg := RootSigner(priv)
868 if err := sg.Promote(ctx, s, "bab", "console", "canary/x", "v2"); err != nil {
869 t.Fatal(err)
870 }
871 err := sg.promote(ctx, s, "bab", "console", "canary/x", "v1", nil, "v1", Promotion{Retry: true})
872 if err == nil || !strings.Contains(err.Error(), "v2") || !strings.Contains(err.Error(), "v1") {
873 t.Fatalf("guarded retry write = %v, want a refusal naming both versions", err)
874 }
875 cp, cerr := Channel(ctx, s, pub, "bab", "console", "canary/x")
876 if cerr != nil || cp.Version != "v2" {
877 t.Fatalf("channel after refused retry = %+v, %v; want v2 untouched", cp, cerr)
878 }
879 // And a retry whose expectation holds still writes.
880 if err := sg.promote(ctx, s, "bab", "console", "canary/x", "v2", nil, "v2", Promotion{Retry: true}); err != nil {
881 t.Fatalf("guarded retry write with a matching version: %v", err)
882 }
883}
884
885// Final review F7: the guarded write's own pointer was stamped this very
886// second. The retry must still produce a strictly later PromotedAt.
887func TestPromoteRetryStampsAfterTheGuardedPointer(t *testing.T) {
888 s := NewDirStore(t.TempDir())
889 ctx := context.Background()
890 priv, pub := testKeys(t)
891 shipRelease(t, s, "bab", "console", "v1", adoptPlatform, []byte("a"))
892 sg := RootSigner(priv)
893 if err := sg.Promote(ctx, s, "bab", "console", "canary/x", "v1"); err != nil {
894 t.Fatal(err)
895 }
896 before, _ := Channel(ctx, s, pub, "bab", "console", "canary/x")
897 if err := sg.promote(ctx, s, "bab", "console", "canary/x", "v1", nil, "v1", Promotion{Retry: true}); err != nil {
898 t.Fatalf("retry: %v", err)
899 }
900 after, _ := Channel(ctx, s, pub, "bab", "console", "canary/x")
901 if !after.PromotedAt.After(before.PromotedAt) {
902 t.Fatalf("retry stamped %v over %v, want strictly later", after.PromotedAt, before.PromotedAt)
903 }
904}
905
906// getHook runs onGet before every Get, so a test can land a competing write
907// between a caller's first pointer read and its guarded one.
908type getHook struct {
909 Store
910 onGet func(key string)
911}
912
913func (h *getHook) Get(ctx context.Context, key string) ([]byte, string, error) {
914 h.onGet(key)
915 return h.Store.Get(ctx, key)
916}
917
918// Final review F7, the race itself: PromoteOnLadder's first read sees an old
919// pointer (no wait needed), then a concurrent retry renews it before the
920// guarded read. The stamp is judged against the pointer the write is
921// conditional on, so it is still strictly later than the competitor's.
922func TestConcurrentRetriesNeverShareAStamp(t *testing.T) {
923 inner := NewDirStore(t.TempDir())
924 ctx := context.Background()
925 priv, pub := testKeys(t)
926 shipRelease(t, inner, "bab", "console", "v1", adoptPlatform, []byte("a"))
927 sg := RootSigner(priv)
928 if err := sg.Promote(ctx, inner, "bab", "console", "canary/x", "v1"); err != nil {
929 t.Fatal(err)
930 }
931 time.Sleep(1100 * time.Millisecond) // the first read's pointer is a second old
932 key := ChannelKey("bab", "console", "canary/x")
933 var competitor time.Time
934 gets := 0
935 s := &getHook{Store: inner}
936 s.onGet = func(k string) {
937 if k != key {
938 return
939 }
940 gets++
941 if gets == 2 { // sg.promote's guarded read
942 if err := sg.promote(ctx, inner, "bab", "console", "canary/x", "v1", nil, "v1", Promotion{Retry: true}); err != nil {
943 t.Errorf("competing retry: %v", err)
944 }
945 cp, _ := Channel(ctx, inner, pub, "bab", "console", "canary/x")
946 competitor = cp.PromotedAt
947 }
948 }
949 if _, err := sg.PromoteOnLadder(ctx, s, "bab", "console", "canary/x", "v1", PromoteOptions{Retry: true}); err != nil {
950 t.Fatalf("retry: %v", err)
951 }
952 if gets < 2 || competitor.IsZero() {
953 t.Fatalf("the competing retry never ran (gets=%d)", gets)
954 }
955 after, _ := Channel(ctx, inner, pub, "bab", "console", "canary/x")
956 if !after.PromotedAt.After(competitor) {
957 t.Fatalf("retry stamped %v, the competitor %v: want strictly later", after.PromotedAt, competitor)
958 }
959}
960
961func TestPromoteRetryNeverChangesTheVersion(t *testing.T) {
962 s := NewDirStore(t.TempDir())
963 ctx := context.Background()
964 priv, _ := testKeys(t)
965 shipRelease(t, s, "bab", "console", "v1", adoptPlatform, []byte("a"))
966 shipRelease(t, s, "bab", "console", "v2", adoptPlatform, []byte("b"))
967 if _, err := PromoteOnLadder(ctx, s, priv, "bab", "console", "canary/x", "v1", PromoteOptions{}); err != nil {
968 t.Fatal(err)
969 }
970 _, err := PromoteOnLadder(ctx, s, priv, "bab", "console", "canary/x", "v2", PromoteOptions{Retry: true})
971 if err == nil || !strings.Contains(err.Error(), "renews a promotion") {
972 t.Fatalf("retry onto a different version = %v, want a refusal", err)
973 }
974}
975