package console import ( "context" "encoding/json" "testing" ) // Hosting is answered PER BOX, from that box's own hibernation_configured // field, and is never inferred from what another box publishes. An earlier // version keyed hosting on data_bucket alone, and because an absent // data_bucket is what an OLD WRITER and a bucket-less new box both produce, // it decided when silence meant "no" by asking whether any OTHER box had // spoken. One box adopting a bucket-publishing build then withdrew every // region that had not yet rolled, all of them healthy. These tests pin the // three states apart. func TestPlacementsAnswersHostingPerBox(t *testing.T) { fx, _ := instanceFixture(t, "code-inst-placementcaps") ctx := context.Background() put := func(key, body string) { t.Helper() if _, err := fx.rel.Put(ctx, key, []byte(body), ""); err != nil { t.Fatal(err) } } // uk: an old writer — no hibernation_configured, no data_bucket. UNKNOWN, // and unknown keeps the eligibility it had before the field existed. put("control/edges/uk-box.json", `{"v":1,"box":"uk-box","eip":"1.2.3.4","placement":"uk","hosts":{}}`) // ie: a new writer that can host. put("control/edges/ie-box.json", `{"v":1,"box":"ie-box","eip":"5.6.7.8","placement":"ie","data_bucket":"carlos-ie","hibernation_configured":true,"hosts":{}}`) // de: a new writer that CANNOT host, and says so explicitly. This is the // state no earlier version could express. put("control/edges/de-box.json", `{"v":1,"box":"de-box","eip":"9.9.9.9","placement":"de","hibernation_configured":false,"hosts":{}}`) // se: reachable by nothing. put("control/edges/se-box.json", `{"v":1,"box":"se-box","eip":"","placement":"se","data_bucket":"carlos-se","hibernation_configured":true,"hosts":{}}`) caps, published := placements(ctx, fx.rel) if !published { t.Fatal("published = false, want true") } // The whole point: ie publishing a bucket must not make uk's silence // mean "no". uk stays eligible until uk itself says otherwise. if !caps["uk"].Hosting { t.Error("uk not hosting: it is an OLD WRITER with no opinion, and one box's upgrade must not withdraw another box's region") } if !caps["ie"].Hosting || !caps["ie"].Staging { t.Errorf("ie = %+v, want hosting and staging", caps["ie"]) } if caps["de"].Hosting { t.Error("de hosting: it published hibernation_configured false, which is an explicit no") } if !caps["de"].Reachable { t.Error("de unreachable: it has an EIP — a box that cannot host is still an edge") } if caps["se"].Reachable || caps["se"].Hosting || caps["se"].Staging { t.Errorf("se = %+v, want nothing: no EIP gates every capability", caps["se"]) } // Staging is strictly stronger than Hosting, and uk is where they part: // eligible to host (unknown), but publishing no bucket coordinates, so // no move can name it — internal/host's moveTargets would drop it and // the worker would wait forever. if caps["uk"].Staging { t.Error("uk staging: it publishes no data_bucket, so a move there could never be prepared") } } // An explicit false is believed even when a SIBLING box in the same region // can host: the fold is an OR across boxes, so one capable box arms the // region, and that is correct — the record only needs one box to land on. func TestPlacementsFoldsAcrossBoxesInARegion(t *testing.T) { fx, _ := instanceFixture(t, "code-inst-placementfold") ctx := context.Background() put := func(key, body string) { t.Helper() if _, err := fx.rel.Put(ctx, key, []byte(body), ""); err != nil { t.Fatal(err) } } put("control/edges/uk-a.json", `{"v":1,"box":"uk-a","eip":"1.1.1.1","placement":"uk","hibernation_configured":false,"hosts":{}}`) put("control/edges/uk-b.json", `{"v":1,"box":"uk-b","eip":"2.2.2.2","placement":"uk","data_bucket":"carlos-uk","hibernation_configured":true,"hosts":{}}`) caps, _ := placements(ctx, fx.rel) if !caps["uk"].Hosting || !caps["uk"].Staging { t.Errorf("uk = %+v, want hosting and staging: one capable box in the region is enough", caps["uk"]) } } // The dormancy that remains is the one the placement field itself shipped // with, and it is about placement codes, not about hosting: a fleet where no // box publishes a code at all leaves the create gate entirely dormant. func TestPlacementsDormantWithoutAnyPlacementCode(t *testing.T) { fx, _ := instanceFixture(t, "code-inst-placementdormant") ctx := context.Background() if _, err := fx.rel.Put(ctx, "control/edges/solo.json", []byte(`{"v":1,"box":"solo","eip":"1.2.3.4","hosts":{}}`), ""); err != nil { t.Fatal(err) } if _, published := placements(ctx, fx.rel); published { t.Error("published = true with no placement code anywhere — the gate must stay dormant") } } // The seeds fields are additive: an edges file from a box that predates // them parses, and reads as no protocol, no floor and no verdict. func TestEdgesFilePlacementViewSeedsFields(t *testing.T) { var old edgesFilePlacementView if err := json.Unmarshal([]byte(`{"box":"kass-1","moves_protocol":1}`), &old); err != nil { t.Fatal(err) } if old.SeedsProtocol != 0 || old.SeedsFloorMinRelease != "" || old.PrevSeedsAllowedFor != "" { t.Fatalf("old writer = %+v", old) } var cur edgesFilePlacementView body := `{"box":"kass-1","seeds_protocol":2,"seeds_floor_min_release":"v0.27.0","prev_seeds_allowed_for":"v0.27.0"}` if err := json.Unmarshal([]byte(body), &cur); err != nil { t.Fatal(err) } if cur.SeedsProtocol != 2 || cur.SeedsFloorMinRelease != "v0.27.0" || cur.PrevSeedsAllowedFor != "v0.27.0" { t.Fatalf("current writer = %+v", cur) } }