console: only a box reconcile would admit can take a region out of automatic eligibility (Ruling F2, refined)
On the "no fresh box fails" side, count only boxes reconcile would admit for the record: the same region code with the shared pool served and inbound accepted, mirroring reconcile.go's inScope (poolServed(cfg.Pools, "")) and its CARLOS_INBOUND=none refusal, the two facts SharedInstanceEligibility publishes. A box that positively publishes serves_shared_pool false (named-pool-only) or accepts_inbound false can never be handed the record, so it neither qualifies nor disqualifies the region. Absent is not false: reconcile reads its own config, never the edges file, and its defaults (CARLOS_POOLS unset serves the shared pool, CARLOS_INBOUND unset accepts) admit a box, so a writer silent on either field counts as possibly admitted and still fails. The "some fresh box qualifies" side is unchanged. Tests: a fresh keyless named-pool-only box beside a fresh keyed eligible box leaves the region eligible (RED against 82a6369d); a fresh keyless shared-pool inbound box beside the same box does not; a silent field does not either.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2 files changed,
+72
−7
internal/console/regioncandidates.go+21 −5internal/console/regioncandidates_test.go+51 −2
diff --git a/internal/console/regioncandidates.go b/internal/console/regioncandidates.go| index 24d8f017f..5530bc736 100644 |
| --- a/internal/console/regioncandidates.go |
| +++ b/internal/console/regioncandidates.go |
| @@ -47,6 +47,9 @@ func (c regionCandidate) unavailableReason() string { |
| func isTrue(b *bool) bool { return b != nil && *b } |
| +// isFalse is a field the box PUBLISHED as false; absent is unknown, not no. |
| +func isFalse(b *bool) bool { return b != nil && !*b } |
| + |
| // regionCandidates folds an edges snapshot into candidates. expectedFP is |
| // the console's own secrets fingerprint; "" means it cannot judge, and then |
| // no coded region is KeyOK. The home is exempt from every eligibility fact. |
| @@ -59,8 +62,8 @@ func isTrue(b *bool) bool { return b != nil && *b } |
| // - at least one fresh box publishing it passes the whole predicate: |
| // a dial address, hibernation_configured, serves_shared_pool and |
| // accepts_inbound all positively true, and the deployment's key; |
| -// - no fresh box publishing it fails that predicate. Absent and unknown |
| -// fields fail it. |
| +// - no fresh box publishing it that reconcile would ADMIT for the record |
| +// fails that predicate. Absent and unknown fields fail it. |
| // |
| // A record names a region, not a box. internal/provision's reconcile pass |
| // (inScope, and the CARLOS_INBOUND=none refusal) admits every box whose |
| @@ -68,8 +71,15 @@ func isTrue(b *bool) bool { return b != nil && *b } |
| // inbound; it never asks whether the box holds the key or has its |
| // activator wired, so a keyless or activator-less sibling can win the lease |
| // and wake the instance with nothing. Every box that could serve the record |
| -// must therefore be able to. Stale boxes count for neither side (F1): they |
| -// are not evidence that a region works, and not evidence that it is broken. |
| +// must therefore be able to. A box that POSITIVELY publishes |
| +// serves_shared_pool false (a named-pool-only box) or accepts_inbound |
| +// false (CARLOS_INBOUND=none) can never be handed the record, so it neither |
| +// qualifies nor disqualifies the region (Ruling F2, refined). Absent is not |
| +// false: a writer predating the field is on an unknown config, and |
| +// reconcile's defaults (CARLOS_POOLS unset serves the shared pool, |
| +// CARLOS_INBOUND unset accepts) admit it, so it counts as possibly |
| +// admitted and fails. Stale boxes count for neither side (F1): they are |
| +// not evidence that a region works, and not evidence that it is broken. |
| func regionCandidates(snap edgesSnapshot, homeName, expectedFP string) []regionCandidate { |
| type acc struct { |
| c regionCandidate |
| @@ -130,7 +140,13 @@ func regionCandidates(snap edgesSnapshot, homeName, expectedFP string) []regionC |
| // nothing can reach it. It just does not make the region Reachable. |
| eligible := reachable && isTrue(f.HibernationConfigured) && isTrue(f.ServesSharedPool) && isTrue(f.AcceptsInbound) && keyOK |
| a.anyEligible = a.anyEligible || eligible |
| - a.allEligible = a.allEligible && eligible |
| + // Only a box reconcile would admit can take the region out: one |
| + // that does not positively refuse the shared pool or inbound. |
| + // This mirrors poolServed(cfg.Pools, "") and cfg.Inbound != |
| + // InboundNone, the two facts SharedInstanceEligibility publishes. |
| + if !isFalse(f.ServesSharedPool) && !isFalse(f.AcceptsInbound) { |
| + a.allEligible = a.allEligible && eligible |
| + } |
| if !reachable { |
| continue |
| } |
diff --git a/internal/console/regioncandidates_test.go b/internal/console/regioncandidates_test.go| index 9731dee64..1bb2037a9 100644 |
| --- a/internal/console/regioncandidates_test.go |
| +++ b/internal/console/regioncandidates_test.go |
| @@ -261,6 +261,54 @@ func TestRegionCandidatesNeedPositiveEligibility(t *testing.T) { |
| } |
| } |
| +// Ruling F2, refined: the "no fresh box fails" side counts only boxes |
| +// reconcile would admit for the record, which is the same region code with |
| +// the shared pool served and inbound accepted (reconcile.go inScope and |
| +// the CARLOS_INBOUND=none refusal). A named-pool-only or no-inbound box can |
| +// never be handed the record, so it neither qualifies nor disqualifies the |
| +// region. ABSENT is not the same as false: a writer that predates the |
| +// field is on an unknown config, and reconcile's defaults (CARLOS_POOLS |
| +// unset serves the shared pool, CARLOS_INBOUND unset accepts) admit it, so |
| +// absent still counts as possibly admitted, and so as failing. |
| +func TestBoxesReconcileWouldNotAdmitNeitherQualifyNorDisqualify(t *testing.T) { |
| + keyless := func(shared, inbound *bool) string { |
| + return edgeJSON("uk-box-2", "1.2.3.5", "eu-west-2", "uk", edgeOpts{hib: bp(true), shared: shared, inbound: inbound}) |
| + } |
| + harmless := map[string]string{ |
| + "keyless named-pool-only box": keyless(bp(false), bp(true)), |
| + "keyless no-inbound box": keyless(bp(true), bp(false)), |
| + "keyless neither": keyless(bp(false), bp(false)), |
| + } |
| + for name, body := range harmless { |
| + cands := regionCandidates(views(t, homeBox, ukEligible, body), "ie", testKeyFP) |
| + if c, _ := findCandidate(cands, "uk"); !c.Eligible { |
| + t.Errorf("%s beside a keyed eligible box: uk = %+v, want eligible (reconcile would never admit it)", name, c) |
| + } |
| + if n := eligibleCount(cands); n != 2 { |
| + t.Errorf("%s: %d eligible candidates, want the home and uk", name, n) |
| + } |
| + } |
| + // Possibly admitted, so failing: a keyless box that serves the shared |
| + // pool and takes inbound (the flipped mixed-box case), and one that |
| + // is silent on either field. |
| + hazard := map[string]string{ |
| + "keyless shared-pool inbound box": keyless(bp(true), bp(true)), |
| + "keyless, shared pool absent": keyless(nil, bp(true)), |
| + "keyless, inbound absent": keyless(bp(true), nil), |
| + } |
| + for name, body := range hazard { |
| + if c, _ := findCandidate(regionCandidates(views(t, homeBox, ukEligible, body), "ie", testKeyFP), "uk"); c.Eligible { |
| + t.Errorf("%s beside a keyed eligible box: uk = %+v, want ineligible", name, c) |
| + } |
| + } |
| + // On its own, a box reconcile would not admit still makes no region |
| + // eligible: nothing qualifies it. Keyed, so the reason is the pool. |
| + 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}) |
| + if c, _ := findCandidate(regionCandidates(views(t, homeBox, poolOnlyKeyed), "ie", testKeyFP), "uk"); c.Eligible || c.unavailableReason() != "not-eligible" { |
| + t.Errorf("pool-only box alone: uk = %+v, want not-eligible", c) |
| + } |
| +} |
| + |
| // Task 10 (final review, MUST FIX): each capability field ABSENT on its |
| // own, and each explicitly FALSE on its own, with everything else eligible |
| // and the key present. The older fixtures that left fields absent also |
| @@ -349,13 +397,14 @@ func TestEligibilityNeedsEveryFreshBoxToQualify(t *testing.T) { |
| "wrong key": sibling(edgeOpts{hib: bp(true), shared: bp(true), inbound: bp(true), fp: "sha256:" + strings.Repeat("0", 64)}), |
| "hibernation false": sibling(keyed(bp(false), bp(true), bp(true))), |
| "hibernation absent": sibling(keyed(nil, bp(true), bp(true))), |
| - "shared pool false": sibling(keyed(bp(true), bp(false), bp(true))), |
| "shared pool absent": sibling(keyed(bp(true), nil, bp(true))), |
| - "inbound false": sibling(keyed(bp(true), bp(true), bp(false))), |
| "inbound absent": sibling(keyed(bp(true), bp(true), nil)), |
| "old writer, every field": sibling(edgeOpts{}), |
| "no dial address": edgeJSON("uk-box-2", "", "eu-west-2", "uk", eligibleBox), |
| } |
| + // A sibling that POSITIVELY publishes serves_shared_pool false or |
| + // accepts_inbound false is one reconcile would never hand the record: |
| + // see TestBoxesReconcileWouldNotAdmitNeitherQualifyNorDisqualify. |
| for name, body := range bad { |
| cands := regionCandidates(views(t, homeBox, ukEligible, body), "ie", testKeyFP) |
| c, _ := findCandidate(cands, "uk") |