carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package console
2
3import (
4 "bytes"
5 "context"
6 "encoding/json"
7 "errors"
8 "io"
9 "log"
10 "os"
11 "strconv"
12 "strings"
13 "testing"
14 "time"
15
16 "github.com/carlosframework/platform/internal/provision"
17 "github.com/carlosframework/platform/internal/release"
18 "github.com/carlosframework/platform/internal/status"
19)
20
21// mustTestSecretsKey is Task 9's (regionconfig_test.go).
22var testKeyPub, testKeyFP = mustTestSecretsKey()
23
24func bp(b bool) *bool { return &b }
25
26type edgeOpts struct {
27 hib, shared, inbound *bool // nil = the writer predates the field
28 fp string
29}
30
31var eligibleBox = edgeOpts{hib: bp(true), shared: bp(true), inbound: bp(true), fp: testKeyFP}
32
33// edgeJSON writes one box's edges file the way internal/host does.
34func edgeJSON(box, eip, awsRegion, placement string, o edgeOpts) string {
35 m := map[string]any{"v": 1, "box": box, "eip": eip, "region": awsRegion, "hosts": map[string]any{}}
36 if placement != "" {
37 m["placement"] = placement
38 }
39 if o.hib != nil {
40 m["hibernation_configured"] = *o.hib
41 }
42 if o.shared != nil {
43 m["serves_shared_pool"] = *o.shared
44 }
45 if o.inbound != nil {
46 m["accepts_inbound"] = *o.inbound
47 }
48 if o.fp != "" {
49 m["secrets_key_fp"] = o.fp
50 }
51 b, _ := json.Marshal(m)
52 return string(b)
53}
54
55var (
56 homeBox = edgeJSON("ie-box", "5.6.7.8", "eu-west-1", "", eligibleBox)
57 ukEligible = edgeJSON("uk-box", "1.2.3.4", "eu-west-2", "uk", eligibleBox)
58 ukNoKey = edgeJSON("uk-box", "1.2.3.4", "eu-west-2", "uk", edgeOpts{hib: bp(true), shared: bp(true), inbound: bp(true)})
59 ukWrongKey = edgeJSON("uk-box", "1.2.3.4", "eu-west-2", "uk", edgeOpts{hib: bp(true), shared: bp(true), inbound: bp(true), fp: "sha256:" + strings.Repeat("0", 64)})
60 ukOldWriter = edgeJSON("uk-box", "1.2.3.4", "eu-west-2", "uk", edgeOpts{})
61 auPoolOnly = edgeJSON("au-box", "9.9.9.9", "ap-southeast-2", "au", edgeOpts{hib: bp(true), shared: bp(false), inbound: bp(true), fp: testKeyFP})
62 usNoInbound = edgeJSON("us-box", "8.8.4.4", "us-east-1", "us", edgeOpts{hib: bp(true), shared: bp(true), inbound: bp(false), fp: testKeyFP})
63 nzNoHosting = edgeJSON("nz-box", "6.6.6.6", "ap-southeast-6", "nz", edgeOpts{hib: bp(false), shared: bp(true), inbound: bp(true), fp: testKeyFP})
64 jpNoEIP = edgeJSON("jp-box", "", "ap-northeast-1", "jp", eligibleBox)
65 fleetBoxFile = edgeJSON("fl-box", "4.4.4.4", "eu-west-2", "fleet/acme", eligibleBox)
66 // ukKeylessSibling is a second uk box reconcile would admit (shared pool,
67 // inbound) that holds no key: the F2 hazard beside ukEligible.
68 ukKeylessSibling = edgeJSON("uk-box-2", "1.2.3.5", "eu-west-2", "uk", edgeOpts{hib: bp(true), shared: bp(true), inbound: bp(true)})
69)
70
71// Partial reads (re-review r1): automatic non-home eligibility needs
72// COMPLETE evidence, the edges snapshot Complete and every listed box's
73// status read succeeded. Otherwise a failing sibling can simply drop out
74// of the fold (its edges GET failed) or read as not fresh (its status GET
75// failed), and the region becomes eligible through the box that was read.
76// On any incomplete read no non-home region is eligible for that request
77// (whole-request scope, the stricter choice); the home stays as it is.
78func TestIncompleteReadsNeverMakeARegionEligible(t *testing.T) {
79 ctx := context.Background()
80 auBox := edgeJSON("au-box", "9.9.9.9", "ap-southeast-2", "au", eligibleBox)
81 s := release.NewDirStore(t.TempDir())
82 for key, body := range map[string]string{"ie-box": homeBox, "uk-box": ukEligible, "uk-box-2": ukKeylessSibling, "au-box": auBox} {
83 if _, err := s.Put(ctx, "control/edges/"+key+".json", []byte(body), ""); err != nil {
84 t.Fatal(err)
85 }
86 setBoxSnapshot(t, s, key, time.Now())
87 }
88 check := func(name string, store release.Store, wantAU bool) {
89 t.Helper()
90 snap, err := readLiveEdgesSnapshot(ctx, store, time.Now())
91 if err != nil {
92 t.Fatalf("%s: %v", name, err)
93 }
94 cands := regionCandidates(snap, "ie", testKeyFP)
95 uk, _ := findCandidate(cands, "uk")
96 au, _ := findCandidate(cands, "au")
97 home, _ := findCandidate(cands, "")
98 if uk.Eligible {
99 t.Errorf("%s: uk = %+v, want ineligible (a keyless sibling was listed)", name, uk)
100 }
101 if au.Eligible != wantAU {
102 t.Errorf("%s: au eligible = %v, want %v", name, au.Eligible, wantAU)
103 }
104 if !home.Eligible {
105 t.Errorf("%s: home = %+v, want eligible regardless", name, home)
106 }
107 }
108 // Control: everything read. uk fails on its keyless sibling, au is clean.
109 check("complete", s, true)
110 // Hole 1: the keyless sibling's EDGES object cannot be read. It drops out
111 // of the fold, and only the keyed box remains to speak for uk.
112 check("edges GET failure", failingGet{Store: s, key: "control/edges/uk-box-2.json"}, false)
113 // Hole 2: the keyless sibling's STATUS snapshot cannot be read. Unknown
114 // liveness is not staleness: it must not be excluded as stale.
115 check("status GET failure", failingGet{Store: s, key: "status/boxes/uk-box-2.json"}, false)
116 // A corrupt status body is an incomplete read too (loadBoxes skips it).
117 corrupt := release.NewDirStore(t.TempDir())
118 for _, key := range []string{"ie-box", "uk-box", "uk-box-2", "au-box"} {
119 body, _, err := s.Get(ctx, "control/edges/"+key+".json")
120 if err != nil {
121 t.Fatal(err)
122 }
123 if _, err := corrupt.Put(ctx, "control/edges/"+key+".json", body, ""); err != nil {
124 t.Fatal(err)
125 }
126 setBoxSnapshot(t, corrupt, key, time.Now())
127 }
128 if _, err := corrupt.Put(ctx, "status/boxes/uk-box-2.json", []byte("{not json"), mustEtag(t, corrupt, "status/boxes/uk-box-2.json")); err != nil {
129 t.Fatal(err)
130 }
131 check("corrupt status snapshot", corrupt, false)
132}
133
134// Unmatched boxes, ruling (A) (re-review r2): a FRESH status snapshot
135// whose box has no edges file is incomplete evidence, whole-request, the
136// same as a failed read. Reconcile does not need the edges file to admit a
137// box, so a keyless live box whose edges publication failed would
138// otherwise never reach the fold while its keyed sibling made the region
139// eligible. Matched by the status Box.Label against the edges file's own
140// box field, the pair edgesfile.EdgeIPs joins on. A STALE snapshot with no
141// edges file is a retired box and is ignored.
142func TestFreshStatusWithoutAnEdgesFileIsIncompleteEvidence(t *testing.T) {
143 ctx := context.Background()
144 s := release.NewDirStore(t.TempDir())
145 for key, body := range map[string]string{"ie-box": homeBox, "uk-box": ukEligible} {
146 if _, err := s.Put(ctx, "control/edges/"+key+".json", []byte(body), ""); err != nil {
147 t.Fatal(err)
148 }
149 setBoxSnapshot(t, s, key, time.Now())
150 }
151 check := func(name string, wantUK bool) {
152 t.Helper()
153 snap, err := readLiveEdgesSnapshot(ctx, s, time.Now())
154 if err != nil {
155 t.Fatalf("%s: %v", name, err)
156 }
157 cands := regionCandidates(snap, "ie", testKeyFP)
158 uk, _ := findCandidate(cands, "uk")
159 home, _ := findCandidate(cands, "")
160 if uk.Eligible != wantUK {
161 t.Errorf("%s: uk = %+v, want eligible %v", name, uk, wantUK)
162 }
163 if !home.Eligible {
164 t.Errorf("%s: home = %+v, want eligible regardless", name, home)
165 }
166 }
167 check("control: every fresh box has an edges file", true)
168 // Box B reports status, fresh, and has published no edges file.
169 setBoxSnapshot(t, s, "uk-box-2", time.Now())
170 check("fresh status-only box", false)
171 // The same box gone dark: a retired box, ignored.
172 setBoxSnapshot(t, s, "uk-box-2", time.Now().Add(-time.Hour))
173 check("stale status-only box", true)
174}
175
176// Unmatched boxes, ruling (B) REVERSED (re-review r2, production
177// evidence): on the flagship every box has both objects, and a retired
178// box leaves both behind, so it reads as a STALE snapshot, never an absent
179// one. An edges file whose box has no status object at all is therefore
180// not a retired box but unknown liveness, and it makes the evidence
181// incomplete, whole-request, like (A). A stale snapshot beside the file is
182// the retired box: not fresh, counts for neither side, and the region
183// stays eligible through its fresh keyed sibling (the F1 case).
184func TestEdgesFileWithoutAnyStatusSnapshotIsIncompleteEvidence(t *testing.T) {
185 ctx := context.Background()
186 s := release.NewDirStore(t.TempDir())
187 for key, body := range map[string]string{"ie-box": homeBox, "uk-box": ukEligible} {
188 if _, err := s.Put(ctx, "control/edges/"+key+".json", []byte(body), ""); err != nil {
189 t.Fatal(err)
190 }
191 setBoxSnapshot(t, s, key, time.Now())
192 }
193 if _, err := s.Put(ctx, "control/edges/uk-box-2.json", []byte(ukKeylessSibling), ""); err != nil {
194 t.Fatal(err)
195 }
196 snap, err := readLiveEdgesSnapshot(ctx, s, time.Now())
197 if err != nil {
198 t.Fatal(err)
199 }
200 if snap.EvidenceComplete() {
201 t.Fatalf("a keyless edges file with NO status object left the evidence complete (edges %v, boxes %v)", snap.Complete, snap.BoxesComplete)
202 }
203 if c, _ := findCandidate(regionCandidates(snap, "ie", testKeyFP), "uk"); c.Eligible {
204 t.Fatalf("uk = %+v, want ineligible: a box with no status object has unknown liveness", c)
205 }
206 if h, _ := findCandidate(regionCandidates(snap, "ie", testKeyFP), ""); !h.Eligible {
207 t.Fatalf("home = %+v, want eligible regardless", h)
208 }
209 // The retired box: the same edges file beside a STALE snapshot. F1's
210 // case, and it must keep working.
211 setBoxSnapshot(t, s, "uk-box-2", time.Now().Add(-time.Hour))
212 snap, err = readLiveEdgesSnapshot(ctx, s, time.Now())
213 if err != nil {
214 t.Fatal(err)
215 }
216 if !snap.EvidenceComplete() {
217 t.Fatalf("a stale snapshot beside its edges file made the evidence incomplete (edges %v, boxes %v); a retired box is complete evidence", snap.Complete, snap.BoxesComplete)
218 }
219 if c, _ := findCandidate(regionCandidates(snap, "ie", testKeyFP), "uk"); !c.Eligible {
220 t.Fatalf("retired keyless sibling: uk = %+v, want eligible through the fresh keyed box", c)
221 }
222}
223
224func mustEtag(t *testing.T, s release.Store, key string) string {
225 t.Helper()
226 _, etag, err := s.Get(context.Background(), key)
227 if err != nil {
228 t.Fatal(err)
229 }
230 return etag
231}
232
233// Incomplete evidence is said once, like the other liveness diagnostics.
234func TestIncompleteEvidenceIsLoggedOnce(t *testing.T) {
235 ctx := context.Background()
236 s := release.NewDirStore(t.TempDir())
237 for key, body := range map[string]string{"ie-box": homeBox, "uk-box": ukEligible} {
238 if _, err := s.Put(ctx, "control/edges/"+key+".json", []byte(body), ""); err != nil {
239 t.Fatal(err)
240 }
241 setBoxSnapshot(t, s, key, time.Now())
242 }
243 var buf bytes.Buffer
244 restore := captureLogTo(&buf)
245 defer restore()
246 livenessSaid.Delete("incomplete")
247 for i := 0; i < 2; i++ {
248 snap, err := readLiveEdgesSnapshot(ctx, failingGet{Store: s, key: "status/boxes/uk-box.json"}, time.Now())
249 if err != nil {
250 t.Fatal(err)
251 }
252 if c, _ := findCandidate(regionCandidates(snap, "ie", testKeyFP), "uk"); c.Eligible {
253 t.Fatalf("round %d: uk eligible on an incomplete read", i)
254 }
255 }
256 if n := strings.Count(buf.String(), "incomplete"); n != 1 {
257 t.Fatalf("incomplete evidence logged %d times, want once: %s", n, buf.String())
258 }
259}
260
261// setEdges creates OR replaces edges files, and gives each box a FRESH
262// status snapshot: automatic eligibility needs one (F1), and the fixture
263// boxes are all meant to be live unless a test says otherwise through
264// setBoxSnapshot. putEdges is create-only (an empty ifMatch on DirStore and
265// S3), so it cannot change a box's file.
266func setEdges(t *testing.T, fx *consoleFixture, files map[string]string) {
267 t.Helper()
268 ctx := context.Background()
269 for name := range files {
270 setBoxSnapshot(t, fx.rel, name, time.Now())
271 }
272 for name, body := range files {
273 key := "control/edges/" + name + ".json"
274 etag := ""
275 if _, cur, err := fx.rel.Get(ctx, key); err == nil {
276 etag = cur
277 } else if !errors.Is(err, os.ErrNotExist) {
278 t.Fatal(err)
279 }
280 if _, err := fx.rel.Put(ctx, key, []byte(body), etag); err != nil {
281 t.Fatalf("set %s: %v", key, err)
282 }
283 }
284}
285
286// setBoxSnapshot writes (or rewrites) a box's status/boxes/<label>.json
287// generated at the given time: now for a live box, an hour ago for one that
288// has stopped reporting (status.BoxStale's rule).
289func setBoxSnapshot(t *testing.T, s release.Store, label string, at time.Time) {
290 t.Helper()
291 if err := status.WriteBox(context.Background(), s, status.Box{Label: label, GeneratedAt: at}); err != nil {
292 t.Fatalf("snapshot %s: %v", label, err)
293 }
294}
295
296// retireBox is what a retired box looks like in production (ruling (B)
297// reversed, re-review r2): its status snapshot lingers and goes STALE, and
298// here its edges file is gone too. Deleting the edges object alone would
299// leave a fresh snapshot with no file, which reads as a live box that has
300// not published (incomplete evidence), not as an absent region.
301func retireBox(t *testing.T, fx *consoleFixture, label string) {
302 t.Helper()
303 if err := fx.rel.Delete(context.Background(), "control/edges/"+label+".json"); err != nil && !errors.Is(err, os.ErrNotExist) {
304 t.Fatal(err)
305 }
306 setBoxSnapshot(t, fx.rel, label, time.Now().Add(-time.Hour))
307}
308
309type failingList struct{ release.Store }
310
311func (failingList) List(context.Context, string) ([]string, error) {
312 return nil, errors.New("bucket unreachable")
313}
314
315func captureLogTo(w io.Writer) (restore func()) {
316 prev, flags := log.Writer(), log.Flags()
317 log.SetOutput(w)
318 return func() { log.SetOutput(prev); log.SetFlags(flags) }
319}
320
321// views is a COMPLETE snapshot of the given edges-file bodies (every file
322// and every status snapshot read), every box FRESH (F1): the pure fold is
323// handed what readLiveEdgesSnapshot would produce for a fleet that is all
324// reporting and all readable. markStale takes one box back.
325func views(t *testing.T, bodies ...string) edgesSnapshot {
326 t.Helper()
327 snap := edgesSnapshot{Complete: true, BoxesComplete: true}
328 for _, b := range bodies {
329 var f edgesFilePlacementView
330 if err := json.Unmarshal([]byte(b), &f); err != nil {
331 t.Fatal(err)
332 }
333 f.Fresh = true
334 snap.Files = append(snap.Files, f)
335 }
336 return snap
337}
338
339// markStale is views' box that has stopped reporting: its edges file is
340// still in the snapshot, its status snapshot is not fresh.
341func markStale(snap edgesSnapshot, box string) edgesSnapshot {
342 for i := range snap.Files {
343 if snap.Files[i].Box == box {
344 snap.Files[i].Fresh = false
345 }
346 }
347 return snap
348}
349
350// F1: a retired box's edges file lingers (nothing deletes it), so the file
351// alone is not liveness. A non-home box counts toward automatic eligibility
352// only while its status snapshot is fresh, the rule edgesfile.EdgeIPs uses.
353// The existing disappearance test deletes the object; this one leaves it.
354func TestRegionCandidatesNeedAFreshBoxSnapshot(t *testing.T) {
355 ctx := context.Background()
356 s := release.NewDirStore(t.TempDir())
357 for key, body := range map[string]string{"ie-box": homeBox, "uk-box": ukEligible} {
358 if _, err := s.Put(ctx, "control/edges/"+key+".json", []byte(body), ""); err != nil {
359 t.Fatal(err)
360 }
361 }
362 now := time.Now()
363 setBoxSnapshot(t, s, "ie-box", now) // the home box reports too; see ruling (B) reversed
364 uk := func(name string) regionCandidate {
365 t.Helper()
366 snap, err := readLiveEdgesSnapshot(ctx, s, now)
367 if err != nil {
368 t.Fatalf("%s: %v", name, err)
369 }
370 c, _ := findCandidate(regionCandidates(snap, "ie", testKeyFP), "uk")
371 return c
372 }
373 // No snapshot at all: unknown liveness (incomplete evidence, ruling (B)
374 // reversed), so not eligible either way, and nothing live publishes uk.
375 if c := uk("no snapshot"); c.Eligible || c.unavailableReason() != "absent" {
376 t.Fatalf("no snapshot at all: uk = %+v, want absent", c)
377 }
378 setBoxSnapshot(t, s, "uk-box", now.Add(-time.Hour))
379 if c := uk("stale"); c.Eligible || c.unavailableReason() != "absent" {
380 t.Fatalf("stale snapshot, edges file still present: uk = %+v, want absent", c)
381 }
382 setBoxSnapshot(t, s, "uk-box", now)
383 if c := uk("fresh"); !c.Eligible {
384 t.Fatalf("fresh snapshot: uk = %+v, want eligible", c)
385 }
386 // The home candidate stays as it is: eligible throughout, exempt from
387 // every eligibility fact.
388 snap, _ := readLiveEdgesSnapshot(ctx, s, now)
389 if h, _ := findCandidate(regionCandidates(snap, "ie", testKeyFP), ""); !h.Eligible || h.AWSRegion != "eu-west-1" {
390 t.Fatalf("home = %+v", h)
391 }
392}
393
394// F1 leaves the explicit door where it was: R9's keyless proof is read
395// from the files, like the armed gate it sits beside, so a stale keyless
396// box still proves its region keyless (C31 on an explicit --region) while
397// automatic placement reads the same region as absent.
398func TestStaleBoxesStillCountForTheKeylessProof(t *testing.T) {
399 snap := markStale(views(t, homeBox, ukNoKey), "uk-box")
400 c, _ := findCandidate(regionCandidates(snap, "ie", testKeyFP), "uk")
401 if c.Eligible || c.unavailableReason() != "absent" || !c.NoKeyProven {
402 t.Fatalf("stale keyless uk = %+v, want absent and proven keyless", c)
403 }
404 // A stale KEYED box beside a fresh keyless one: not proven, as before.
405 keyless2 := edgeJSON("uk-box-2", "1.2.3.5", "eu-west-2", "uk", edgeOpts{hib: bp(true), shared: bp(true), inbound: bp(true)})
406 c, _ = findCandidate(regionCandidates(markStale(views(t, homeBox, ukEligible, keyless2), "uk-box"), "ie", testKeyFP), "uk")
407 if c.NoKeyProven || c.Eligible {
408 t.Fatalf("stale keyed beside fresh keyless = %+v, want neither proven nor eligible", c)
409 }
410}
411
412// F1: snapshots that cannot be loaded make no box fresh, and the failure
413// is never said twice. The once-per-process throttle lives in edgesfile
414// with the read (stage B Task 10), where TestReadLiveSnapshotSaysALoad
415// FailureOnce resets it; here an earlier test may already have tripped
416// it, so the count is at most one (Task 10 review M10).
417func TestLiveEdgesSnapshotLoadFailureIsNotFreshAndLoggedOnce(t *testing.T) {
418 ctx := context.Background()
419 s := release.NewDirStore(t.TempDir())
420 if _, err := s.Put(ctx, "control/edges/uk-box.json", []byte(ukEligible), ""); err != nil {
421 t.Fatal(err)
422 }
423 setBoxSnapshot(t, s, "uk-box", time.Now())
424 var buf bytes.Buffer
425 restore := captureLogTo(&buf)
426 defer restore()
427 for i := 0; i < 2; i++ {
428 snap, err := readLiveEdgesSnapshot(ctx, failingListPrefix{Store: s, prefix: "status/boxes/"}, time.Now())
429 if err != nil {
430 t.Fatal(err)
431 }
432 if c, _ := findCandidate(regionCandidates(snap, "ie", testKeyFP), "uk"); c.Eligible {
433 t.Fatalf("round %d: snapshots unloadable, uk = %+v, want ineligible", i, c)
434 }
435 }
436 // Count the load-failure line itself. "box snapshots" alone also
437 // matches the request-level incomplete-read line, which has its own
438 // once-per-process latch: the test passed only when an earlier test in
439 // the same process had already tripped it.
440 if n := strings.Count(buf.String(), "could not load box snapshots"); n > 1 {
441 t.Fatalf("load failure logged %d times, want at most once: %s", n, buf.String())
442 }
443}
444
445// failingListPrefix fails the listing of ONE prefix (the status half of the
446// join) and serves every other read.
447type failingListPrefix struct {
448 release.Store
449 prefix string
450}
451
452func (f failingListPrefix) List(ctx context.Context, prefix string) ([]string, error) {
453 if prefix == f.prefix {
454 return nil, errors.New("status bucket unreachable")
455 }
456 return f.Store.List(ctx, prefix)
457}
458
459func TestRegionCandidatesNeedPositiveEligibility(t *testing.T) {
460 ca := edgeJSON("ca-box", "7.7.7.7", "ca-central-1", "ca", edgeOpts{})
461 cands := regionCandidates(views(t, homeBox, ukEligible, auPoolOnly, usNoInbound, ca, nzNoHosting, jpNoEIP, fleetBoxFile), "ie", testKeyFP)
462 want := map[string]string{"": "", "uk": "", "au": "not-eligible", "us": "not-eligible",
463 "ca": "no-secrets-key", "nz": "not-hosting", "jp": "absent"}
464 if len(cands) != len(want) {
465 t.Fatalf("candidates = %+v, want exactly %v (no fleet code)", cands, want)
466 }
467 for code, reason := range want {
468 c, ok := findCandidate(cands, code)
469 if !ok {
470 t.Fatalf("no candidate %q", code)
471 }
472 if got := c.unavailableReason(); got != reason {
473 t.Errorf("%q: reason %q, want %q", code, got, reason)
474 }
475 }
476 if h, _ := findCandidate(cands, ""); !h.Home || h.AWSRegion != "eu-west-1" || !h.Eligible {
477 t.Errorf("home = %+v", h)
478 }
479}
480
481// Ruling F2, refined: the "no fresh box fails" side counts only boxes
482// reconcile would admit for the record, which is the same region code with
483// the shared pool served and inbound accepted (reconcile.go inScope and
484// the CARLOS_INBOUND=none refusal). A named-pool-only or no-inbound box can
485// never be handed the record, so it neither qualifies nor disqualifies the
486// region. ABSENT is not the same as false: a writer that predates the
487// field is on an unknown config, and reconcile's defaults (CARLOS_POOLS
488// unset serves the shared pool, CARLOS_INBOUND unset accepts) admit it, so
489// absent still counts as possibly admitted, and so as failing.
490func TestBoxesReconcileWouldNotAdmitNeitherQualifyNorDisqualify(t *testing.T) {
491 keyless := func(shared, inbound *bool) string {
492 return edgeJSON("uk-box-2", "1.2.3.5", "eu-west-2", "uk", edgeOpts{hib: bp(true), shared: shared, inbound: inbound})
493 }
494 harmless := map[string]string{
495 "keyless named-pool-only box": keyless(bp(false), bp(true)),
496 "keyless no-inbound box": keyless(bp(true), bp(false)),
497 "keyless neither": keyless(bp(false), bp(false)),
498 }
499 for name, body := range harmless {
500 cands := regionCandidates(views(t, homeBox, ukEligible, body), "ie", testKeyFP)
501 if c, _ := findCandidate(cands, "uk"); !c.Eligible {
502 t.Errorf("%s beside a keyed eligible box: uk = %+v, want eligible (reconcile would never admit it)", name, c)
503 }
504 if n := eligibleCount(cands); n != 2 {
505 t.Errorf("%s: %d eligible candidates, want the home and uk", name, n)
506 }
507 }
508 // Possibly admitted, so failing: a keyless box that serves the shared
509 // pool and takes inbound (the flipped mixed-box case), and one that
510 // is silent on either field.
511 hazard := map[string]string{
512 "keyless shared-pool inbound box": keyless(bp(true), bp(true)),
513 "keyless, shared pool absent": keyless(nil, bp(true)),
514 "keyless, inbound absent": keyless(bp(true), nil),
515 }
516 for name, body := range hazard {
517 if c, _ := findCandidate(regionCandidates(views(t, homeBox, ukEligible, body), "ie", testKeyFP), "uk"); c.Eligible {
518 t.Errorf("%s beside a keyed eligible box: uk = %+v, want ineligible", name, c)
519 }
520 }
521 // On its own, a box reconcile would not admit still makes no region
522 // eligible: nothing qualifies it. Keyed, so the reason is the pool.
523 poolOnlyKeyed := edgeJSON("uk-box-2", "1.2.3.5", "eu-west-2", "uk", edgeOpts{hib: bp(true), shared: bp(false), inbound: bp(true), fp: testKeyFP})
524 if c, _ := findCandidate(regionCandidates(views(t, homeBox, poolOnlyKeyed), "ie", testKeyFP), "uk"); c.Eligible || c.unavailableReason() != "not-eligible" {
525 t.Errorf("pool-only box alone: uk = %+v, want not-eligible", c)
526 }
527}
528
529// Task 10 (final review, MUST FIX): each capability field ABSENT on its
530// own, and each explicitly FALSE on its own, with everything else eligible
531// and the key present. The older fixtures that left fields absent also
532// lacked the key, so a permissive nil (absent read as yes) would have been
533// hidden behind the key check. Unknown is not yes (spec §6.1).
534func TestEachCapabilityFieldAloneBlocksEligibility(t *testing.T) {
535 cases := []struct {
536 name string
537 o edgeOpts
538 reason string
539 }{
540 {"hibernation absent", edgeOpts{shared: bp(true), inbound: bp(true), fp: testKeyFP}, "not-eligible"},
541 {"hibernation false", edgeOpts{hib: bp(false), shared: bp(true), inbound: bp(true), fp: testKeyFP}, "not-hosting"},
542 {"shared pool absent", edgeOpts{hib: bp(true), inbound: bp(true), fp: testKeyFP}, "not-eligible"},
543 {"shared pool false", edgeOpts{hib: bp(true), shared: bp(false), inbound: bp(true), fp: testKeyFP}, "not-eligible"},
544 {"inbound absent", edgeOpts{hib: bp(true), shared: bp(true), fp: testKeyFP}, "not-eligible"},
545 {"inbound false", edgeOpts{hib: bp(true), shared: bp(true), inbound: bp(false), fp: testKeyFP}, "not-eligible"},
546 }
547 for _, c := range cases {
548 box := edgeJSON("uk-box", "1.2.3.4", "eu-west-2", "uk", c.o)
549 cands := regionCandidates(views(t, homeBox, box), "ie", testKeyFP)
550 uk, ok := findCandidate(cands, "uk")
551 if !ok {
552 t.Fatalf("%s: no uk candidate", c.name)
553 }
554 if uk.Eligible || uk.unavailableReason() != c.reason {
555 t.Errorf("%s: uk = %+v (reason %q), want ineligible with reason %q", c.name, uk, uk.unavailableReason(), c.reason)
556 }
557 if n := eligibleCount(cands); n != 1 {
558 t.Errorf("%s: %d eligible candidates, want the home alone", c.name, n)
559 }
560 }
561 // The control: the same box with every field present and true is
562 // eligible, so the cases above fail on the one field they vary.
563 if uk, _ := findCandidate(regionCandidates(views(t, homeBox, ukEligible), "ie", testKeyFP), "uk"); !uk.Eligible {
564 t.Fatalf("control: uk = %+v, want eligible", uk)
565 }
566}
567
568// R9: a box must hold the deployment's secrets key to receive placed
569// instances, and only a CURRENT writer can prove it does not.
570func TestRegionCandidatesSecretsKey(t *testing.T) {
571 cases := []struct {
572 name string
573 body string
574 expected string
575 reason string
576 noKeyProven bool
577 }{
578 {"matching key", ukEligible, testKeyFP, "", false},
579 {"no key, current writer", ukNoKey, testKeyFP, "no-secrets-key", true},
580 {"wrong key, current writer", ukWrongKey, testKeyFP, "no-secrets-key", true},
581 {"old writer: unknown", ukOldWriter, testKeyFP, "no-secrets-key", false},
582 {"console cannot judge", ukEligible, "", "no-secrets-key", false},
583 }
584 for _, c := range cases {
585 cand, _ := findCandidate(regionCandidates(views(t, homeBox, c.body), "ie", c.expected), "uk")
586 if got := cand.unavailableReason(); got != c.reason || cand.NoKeyProven != c.noKeyProven {
587 t.Errorf("%s: reason %q proven %v, want %q %v", c.name, got, cand.NoKeyProven, c.reason, c.noKeyProven)
588 }
589 }
590 // One box with the key and one without: the region is not PROVEN
591 // keyless (the explicit door may still use it), but it is NOT eligible
592 // for automatic placement (final review F2). A record names a region,
593 // not a box, and reconcile admits every box in the region that serves
594 // the shared pool and takes inbound: the keyless box can win the lease
595 // and wake the instance with no config, secrets or store binding.
596 ukKeyless2 := edgeJSON("uk-box-2", "1.2.3.5", "eu-west-2", "uk", edgeOpts{hib: bp(true), shared: bp(true), inbound: bp(true)})
597 cand, _ := findCandidate(regionCandidates(views(t, homeBox, ukEligible, ukKeyless2), "ie", testKeyFP), "uk")
598 if cand.Eligible || cand.NoKeyProven || cand.unavailableReason() != "not-eligible" {
599 t.Errorf("mixed region = %+v, want ineligible (not-eligible) and not proven keyless", cand)
600 }
601}
602
603// F2: a region is automatically eligible only when at least one fresh box
604// is fully eligible AND no fresh box publishing the code fails the
605// predicate. One box reconcile would admit that cannot serve the record
606// takes the region out; stale boxes count for neither side.
607func TestEligibilityNeedsEveryFreshBoxToQualify(t *testing.T) {
608 sibling := func(o edgeOpts) string { return edgeJSON("uk-box-2", "1.2.3.5", "eu-west-2", "uk", o) }
609 keyed := func(hib, shared, inbound *bool) edgeOpts {
610 return edgeOpts{hib: hib, shared: shared, inbound: inbound, fp: testKeyFP}
611 }
612 bad := map[string]string{
613 "keyless": sibling(edgeOpts{hib: bp(true), shared: bp(true), inbound: bp(true)}),
614 "wrong key": sibling(edgeOpts{hib: bp(true), shared: bp(true), inbound: bp(true), fp: "sha256:" + strings.Repeat("0", 64)}),
615 "hibernation false": sibling(keyed(bp(false), bp(true), bp(true))),
616 "hibernation absent": sibling(keyed(nil, bp(true), bp(true))),
617 "shared pool absent": sibling(keyed(bp(true), nil, bp(true))),
618 "inbound absent": sibling(keyed(bp(true), bp(true), nil)),
619 "old writer, every field": sibling(edgeOpts{}),
620 "no dial address": edgeJSON("uk-box-2", "", "eu-west-2", "uk", eligibleBox),
621 }
622 // A sibling that POSITIVELY publishes serves_shared_pool false or
623 // accepts_inbound false is one reconcile would never hand the record:
624 // see TestBoxesReconcileWouldNotAdmitNeitherQualifyNorDisqualify.
625 for name, body := range bad {
626 cands := regionCandidates(views(t, homeBox, ukEligible, body), "ie", testKeyFP)
627 c, _ := findCandidate(cands, "uk")
628 if c.Eligible || c.unavailableReason() == "" {
629 t.Errorf("%s sibling: uk = %+v, want ineligible", name, c)
630 }
631 if n := eligibleCount(cands); n != 1 {
632 t.Errorf("%s sibling: %d eligible candidates, want the home alone", name, n)
633 }
634 // The diagnostics still describe the boxes that ARE there: the
635 // eligible box is reachable, hosting and keyed, so the C16 word is
636 // not-eligible, never absent.
637 if c.unavailableReason() == "absent" {
638 t.Errorf("%s sibling: uk read as absent with a reachable box present", name)
639 }
640 }
641 // Two fully eligible boxes: eligible.
642 if c, _ := findCandidate(regionCandidates(views(t, homeBox, ukEligible, sibling(eligibleBox)), "ie", testKeyFP), "uk"); !c.Eligible {
643 t.Fatalf("two eligible boxes: uk = %+v, want eligible", c)
644 }
645 // A STALE failing sibling counts for neither side: the live box alone
646 // decides, and the region stays eligible.
647 if c, _ := findCandidate(regionCandidates(markStale(views(t, homeBox, ukEligible, bad["keyless"]), "uk-box-2"), "ie", testKeyFP), "uk"); !c.Eligible {
648 t.Fatalf("stale keyless sibling: uk = %+v, want eligible", c)
649 }
650 // A stale ELIGIBLE box beside a fresh failing one lends nothing.
651 if c, _ := findCandidate(regionCandidates(markStale(views(t, homeBox, ukEligible, bad["keyless"]), "uk-box"), "ie", testKeyFP), "uk"); c.Eligible || c.unavailableReason() != "no-secrets-key" {
652 t.Fatalf("stale eligible, fresh keyless: uk = %+v, want no-secrets-key", c)
653 }
654}
655
656// codex plan-R2 1: a negative proof needs EVERY box. With one file
657// unreadable or malformed, a readable keyless box proves nothing.
658func TestNoKeyProvenNeedsACompleteRead(t *testing.T) {
659 incomplete := views(t, homeBox, ukNoKey)
660 incomplete.Complete = false
661 if c, _ := findCandidate(regionCandidates(incomplete, "ie", testKeyFP), "uk"); c.NoKeyProven {
662 t.Fatal("an incomplete read proved a region keyless")
663 }
664 ctx := context.Background()
665 for name, extra := range map[string]string{"malformed": "{not json", "unversioned": `{"box":"uk-box-2"}`} {
666 s := release.NewDirStore(t.TempDir())
667 for key, body := range map[string]string{"ie-box": homeBox, "uk-box": ukNoKey, "uk-box-2": extra} {
668 if _, err := s.Put(ctx, "control/edges/"+key+".json", []byte(body), ""); err != nil {
669 t.Fatal(err)
670 }
671 }
672 snap, err := readEdgesSnapshot(ctx, s)
673 if err != nil || snap.Complete {
674 t.Fatalf("%s: snapshot complete=%v err=%v, want incomplete", name, snap.Complete, err)
675 }
676 if c, _ := findCandidate(regionCandidates(snap, "ie", testKeyFP), "uk"); c.NoKeyProven {
677 t.Fatalf("%s: proved keyless with a box unassessed", name)
678 }
679 }
680 s := release.NewDirStore(t.TempDir())
681 for key, body := range map[string]string{"ie-box": homeBox, "uk-box": ukNoKey, "uk-box-2": ukEligible} {
682 if _, err := s.Put(ctx, "control/edges/"+key+".json", []byte(body), ""); err != nil {
683 t.Fatal(err)
684 }
685 }
686 snap, err := readEdgesSnapshot(ctx, failingGet{Store: s, key: "control/edges/uk-box-2.json"})
687 if err != nil || snap.Complete {
688 t.Fatalf("failed GET: complete=%v err=%v, want incomplete", snap.Complete, err)
689 }
690 if c, _ := findCandidate(regionCandidates(snap, "ie", testKeyFP), "uk"); c.NoKeyProven {
691 t.Fatal("a failed GET let the readable keyless box prove the region keyless")
692 }
693}
694
695type failingGet struct {
696 release.Store
697 key string
698}
699
700func (f failingGet) Get(ctx context.Context, key string) ([]byte, string, error) {
701 if key == f.key {
702 return nil, "", errors.New("read timeout")
703 }
704 return f.Store.Get(ctx, key)
705}
706
707// codex plan-R2 2: a candidate's coordinates come from its fully eligible
708// boxes only; ties among them go to the lexically smallest AWS region. The
709// mixed region is no longer eligible (F2), but it is still NAMED (placeOf,
710// C16's refusal) from the box that qualifies, not from the ones that do not.
711func TestCandidateLocationComesFromEligibleBoxes(t *testing.T) {
712 syd1 := edgeJSON("uk-syd-1", "2.2.2.1", "ap-southeast-2", "uk", edgeOpts{hib: bp(true), shared: bp(true), inbound: bp(true)})
713 syd2 := edgeJSON("uk-syd-2", "2.2.2.2", "ap-southeast-2", "uk", edgeOpts{hib: bp(true), shared: bp(true), inbound: bp(false), fp: testKeyFP})
714 c, _ := findCandidate(regionCandidates(views(t, homeBox, ukEligible, syd1, syd2), "ie", testKeyFP), "uk")
715 if c.Eligible || c.AWSRegion != "eu-west-2" {
716 t.Fatalf("mixed eligibility = %+v, want ineligible (F2) and located at the qualifying box's eu-west-2", c)
717 }
718 dub := edgeJSON("uk-dub", "2.2.2.3", "eu-west-1", "uk", eligibleBox)
719 if c, _ := findCandidate(regionCandidates(views(t, homeBox, ukEligible, dub), "ie", testKeyFP), "uk"); c.AWSRegion != "eu-west-1" {
720 t.Fatalf("eligible tie = %q, want the lexically smallest eu-west-1", c.AWSRegion)
721 }
722}
723
724func TestRegionCandidatesFoldTheHomesName(t *testing.T) {
725 coded := edgeJSON("ie2", "5.6.7.9", "eu-west-1", "ie", eligibleBox)
726 cands := regionCandidates(views(t, homeBox, coded), "ie", testKeyFP)
727 if len(cands) != 1 || !cands[0].Home {
728 t.Fatalf("candidates = %+v, want the home alone", cands)
729 }
730}
731
732// spec §6.3, §6.5: a candidate with no point is logged once per code, and
733// the line carries no caller coordinates (there are none to carry).
734func TestRegionCandidatesLogAMissingPointOnce(t *testing.T) {
735 var buf bytes.Buffer
736 restore := captureLogTo(&buf)
737 defer restore()
738 noPointSaid.Delete("odd") // the throttle is process-wide; -count=N reruns this test
739 odd := edgeJSON("odd-box", "3.3.3.3", "xx-nowhere-1", "odd", eligibleBox)
740 regionCandidates(views(t, homeBox, odd), "ie", testKeyFP)
741 regionCandidates(views(t, homeBox, odd), "ie", testKeyFP)
742 if n := strings.Count(buf.String(), `region "odd"`); n != 1 {
743 t.Fatalf("missing-point line logged %d times, want once: %s", n, buf.String())
744 }
745}
746
747func TestReadEdgesSnapshotReportsListFailure(t *testing.T) {
748 if _, err := readEdgesSnapshot(context.Background(), failingList{}); err == nil {
749 t.Fatal("a failed listing must be an error, not an empty answer (codex R2-2)")
750 }
751 snap, err := readEdgesSnapshot(context.Background(), release.NewDirStore(t.TempDir()))
752 if err != nil || len(snap.Files) != 0 || !snap.Complete {
753 t.Fatalf("an empty prefix = %+v %v, want complete and empty", snap, err)
754 }
755}
756
757func TestAppPolicyStates(t *testing.T) {
758 fx, _ := instanceFixture(t, "code-policy")
759 acct := &Account{ID: OpsAccountID, Sqid: "bab"}
760 ctx := context.Background()
761 if p := fx.srv.appPolicy(ctx, acct, "titogo"); p.State != policyAbsent || !p.allows("uk") {
762 t.Fatalf("no manifest = %+v", p)
763 }
764 m := titoManifest()
765 m.Pool, m.Regions = "", []string{"uk"}
766 enableInstances(t, fx, "titogo", m)
767 if p := fx.srv.appPolicy(ctx, acct, "titogo"); p.State != policyKnown || p.allows("") || !p.allows("uk") || p.pooled() {
768 t.Fatalf("known = %+v", p)
769 }
770 m.Pool = "tito"
771 enableInstances(t, fx, "titogo", m)
772 if !fx.srv.appPolicy(ctx, acct, "titogo").pooled() {
773 t.Fatal("a pooled manifest must read as pooled")
774 }
775 if err := fx.st.SetAppPlacement(OpsAccountID, "titogo", "fleet/acme"); err != nil {
776 t.Fatal(err)
777 }
778 if p := fx.srv.appPolicy(ctx, acct, "titogo"); p.Placed != "fleet/acme" {
779 t.Fatalf("placed = %q", p.Placed)
780 }
781 _, etag, err := fx.rel.Get(ctx, provision.ManifestKey("bab", "titogo"))
782 if err != nil {
783 t.Fatal(err)
784 }
785 if _, err := fx.rel.Put(ctx, provision.ManifestKey("bab", "titogo"), []byte("{not json"), etag); err != nil {
786 t.Fatal(err)
787 }
788 if p := fx.srv.appPolicy(ctx, acct, "titogo"); p.State != policyUnreadable || p.allows("") {
789 t.Fatalf("unreadable = %+v, want unreadable and nothing allowed", p)
790 }
791 fx.srv.cfg.Releases = nil
792 if p := fx.srv.appPolicy(ctx, acct, "titogo"); p.State != policyUnreadable || p.allows("uk") {
793 t.Fatalf("no bucket = %+v, want unreadable and nothing allowed", p)
794 }
795}
796
797// splitCapability pairs two boxes in one region that between them publish
798// every eligibility fact while neither publishes them all (codex
799// plan-R3 4). Task 11 reuses it for the guess.
800var splitCapability = map[string][2]string{
801 "keyless ordinary box beside a keyed named-pool-only box": {
802 edgeJSON("uk-a", "1.2.3.4", "eu-west-2", "uk", edgeOpts{hib: bp(true), shared: bp(true), inbound: bp(true)}),
803 edgeJSON("uk-b", "1.2.3.5", "eu-west-2", "uk", edgeOpts{hib: bp(true), shared: bp(false), inbound: bp(true), fp: testKeyFP}),
804 },
805 "keyed no-inbound box beside a keyless inbound box": {
806 edgeJSON("uk-a", "1.2.3.4", "eu-west-2", "uk", edgeOpts{hib: bp(true), shared: bp(true), inbound: bp(false), fp: testKeyFP}),
807 edgeJSON("uk-b", "1.2.3.5", "eu-west-2", "uk", edgeOpts{hib: bp(true), shared: bp(true), inbound: bp(true)}),
808 },
809 "keyed non-hosting box beside a keyless hosting box": {
810 edgeJSON("uk-a", "1.2.3.4", "eu-west-2", "uk", edgeOpts{hib: bp(false), shared: bp(true), inbound: bp(true), fp: testKeyFP}),
811 edgeJSON("uk-b", "1.2.3.5", "eu-west-2", "uk", edgeOpts{hib: bp(true), shared: bp(true), inbound: bp(true)}),
812 },
813}
814
815// Eligibility is ONE box's facts, never the region's union of them.
816func TestEligibilityNeverCombinesBoxes(t *testing.T) {
817 for name, boxes := range splitCapability {
818 cands := regionCandidates(views(t, homeBox, boxes[0], boxes[1]), "ie", testKeyFP)
819 if c, _ := findCandidate(cands, "uk"); c.Eligible || c.unavailableReason() == "" {
820 t.Errorf("%s: uk = %+v, want ineligible", name, c)
821 }
822 if n := eligibleCount(cands); n != 1 {
823 t.Errorf("%s: %d eligible candidates, want the home alone", name, n)
824 }
825 }
826}
827
828// candsOrHome answers from this console's home and secrets key, and falls
829// back to the home alone when the listing fails or there is no bucket.
830func TestCandsOrHomeUsesTheServerAndFallsBackToTheHome(t *testing.T) {
831 fx := newConsoleFixture(t)
832 ctx := context.Background()
833 fx.srv.cfg.HomeRegion, fx.srv.cfg.SecretsPub = "ie", testKeyPub
834 setEdges(t, fx, map[string]string{"ie-box": homeBox, "uk-box": ukEligible, "ie2": edgeJSON("ie2", "5.6.7.9", "eu-west-1", "ie", eligibleBox)})
835 cands := fx.srv.candsOrHome(ctx)
836 if len(cands) != 2 || eligibleCount(cands) != 2 {
837 t.Fatalf("candidates = %+v, want the home (ie folded in) and uk, both eligible", cands)
838 }
839 fx.srv.cfg.SecretsPub = ""
840 if c, _ := findCandidate(fx.srv.candsOrHome(ctx), "uk"); c.Eligible || c.unavailableReason() != "no-secrets-key" {
841 t.Fatalf("no console key: uk = %+v, want no-secrets-key", c)
842 }
843 for name, store := range map[string]release.Store{"failed listing": failingList{Store: fx.rel}, "no bucket": nil} {
844 fx.srv.cfg.Releases = store
845 if cands := fx.srv.candsOrHome(ctx); len(cands) != 1 || !cands[0].Home || !cands[0].Eligible {
846 t.Errorf("%s: candidates = %+v, want the home alone", name, cands)
847 }
848 }
849}
850
851// O6, O7 and O13 to O15 are this file's diagnostics, taken verbatim from
852// the approved copy record and compared as quoted literals.
853func TestRegionCandidatesCopyMatchesTheRecord(t *testing.T) {
854 raw, err := os.ReadFile("../../docs/superpowers/plans/2026-10-02-nearest-region-copy.json")
855 if err != nil {
856 t.Fatal(err)
857 }
858 var rec struct {
859 Entries []struct {
860 ID, File, Text string
861 } `json:"entries"`
862 }
863 if err := json.Unmarshal(raw, &rec); err != nil {
864 t.Fatal(err)
865 }
866 src, err := os.ReadFile("regioncandidates.go")
867 if err != nil {
868 t.Fatal(err)
869 }
870 var ids []string
871 for _, e := range rec.Entries {
872 if e.File != "internal/console/regioncandidates.go" {
873 continue
874 }
875 ids = append(ids, e.ID)
876 if !strings.Contains(string(src), strconv.Quote(e.Text)) {
877 t.Errorf("%s: regioncandidates.go has no literal %s", e.ID, strconv.Quote(e.Text))
878 }
879 }
880 if got := strings.Join(ids, ","); got != "O6,O7,O13,O14,O15" {
881 t.Errorf("the record maps %s to regioncandidates.go, want O6,O7,O13,O14,O15", got)
882 }
883}
884
885func TestEdgesViewParsesBoxEvidenceAndToleratesItsAbsence(t *testing.T) {
886 ctx, s := context.Background(), release.NewDirStore(t.TempDir())
887 with := `{"v":1,"box":"i-0abc","eip":"1.2.3.4","region":"eu-west-2","placement":"uk","hosts":{},"box_id":"i-0abc","capabilities":["region-ha-1"],"draining":true,"headroom":{"mem_free_mib":2210,"disk_free_mib":31200,"awake":14}}`
888 without := `{"v":1,"box":"old-box","eip":"5.6.7.8","region":"eu-west-1","hosts":{}}`
889 for k, b := range map[string]string{"i-0abc": with, "old-box": without} {
890 if _, err := s.Put(ctx, "control/edges/"+k+".json", []byte(b), ""); err != nil {
891 t.Fatal(err)
892 }
893 }
894 snap, err := readEdgesSnapshot(ctx, s)
895 if err != nil || !snap.Complete {
896 t.Fatalf("snap complete=%v err=%v", snap.Complete, err)
897 }
898 by := map[string]edgesFilePlacementView{}
899 for _, f := range snap.Files {
900 by[f.Box] = f
901 }
902 n := by["i-0abc"]
903 if n.BoxID != "i-0abc" || len(n.Capabilities) != 1 || !n.Draining || n.Headroom == nil || n.Headroom.Awake != 14 {
904 t.Fatalf("new box parsed as %+v", n)
905 }
906 o := by["old-box"]
907 if o.BoxID != "" || o.Capabilities != nil || o.Draining || o.Headroom != nil {
908 t.Fatalf("old box parsed as %+v", o)
909 }
910}
911