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/json"
8 "errors"
9 "fmt"
10 "os"
11 "path/filepath"
12 "strings"
13 "testing"
14 "time"
15)
16
17// D3-held (controller ruling): a held stage applies D3 exactly as adoption
18// does (ProofGate.admit before staging, ProofGate.bind before publishing)
19// and fails closed. Where adoption would refuse, the held stage returns
20// ErrHeldNotProven before publishing and live/<host> is untouched; with no
21// policy, or a policy that does not list the app, nothing changes.
22
23// heldProofEnv is heldFixture with the held host already live on "v0" (so
24// "untouched" means something), a policy path, a bake directory, a prover
25// key and a gate.
26type heldProofEnv struct {
27 ctx context.Context
28 store Store
29 opt, bake string
30 policy string
31 gate *ProofGate
32 proverPub string
33 prover ed25519.PrivateKey
34 pinnedSHA string
35}
36
37func newHeldProofEnv(t *testing.T) *heldProofEnv {
38 t.Helper()
39 ctx, s, opt := heldFixture(t)
40 if err := StageArtifact(opt, "bab", "v0", "console", []byte("earlier build")); err != nil {
41 t.Fatal(err)
42 }
43 if err := PointLive(opt, heldTestHost, "bab", "v0", "console"); err != nil {
44 t.Fatal(err)
45 }
46 prover, _, pubB64 := proverKey(t)
47 e := &heldProofEnv{ctx: ctx, store: s, opt: opt, bake: t.TempDir(),
48 policy: filepath.Join(t.TempDir(), "proof-policy.json"), proverPub: pubB64, prover: prover,
49 pinnedSHA: etagOf([]byte("pinned build"))}
50 e.gate = NewProofGate(e.policy, e.bake)
51 return e
52}
53
54func (e *heldProofEnv) writePolicy(t *testing.T, body string) {
55 t.Helper()
56 if err := os.WriteFile(e.policy, []byte(body), 0o644); err != nil {
57 t.Fatal(err)
58 }
59}
60
61func (e *heldProofEnv) require(t *testing.T, app string) {
62 e.writePolicy(t, fmt.Sprintf(`{"schema":1,"require":[{"app":%q,"prover":"dev","key":%q}]}`, app, e.proverPub))
63}
64
65func (e *heldProofEnv) prove(t *testing.T, sha string) {
66 t.Helper()
67 p, err := SignProof(e.prover, Proof{App: "console", Platform: adoptPlatform, SHA256: sha, Version: "v1", Prover: "dev",
68 ActiveSince: time.Date(2026, 10, 6, 9, 0, 0, 0, time.UTC), ProvenAt: time.Date(2026, 10, 6, 10, 0, 0, 0, time.UTC)})
69 if err != nil {
70 t.Fatal(err)
71 }
72 b, err := json.MarshalIndent(p, "", " ")
73 if err != nil {
74 t.Fatal(err)
75 }
76 if _, err := e.store.Put(e.ctx, ProofKey("bab", "console", sha, "dev"), b, ""); err != nil {
77 t.Fatal(err)
78 }
79}
80
81// staleCopyDown is stageStaleCopyKept for v1: a copy of other bytes staged
82// while v1's canonical manifest cannot be read, so only D3's bind can catch
83// it. A held stage reaches bind on such a copy only on pointer trust (want
84// "", a build promoted before records, settings spec §4.1): under v1's
85// promotion record the stage binds the record's digest and refuses the copy
86// before bind (TestHeldStageRecordRefusesAStaleCopyItCannotCheck). So v1's
87// record is removed first, and the digestless canary/x pointer is the
88// stage's evidence.
89func (e *heldProofEnv) staleCopyDown(t *testing.T, stale []byte) Store {
90 t.Helper()
91 if err := e.store.Delete(e.ctx, PromotionRecordKey("bab", "console", "v1")); err != nil {
92 t.Fatal(err)
93 }
94 return stageStaleCopyKept(t, e.ctx, e.store, e.opt, "bab", "console", "v1", stale)
95}
96
97func (e *heldProofEnv) live() string { v, _ := LiveVersion(e.opt, heldTestHost); return v }
98
99// materialize runs MaterializeHeld with a guard that records whether it
100// was asked to publish, and returns the error, whether the guard ran, and
101// what was printed.
102func (e *heldProofEnv) materialize(t *testing.T, s Store) (error, bool, string) {
103 t.Helper()
104 var (
105 err error
106 guarded bool
107 out bytes.Buffer
108 )
109 withCapturedStdout(t, &out, func() {
110 _, err = MaterializeHeld(e.ctx, s, heldPub, nr(t), e.opt, heldRoute(), "v1", e.gate, func(publish func() error) error {
111 guarded = true
112 return publish()
113 })
114 })
115 return err, guarded, out.String()
116}
117
118func (e *heldProofEnv) refusedUntouched(t *testing.T, err error, guarded bool) {
119 t.Helper()
120 if !errors.Is(err, ErrHeldNotProven) {
121 t.Fatalf("MaterializeHeld: %v, want ErrHeldNotProven", err)
122 }
123 if guarded {
124 t.Fatal("the held stage asked its authority to publish an unproven release")
125 }
126 if e.live() != "v0" {
127 t.Fatalf("live = %q, want v0 untouched", e.live())
128 }
129}
130
131func TestHeldStageProofCoveredUnprovenIsRefused(t *testing.T) {
132 e := newHeldProofEnv(t)
133 e.require(t, "console")
134 err, guarded, out := e.materialize(t, e.store)
135 e.refusedUntouched(t, err, guarded)
136 if !strings.Contains(out, "is not proven on dev") {
137 t.Fatalf("printed %q, want adoption's not-proven line", out)
138 }
139 // admit runs before staging: an unproven build is never downloaded.
140 if _, err := os.Stat(VersionPath(e.opt, "bab", "v1", "console")); !errors.Is(err, os.ErrNotExist) {
141 t.Fatalf("v1 was staged for an unproven held stage: %v", err)
142 }
143 // A missing proof is not a refusal record, as in adoption.
144 for _, base := range []string{e.bake, HeldRefusalBase(e.bake)} {
145 if r, _ := ReadRefusal(base, heldTestHost); r != nil {
146 t.Fatalf("a missing proof wrote a refusal: %+v", r)
147 }
148 }
149}
150
151func TestHeldStageProofBadProofIsRefused(t *testing.T) {
152 e := newHeldProofEnv(t)
153 e.require(t, "console")
154 other, _, _ := proverKey(t)
155 p, err := SignProof(other, Proof{App: "console", Platform: adoptPlatform, SHA256: e.pinnedSHA, Version: "v1", Prover: "dev",
156 ActiveSince: time.Date(2026, 10, 6, 9, 0, 0, 0, time.UTC), ProvenAt: time.Date(2026, 10, 6, 10, 0, 0, 0, time.UTC)})
157 if err != nil {
158 t.Fatal(err)
159 }
160 b, _ := json.Marshal(p)
161 if _, err := e.store.Put(e.ctx, ProofKey("bab", "console", e.pinnedSHA, "dev"), b, ""); err != nil {
162 t.Fatal(err)
163 }
164 err, guarded, _ := e.materialize(t, e.store)
165 e.refusedUntouched(t, err, guarded)
166}
167
168func TestHeldStageProofCoveredAdmittedPublishes(t *testing.T) {
169 e := newHeldProofEnv(t)
170 e.require(t, "console")
171 e.prove(t, e.pinnedSHA)
172 if err, guarded, _ := e.materialize(t, e.store); err != nil || !guarded {
173 t.Fatalf("MaterializeHeld: %v (guarded %v)", err, guarded)
174 }
175 if e.live() != "v1" {
176 t.Fatalf("live = %q, want the proven v1", e.live())
177 }
178}
179
180func TestHeldStageProofHealthyHistoryPublishes(t *testing.T) {
181 e := newHeldProofEnv(t)
182 e.require(t, "console")
183 if err := RecordHealthy(e.bake, "bab", "console", HealthyBuild{SHA256: e.pinnedSHA, Version: "v1", How: "settle", At: time.Now()}); err != nil {
184 t.Fatal(err)
185 }
186 cs := &proofReadCountingStore{Store: e.store}
187 if err, _, _ := e.materialize(t, cs); err != nil {
188 t.Fatalf("MaterializeHeld: %v", err)
189 }
190 if e.live() != "v1" || cs.proofGets() != 0 {
191 t.Fatalf("live = %q, %d proof GETs; want v1 from the healthy history and no proof read", e.live(), cs.proofGets())
192 }
193}
194
195func TestHeldStageProofUncoveredIsUnchanged(t *testing.T) {
196 for name, setup := range map[string]func(t *testing.T, e *heldProofEnv){
197 "no policy file": func(*testing.T, *heldProofEnv) {},
198 "policy lists another app": func(t *testing.T, e *heldProofEnv) { e.require(t, "other") },
199 } {
200 t.Run(name, func(t *testing.T) {
201 e := newHeldProofEnv(t)
202 setup(t, e)
203 cs := &proofReadCountingStore{Store: e.store}
204 if err, guarded, _ := e.materialize(t, cs); err != nil || !guarded {
205 t.Fatalf("MaterializeHeld: %v (guarded %v)", err, guarded)
206 }
207 if e.live() != "v1" {
208 t.Fatalf("live = %q, want v1", e.live())
209 }
210 if n := cs.proofGets(); n != 0 {
211 t.Fatalf("%d proof GETs for an app no policy covers, want 0", n)
212 }
213 })
214 }
215}
216
217// A policy that does not load is closed for every held route, named unit
218// or not: the held path cannot vouch for the route's unit binding.
219func TestHeldStageProofBrokenPolicyIsRefused(t *testing.T) {
220 e := newHeldProofEnv(t)
221 e.writePolicy(t, `{"schema":1,"require":[`)
222 err, guarded, _ := e.materialize(t, e.store)
223 e.refusedUntouched(t, err, guarded)
224}
225
226// bind: the manifest's sha256 is proven, but the staged file holds other
227// bytes (a kept stale copy: see stageStaleCopyKept). Refused before
228// publishing, with adoption's not-proven record.
229func TestHeldStageProofBindingRefusesOtherStagedBytes(t *testing.T) {
230 e := newHeldProofEnv(t)
231 e.require(t, "console")
232 e.prove(t, e.pinnedSHA)
233 stale := []byte("not the shipped bytes")
234 down := e.staleCopyDown(t, stale)
235 // bind runs inside the publication (fix round 1), so the guard is asked
236 // and the refusal comes from within it.
237 err, _, out := e.materialize(t, down)
238 if !errors.Is(err, ErrHeldNotProven) || e.live() != "v0" {
239 t.Fatalf("MaterializeHeld: %v, live %q; want ErrHeldNotProven and v0", err, e.live())
240 }
241 if !strings.Contains(out, "not pointed live") {
242 t.Fatalf("printed %q, want the binding refusal", out)
243 }
244 r, _ := ReadRefusal(HeldRefusalBase(e.bake), heldTestHost)
245 if r == nil || r.Kind != RefusalNotProven || !strings.Contains(r.Reason, etagOf(stale)) || r.RestoredVersion != "v0" {
246 t.Fatalf("refusal = %+v, want a not-proven record naming the staged hash, still on v0", r)
247 }
248}
249
250// PublishHeld called on its own (after StageHeld) runs the same check.
251func TestHeldStageProofPublishHeldChecksToo(t *testing.T) {
252 e := newHeldProofEnv(t)
253 if _, err := StageHeld(e.ctx, e.store, heldPub, nr(t), e.opt, heldRoute(), "v1"); err != nil {
254 t.Fatal(err)
255 }
256 e.require(t, "console")
257 var out bytes.Buffer
258 var err error
259 withCapturedStdout(t, &out, func() {
260 err = PublishHeld(e.ctx, e.store, e.opt, heldRoute(), "v1", e.gate, openGuard)
261 })
262 if !errors.Is(err, ErrHeldNotProven) || e.live() != "v0" {
263 t.Fatalf("PublishHeld: %v, live %q; want ErrHeldNotProven and v0", err, e.live())
264 }
265}
266
267// A held stage with no gate cannot publish: D3 is not optional here.
268func TestHeldStageProofNeedsAGate(t *testing.T) {
269 e := newHeldProofEnv(t)
270 if _, err := MaterializeHeld(e.ctx, e.store, heldPub, nr(t), e.opt, heldRoute(), "v1", nil, openGuard); err == nil {
271 t.Fatal("MaterializeHeld published with no proof gate")
272 }
273 if err := PublishHeld(e.ctx, e.store, e.opt, heldRoute(), "v1", nil, openGuard); err == nil {
274 t.Fatal("PublishHeld published with no proof gate")
275 }
276 if e.live() != "v0" {
277 t.Fatalf("live = %q, want v0", e.live())
278 }
279 if _, err := os.Stat(VersionPath(e.opt, "bab", "v1", "console")); !errors.Is(err, os.ErrNotExist) {
280 t.Fatalf("v1 was staged with no gate: %v", err)
281 }
282}
283
284// The root relay: an unproven request is removed, never published and
285// never acknowledged; a proven one goes live.
286func TestServeHeldStageRequestsAppliesTheProofGate(t *testing.T) {
287 e := newHeldProofEnv(t)
288 e.require(t, "console")
289 dd := t.TempDir()
290 path := writeHeldRequest(t, dd, heldTestHost+".json", goodHeldRequest())
291 var out bytes.Buffer
292 withCapturedStdout(t, &out, func() {
293 if err := ServeHeldStageRequests(e.ctx, e.store, heldPub, nr(t), e.opt, dd, e.gate, passAuth(nil)); err != nil {
294 t.Fatalf("ServeHeldStageRequests: %v", err)
295 }
296 })
297 gone(t, path)
298 gone(t, filepath.Join(HeldStagedDir(dd), heldTestHost+".json"))
299 if e.live() != "v0" {
300 t.Fatalf("live = %q, want v0 untouched", e.live())
301 }
302 if !strings.Contains(out.String(), "refused") {
303 t.Fatalf("printed %q, want the refusal", out.String())
304 }
305
306 e.prove(t, e.pinnedSHA)
307 e.gate = NewProofGate(e.policy, e.bake) // a fresh root pass: no cached miss
308 writeHeldRequest(t, dd, heldTestHost+".json", goodHeldRequest())
309 withCapturedStdout(t, &out, func() {
310 if err := ServeHeldStageRequests(e.ctx, e.store, heldPub, nr(t), e.opt, dd, e.gate, passAuth(nil)); err != nil {
311 t.Fatalf("ServeHeldStageRequests: %v", err)
312 }
313 })
314 if e.live() != "v1" {
315 t.Fatalf("live = %q, want the proven v1", e.live())
316 }
317}
318
319// materializeWith is materialize with the caller's guard.
320func (e *heldProofEnv) materializeWith(t *testing.T, guard func(publish func() error) error) (error, string) {
321 t.Helper()
322 var (
323 err error
324 out bytes.Buffer
325 )
326 withCapturedStdout(t, &out, func() {
327 _, err = MaterializeHeld(e.ctx, e.store, heldPub, nr(t), e.opt, heldRoute(), "v1", e.gate, guard)
328 })
329 return err, out.String()
330}
331
332// Fix round 1, finding 1: the authority's re-check (bucket reads, the
333// registry freeze) runs between staging and publication, and the staged
334// bytes can be replaced meanwhile. bind runs inside the publication, after
335// the re-check, so bytes replaced while the guard ran are refused.
336func TestHeldStageProofBindsInsideThePublication(t *testing.T) {
337 e := newHeldProofEnv(t)
338 e.require(t, "console")
339 e.prove(t, e.pinnedSHA)
340 stale := []byte("replaced while the guard re-checked")
341 err, _ := e.materializeWith(t, func(publish func() error) error {
342 if err := StageArtifact(e.opt, "bab", "v1", "console", stale); err != nil {
343 t.Fatal(err)
344 }
345 return publish()
346 })
347 if !errors.Is(err, ErrHeldNotProven) || e.live() != "v0" {
348 t.Fatalf("MaterializeHeld: %v, live %q; want ErrHeldNotProven and v0", err, e.live())
349 }
350}
351
352// Fix round 1, finding 1: bytes replaced between bind and the link swap
353// are caught by a re-hash at the swap, and the link goes back to what it
354// was (or away, when there was nothing).
355func TestHeldStageProofRehashesAtTheSwap(t *testing.T) {
356 for name, hadLive := range map[string]bool{"was on v0": true, "had no link": false} {
357 t.Run(name, func(t *testing.T) {
358 e := newHeldProofEnv(t)
359 if !hadLive {
360 if err := os.Remove(LiveLink(e.opt, heldTestHost)); err != nil {
361 t.Fatal(err)
362 }
363 }
364 e.require(t, "console")
365 e.prove(t, e.pinnedSHA)
366 prev := heldBeforeSwap
367 t.Cleanup(func() { heldBeforeSwap = prev })
368 swapped := false
369 heldBeforeSwap = func() {
370 swapped = true
371 if err := StageArtifact(e.opt, "bab", "v1", "console", []byte("replaced after bind")); err != nil {
372 t.Fatal(err)
373 }
374 }
375 err, guarded, _ := e.materialize(t, e.store)
376 if !swapped || !guarded {
377 t.Fatalf("the seam ran %v, the guard ran %v", swapped, guarded)
378 }
379 want := "v0"
380 if !hadLive {
381 want = ""
382 }
383 if !errors.Is(err, ErrHeldNotProven) || e.live() != want {
384 t.Fatalf("MaterializeHeld: %v, live %q; want ErrHeldNotProven and %q", err, e.live(), want)
385 }
386 })
387 }
388}
389
390// Fix round 1, finding 2: a held binding refusal carries its move's op.
391func TestHeldStageProofRefusalCarriesTheMove(t *testing.T) {
392 e := newHeldProofEnv(t)
393 e.require(t, "console")
394 e.prove(t, e.pinnedSHA)
395 down := e.staleCopyDown(t, []byte("not the shipped bytes"))
396 e.materialize(t, down)
397 r, _ := ReadRefusal(HeldRefusalBase(e.bake), heldTestHost)
398 if r == nil || r.MoveOp != heldTestOp || r.Kind != RefusalNotProven {
399 t.Fatalf("refusal = %+v, want a not-proven record naming op %s", r, heldTestOp)
400 }
401 // Final review finding 3: in the held namespace, never adoption's.
402 if r, _ := ReadRefusal(e.bake, heldTestHost); r != nil {
403 t.Fatalf("the held refusal reached adoption's records: %+v", r)
404 }
405}
406
407// Fix round 1, finding 2: an adoption refusal already standing for the host
408// is never overwritten by a held one. Since final review finding 3 the held
409// one is recorded beside it, in its own namespace.
410func TestHeldStageProofLeavesAnAdoptionRefusal(t *testing.T) {
411 e := newHeldProofEnv(t)
412 e.require(t, "console")
413 e.prove(t, e.pinnedSHA)
414 adoption := Refusal{Host: heldTestHost, App: "console", Version: "v0", SHA256: "aa", Reason: "did not come up",
415 PointerPromotedAt: time.Date(2026, 10, 1, 0, 0, 0, 0, time.UTC), RefusedAt: time.Date(2026, 10, 1, 0, 1, 0, 0, time.UTC)}
416 if err := WriteRefusal(e.bake, adoption); err != nil {
417 t.Fatal(err)
418 }
419 before, _ := os.ReadFile(filepath.Join(RefusalDir(e.bake), heldTestHost+".json"))
420 down := e.staleCopyDown(t, []byte("not the shipped bytes"))
421 err, _, _ := e.materialize(t, down)
422 if !errors.Is(err, ErrHeldNotProven) {
423 t.Fatalf("MaterializeHeld: %v, want ErrHeldNotProven", err)
424 }
425 after, _ := os.ReadFile(filepath.Join(RefusalDir(e.bake), heldTestHost+".json"))
426 if !bytes.Equal(before, after) {
427 t.Fatalf("the adoption refusal changed:\n%s\nwant\n%s", after, before)
428 }
429 if r, _ := ReadRefusal(HeldRefusalBase(e.bake), heldTestHost); r == nil || r.MoveOp != heldTestOp {
430 t.Fatalf("the held refusal = %+v, want it recorded beside the adoption one", r)
431 }
432}
433
434// Fix round 1, finding 2: a later held stage of the same move that
435// succeeds retires that move's refusal.
436func TestHeldStageProofSuccessRetiresTheMovesRefusal(t *testing.T) {
437 e := newHeldProofEnv(t)
438 e.require(t, "console")
439 e.prove(t, e.pinnedSHA)
440 down := e.staleCopyDown(t, []byte("not the shipped bytes"))
441 e.materialize(t, down)
442 if r, _ := ReadRefusal(HeldRefusalBase(e.bake), heldTestHost); r == nil {
443 t.Fatal("no refusal to retire")
444 }
445 if err := StageArtifact(e.opt, "bab", "v1", "console", []byte("pinned build")); err != nil {
446 t.Fatal(err)
447 }
448 if err, _, _ := e.materialize(t, e.store); err != nil || e.live() != "v1" {
449 t.Fatalf("retry: %v, live %q", err, e.live())
450 }
451 if r, _ := ReadRefusal(HeldRefusalBase(e.bake), heldTestHost); r != nil {
452 t.Fatalf("the move's refusal outlived its successful retry: %+v", r)
453 }
454}
455
456// RetireHeldRefusals removes a held refusal whose move is over and nothing
457// else: not a live move's, never an adoption refusal.
458func TestRetireHeldRefusals(t *testing.T) {
459 bake := t.TempDir()
460 at := time.Date(2026, 10, 7, 0, 0, 0, 0, time.UTC)
461 for _, r := range []Refusal{
462 {Host: "live.example.com", App: "a", Version: "v1", RefusedAt: at, Kind: RefusalNotProven},
463 {Host: "over.example.com", App: "a", Version: "v1", RefusedAt: at, Kind: RefusalNotProven},
464 } {
465 if err := heldRefusalWriter(bake, "mv_"+strings.TrimSuffix(r.Host, ".example.com"))(r); err != nil {
466 t.Fatal(err)
467 }
468 }
469 if err := WriteRefusal(bake, Refusal{Host: "adopted.example.com", App: "a", Version: "v1", RefusedAt: at, Kind: RefusalNotProven}); err != nil {
470 t.Fatal(err)
471 }
472 retired, err := RetireHeldRefusals(bake, func(r Refusal) bool { return r.MoveOp == "mv_live" })
473 if err != nil || len(retired) != 1 || retired[0] != "over.example.com" {
474 t.Fatalf("retired %v, %v; want over.example.com", retired, err)
475 }
476 left, _ := ListHeldRefusals(bake)
477 if len(left) != 1 || left[0].Host != "live.example.com" {
478 t.Fatalf("held left %+v", left)
479 }
480 if adopted, _ := ListRefusals(bake); len(adopted) != 1 || adopted[0].Host != "adopted.example.com" {
481 t.Fatalf("adoption's records %+v", adopted)
482 }
483 // With no refusal directory it reads nothing and fails nothing.
484 if retired, err := RetireHeldRefusals(t.TempDir(), func(Refusal) bool { return false }); err != nil || len(retired) != 0 {
485 t.Fatalf("empty: %v, %v", retired, err)
486 }
487}
488