carlos / platform Public

settings-5 task 8: fix round 1 (held wake reads untagged decisions, effective-account compare, fail closed on registry errors)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 3e24809fe69cede8541798ee8925d69e2486f29f parent 1b60322
8 files changed, +267 −43
  • internal/host/buildstate.go +18 −9
  • internal/host/buildstate_astra_test.go +27 −12
  • internal/host/buildstate_test.go +1 −2
  • internal/host/tenantchange_test.go +100 −9
  • internal/registry/registry.go +43 −7
  • internal/registry/updatesettings_test.go +61 −0
  • internal/release/buildstate.go +5 −4
  • internal/release/tenantchange_test.go +12 −0
diff --git a/internal/host/buildstate.go b/internal/host/buildstate.go
index f81f46d92..a1c56d2fa 100644
--- a/internal/host/buildstate.go
+++ b/internal/host/buildstate.go
@@ -519,8 +519,15 @@ func (b *buildStates) beforeExec(ctx context.Context, host, build string) error
}
// The held target is revalidated against the state read under the lease
// (Astra round 1 finding 1): a wake that decided on a stale copy is
- // refused here, before anything is written or run.
- rt, _ := b.reg.Get(host)
+ // refused here, before anything is written or run. A route that does
+ // not read refuses the exec before any write (Astra r4 finding 3): the
+ // tenant check below needs it, and an unread tenant must not merge a
+ // hostname-keyed file into the state. A route that is absent (nil, no
+ // error) is read as before.
+ rt, err := b.reg.Get(host)
+ if err != nil {
+ return fmt.Errorf("read the route of %s: %w", host, err)
+ }
// A file root wrote for another tenant of this hostname carries nothing
// into this tenant's state (ruling R26). An untagged one is read as the
// route's own, as before.
@@ -661,15 +668,17 @@ func (b *buildStates) heldTarget(host string) activator.HeldWake {
// heldWakeInput reads DecideHeldWake's input for rt. state, when set, is
// the build state just read under the lease (beforeExec's revalidation), in
// place of the registry copy. A consumed file or refusal record that does
-// not read is no word from root (nil), and so is a consumed file not
-// decided for rt's tenant (Consumed.DecidedFor); a copy that does not parse
-// is unread.
+// not read is no word from root (nil), and so is a consumed file tagged for
+// another tenant (Consumed.ForeignTo); a copy that does not parse is unread.
func (b *buildStates) heldWakeInput(rt registry.Route, state *release.BuildState) release.HeldWakeInput {
in := release.HeldWakeInput{Settings: rt.UpdateSettings}
- // Only a decision root made for this route's tenant counts (ruling R26):
- // another tenant's file, or an untagged one from before ruling R22, is
- // no word from root, so a wake that could be held holds and asks root.
- if c, _ := release.ReadConsumed(b.optDir, rt.Host); c.DecidedFor(rt) {
+ // Another tenant's file is no word from root for this route (ruling
+ // R26), so a wake that could be held holds and asks root. An untagged
+ // file, from before ruling R22, is read as before, as every other read
+ // of it is (ruling R28): ignoring its failed decision would let a wake
+ // whose target is already live run a build root reports failed, as
+ // awaitRootDecision does not wait when the live link names the target.
+ if c, _ := release.ReadConsumed(b.optDir, rt.Host); !c.ForeignTo(rt) {
in.Consumed = c
}
in.Refusal, _ = release.ReadRefusal(b.bakeDir, rt.Host)
diff --git a/internal/host/buildstate_astra_test.go b/internal/host/buildstate_astra_test.go
index 5b5416890..2750dca19 100644
--- a/internal/host/buildstate_astra_test.go
+++ b/internal/host/buildstate_astra_test.go
@@ -83,19 +83,34 @@ func TestAMajorHoldIsNotBypassedBeforeRootDecidesForTheSettings(t *testing.T) {
// build state read and absent, T installed, and root's decision that T
// failed. The installed-build fallback is failure-checked like
// last_executed: held on T and failed, never "not held" (which ran T).
+//
+// It runs twice: with root's file tagged for the route's tenant, as root
+// writes it now, and with the file left untagged, as a settings-4 build
+// wrote it (ruling R28: an untagged file is read as before task 8).
func TestAFailedInstalledBuildWithNoBuildStateIsHeldFailed(t *testing.T) {
- b, _, reg := testBuildStates(t)
- installLive(t, b, "T")
- setSettings(t, reg, release.UpdateSettings{Updates: release.UpdatesManual, Channel: "stable"})
- setCopy(t, reg, release.BuildStateCopy{Known: true, Replica: true})
- decided(t, b, release.Consumed{Reason: string(release.ReasonFailed), Detail: "T"})
- if h := b.heldTarget(astraHost); !h.Held || !h.Failed || h.Target != "T" || h.Staged != nil {
- t.Fatalf("heldTarget = %+v; want T held and failed", h)
- }
- // Not failed: held on the installed build, as root's Target holds it.
- decided(t, b, release.Consumed{Reason: string(release.ReasonManual), Desired: "T", Detail: "T"})
- if h := b.heldTarget(astraHost); !h.Held || h.Failed || h.Target != "T" {
- t.Fatalf("heldTarget = %+v; want held on the installed T", h)
+ for _, untagged := range []bool{false, true} {
+ b, _, reg := testBuildStates(t)
+ installLive(t, b, "T")
+ setSettings(t, reg, release.UpdateSettings{Updates: release.UpdatesManual, Channel: "stable"})
+ setCopy(t, reg, release.BuildStateCopy{Known: true, Replica: true})
+ write := func(c release.Consumed) {
+ if !untagged {
+ decided(t, b, c)
+ return
+ }
+ rt, _ := reg.Get(astraHost)
+ c.Settings = release.SettingsDigest(rt.UpdateSettings)
+ writeConsumedAsIs(t, b, c)
+ }
+ write(release.Consumed{Reason: string(release.ReasonFailed), Detail: "T"})
+ if h := b.heldTarget(astraHost); !h.Held || !h.Failed || h.Target != "T" || h.Staged != nil {
+ t.Fatalf("untagged %v: heldTarget = %+v; want T held and failed", untagged, h)
+ }
+ // Not failed: held on the installed build, as root's Target holds it.
+ write(release.Consumed{Reason: string(release.ReasonManual), Desired: "T", Detail: "T"})
+ if h := b.heldTarget(astraHost); !h.Held || h.Failed || h.Target != "T" {
+ t.Fatalf("untagged %v: heldTarget = %+v; want held on the installed T", untagged, h)
+ }
}
}
diff --git a/internal/host/buildstate_test.go b/internal/host/buildstate_test.go
index 5eaeab15f..d1fbbe225 100644
--- a/internal/host/buildstate_test.go
+++ b/internal/host/buildstate_test.go
@@ -663,8 +663,7 @@ func TestClaimsOnRegistersOnlyTheClaimBucket(t *testing.T) {
// writeConsumed writes root's consumed file for the test host. An untagged
// c is tagged with the route's tenant, as root writes every file since
-// ruling R22; the held wake check reads no other (ruling R26). Tests of an
-// untagged or foreign file use writeConsumedAsIs.
+// ruling R22. Tests of an untagged or foreign file use writeConsumedAsIs.
func writeConsumed(t *testing.T, b *buildStates, c release.Consumed) {
t.Helper()
c.Host = "a.example.com"
diff --git a/internal/host/tenantchange_test.go b/internal/host/tenantchange_test.go
index edc505e79..8d2047088 100644
--- a/internal/host/tenantchange_test.go
+++ b/internal/host/tenantchange_test.go
@@ -2,8 +2,13 @@ package host
import (
"context"
+ "net/http"
+ "sync/atomic"
"testing"
+ "time"
+ "github.com/carlosframework/platform/internal/activator"
+ "github.com/carlosframework/platform/internal/registry"
"github.com/carlosframework/platform/internal/release"
)
@@ -11,8 +16,8 @@ import (
// keyed by hostname, and a hostname can move to another tenant on one box.
// A file tagged for another tenant is absent to the pre-exec write and to a
// move's carry; an untagged file is read there as today. The held wake check
-// trusts only a file decided for the route (DecidedFor): with a foreign or an
-// untagged file it holds and asks root.
+// reads the file by the same rule (ruling R28): with a foreign file it holds
+// and asks root, and an untagged file is read as today.
// foreignRecord is tenant A's (cdf/blog) record for the test host: a
// resolved request, a failed completion watermark and a manual decision.
@@ -61,6 +66,33 @@ func TestBeforeExecIgnoresAnotherTenantsFile(t *testing.T) {
}
}
+// Astra r4 finding 3: the pre-exec write learns the route's tenant from the
+// registry. A read that fails is no answer: B owns the host, A's file is
+// still on disk, and an unread route cannot tell them apart. beforeExec
+// returns the error before any build-state write, so the durable object is
+// left exactly as it was. (A route that is absent, read without error, is
+// today's behaviour and is not this case.)
+func TestBeforeExecFailsClosedOnARegistryReadError(t *testing.T) {
+ b, s, reg := testBuildStates(t)
+ ctx := context.Background()
+ if err := put(t, b, ctx, astraHost, release.BuildState{LastExecuted: "B1", Completed: 1}); err != nil {
+ t.Fatal(err)
+ }
+ key := release.BuildStateKey("inst/" + astraHost)
+ etag := mustEtag(t, s, key)
+ writeConsumedAsIs(t, b, foreignRecord())
+ if err := reg.Close(); err != nil { // every Get now fails
+ t.Fatal(err)
+ }
+ if err := b.beforeExec(ctx, astraHost, "v1"); err == nil {
+ t.Fatal("beforeExec with an unreadable route returned nil")
+ }
+ if got := mustEtag(t, s, key); got != etag {
+ st, _, _ := getState(ctx, s, key, astraHost)
+ t.Fatalf("the durable build state was written with the route unread: %+v (resolved %+v)", st, st.Resolved)
+ }
+}
+
func TestACarryIgnoresAnotherTenantsFile(t *testing.T) {
b, s, reg := testBuildStates(t)
ctx := context.Background()
@@ -105,7 +137,6 @@ func TestTheHeldWakeTrustsOnlyAFileDecidedForTheRoute(t *testing.T) {
for _, tc := range []struct{ name, account, app string }{
{"another tenant's file", "cdf", "blog"},
{"another app's file on the same account", release.OpsAccount, "blog"},
- {"an untagged file", "", ""},
} {
c := record
c.Account, c.App = tc.account, tc.app
@@ -115,11 +146,71 @@ func TestTheHeldWakeTrustsOnlyAFileDecidedForTheRoute(t *testing.T) {
}
}
- // The route's own file is root's word: the request's T.
- c := record
- c.Account, c.App = release.OpsAccount, "km"
- writeConsumedAsIs(t, b, c)
- if h := b.heldTarget(astraHost); !h.Held || h.AskRoot || h.Target != "v4" {
- t.Fatalf("the route's own file: heldTarget = %+v; want the request's v4", h)
+ // The route's own file is root's word: the request's T. An untagged file
+ // is read as before task 8 (ruling R28), as the route's own.
+ for _, tc := range []struct{ name, account, app string }{
+ {"the route's own file", release.OpsAccount, "km"},
+ {"an untagged file", "", ""},
+ } {
+ c := record
+ c.Account, c.App = tc.account, tc.app
+ writeConsumedAsIs(t, b, c)
+ if h := b.heldTarget(astraHost); !h.Held || h.AskRoot || h.Target != "v4" {
+ t.Fatalf("%s: heldTarget = %+v; want the request's v4", tc.name, h)
+ }
+ }
+}
+
+// identityRecorder is plannedBackend with a live link it can capture: it
+// counts the captures and answers with T's identity, so a wake that gets as
+// far as running the live link records it.
+type identityRecorder struct {
+ *plannedBackend
+ captured atomic.Int32
+}
+
+func (r *identityRecorder) LiveIdentity(context.Context, registry.Route) (release.StagedIdentity, error) {
+ r.captured.Add(1)
+ return release.StagedIdentity{Version: r.live}, nil
+}
+
+// Astra r4 finding 1 (ruling R28), through the real wake: a same-tenant
+// manual instance with T installed, its build state read and absent, and an
+// UNTAGGED consumed file (a settings-4 build's) recording that T failed.
+// There is no refusal record and no planner. The activator's held wake check
+// must refuse the wake as failed: it never captures the live link, never
+// records T, never runs anything.
+func TestAnUntaggedFailedDecisionRefusesTheWake(t *testing.T) {
+ b, s, reg := testBuildStates(t)
+ b.prefix = func(h string) string { return activator.HibernatePrefix("", h) }
+ installLive(t, b, "T")
+ setSettings(t, reg, release.UpdateSettings{Updates: release.UpdatesManual, Channel: "stable"})
+ setCopy(t, reg, release.BuildStateCopy{Known: true, Replica: true})
+ rt, _ := reg.Get(astraHost)
+ writeConsumedAsIs(t, b, release.Consumed{Reason: string(release.ReasonFailed), Detail: "T",
+ Settings: release.SettingsDigest(rt.UpdateSettings)})
+ if r, _ := release.ReadRefusal(b.bakeDir, astraHost); r != nil {
+ t.Fatalf("a refusal record exists: %+v", r)
+ }
+
+ be := &identityRecorder{plannedBackend: &plannedBackend{live: "T"}}
+ svc := activator.New(activator.Config{Bucket: "hib", Idle: time.Hour}, be, reg)
+ svc.OnGen = func(activator.GenEvent) {}
+ svc.BeforeExec = b.beforeExec
+ svc.HeldTarget = b.heldTarget
+ t.Cleanup(func() { svc.Shutdown(context.Background()) })
+
+ rec := serve(t, svc, reg)
+ if rec.Code == http.StatusOK {
+ t.Fatalf("the wake ran T, which root reports failed: %q", rec.Body.String())
+ }
+ if n := be.captured.Load(); n != 0 {
+ t.Fatalf("the wake captured the live link %d times; want it refused before", n)
+ }
+ if got := be.builds(); len(got) != 0 {
+ t.Fatalf("generations %v, want none", got)
+ }
+ if st := durable(t, b, s); st != nil {
+ t.Fatalf("durable state %+v; want none (T never recorded)", st)
}
}
diff --git a/internal/registry/registry.go b/internal/registry/registry.go
index 3867c14ab..3dafe927e 100644
--- a/internal/registry/registry.go
+++ b/internal/registry/registry.go
@@ -22,6 +22,8 @@ import (
"time"
"modernc.org/sqlite"
+
+ "github.com/carlosframework/platform/internal/controlname"
)
type Registry struct {
@@ -1085,17 +1087,51 @@ func (r *Registry) Delete(host string) error {
// Route53 latency records — a real incident this repo caught in review before
// it shipped, not a hypothetical.
// `update_settings` and `build_state` keep an existing value only while the
-// row's tenant (account, app) stays the same. An Upsert that moves the
-// hostname to another tenant takes the incoming values, empty included:
+// row's tenant (effective account, app) stays the same. An Upsert that moves
+// the hostname to another tenant takes the incoming values, empty included:
// one tenant's manual or major hold, or its build state, is never another
-// tenant's (settings-5 task 8). SQLite evaluates every SET expression
-// against the row as it was, so `routes.account` and `routes.app` there are
-// still the old tenant's.
+// tenant's (settings-5 task 8). The account compared is the effective one
+// (EffectiveAccount, as release.RouteAccount reads it), so a legacy row's
+// empty account re-added as the ops account is the same tenant and keeps
+// both (Astra r4 finding 2). SQLite evaluates every SET expression against
+// the row as it was, so `routes.account` and `routes.app` there are still
+// the old tenant's.
// `good_version`, `good_digest`, `good_demand` and `serving_version`
// (zero-downtime spec §8.5) are absent from the ON CONFLICT clause on
// purpose: an existing row keeps them whatever the incoming Route carries.
// They have one writer each (SetGoodBuild, SetServingVersion), and no
// full-row caller knows what an instance last served.
+// opsAccount is the ops account's sqid: account #1, pinned by migrate. It is
+// release.OpsAccount (release cannot be imported here; a test in release
+// pins the two together through EffectiveAccount).
+const opsAccount = "bab"
+
+// EffectiveAccount is the account a route's stored account names: itself
+// when it is a valid account sqid (controlname.ValidAccount), else the ops
+// account. It is release.RouteAccount on the account alone, which root and
+// the agent use for a route's tenant; Upsert compares tenants by it.
+func EffectiveAccount(account string) string {
+ if controlname.ValidAccount(account) {
+ return account
+ }
+ return opsAccount
+}
+
+// effectiveAccountSQL is EffectiveAccount as a SQLite expression over col.
+// controlname.ValidAccount is ^[a-z]{3,16}$: three to sixteen characters,
+// none outside a-z. TestEffectiveAccountSQLIsEffectiveAccount pins the two
+// together.
+func effectiveAccountSQL(col string) string {
+ return `(CASE WHEN length(` + col + `) BETWEEN 3 AND 16 AND ` + col + ` NOT GLOB '*[^a-z]*' THEN ` + col +
+ ` ELSE '` + opsAccount + `' END)`
+}
+
+// tenantChangedSQL is true in Upsert's ON CONFLICT clause when the incoming
+// row names another tenant than the stored one: another effective account
+// or another app.
+var tenantChangedSQL = effectiveAccountSQL("routes.account") + ` != ` + effectiveAccountSQL("excluded.account") +
+ ` OR routes.app != excluded.app`
+
func (r *Registry) Upsert(t Route) error {
if r.remote != nil {
_, err := r.remote.call(nil, remoteRequest{Method: "Upsert", TValue: t}, nil)
@@ -1166,9 +1202,9 @@ ON CONFLICT(host) DO UPDATE SET
args=CASE WHEN excluded.args = '' THEN routes.args ELSE excluded.args END,
idle_seconds=CASE WHEN excluded.idle_seconds = 0 THEN routes.idle_seconds ELSE excluded.idle_seconds END,
max_run_seconds=CASE WHEN excluded.max_run_seconds = 0 THEN routes.max_run_seconds ELSE excluded.max_run_seconds END,
- update_settings=CASE WHEN routes.account != excluded.account OR routes.app != excluded.app THEN excluded.update_settings
+ update_settings=CASE WHEN `+tenantChangedSQL+` THEN excluded.update_settings
WHEN excluded.update_settings = '' THEN routes.update_settings ELSE excluded.update_settings END,
- build_state=CASE WHEN routes.account != excluded.account OR routes.app != excluded.app THEN excluded.build_state
+ build_state=CASE WHEN `+tenantChangedSQL+` THEN excluded.build_state
WHEN excluded.build_state = '' THEN routes.build_state ELSE excluded.build_state END
WHERE routes.claim_incarnation=excluded.claim_incarnation AND routes.claim_stamp=excluded.claim_stamp
AND routes.claim_owner=excluded.claim_owner AND routes.claim_row_id=excluded.claim_row_id`,
diff --git a/internal/registry/updatesettings_test.go b/internal/registry/updatesettings_test.go
index c164f368d..17fd5ad82 100644
--- a/internal/registry/updatesettings_test.go
+++ b/internal/registry/updatesettings_test.go
@@ -103,6 +103,67 @@ func TestATenantChangeUpsertTakesTheNewTenantsSettings(t *testing.T) {
}
}
+// Astra r4 finding 2: the tenant is the EFFECTIVE account, as
+// release.RouteAccount reads it (an empty or invalid account is the ops
+// account, "bab"). A legacy row with account "" re-added as "bab" (or the
+// reverse, or an invalid spelling) is the same tenant, so its columns are
+// kept as today. A genuine account change with empty incoming columns
+// clears both.
+func TestAnEquivalentAccountSpellingIsTheSameTenant(t *testing.T) {
+ reg, err := Open(filepath.Join(t.TempDir(), "r.db"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ const host = "a.example.com"
+ settings := `{"updates":"manual","channel":"stable"}`
+ state := `{"known":true,"replica":true}`
+ for _, tc := range []struct{ from, to string }{
+ {"", "bab"}, {"bab", ""}, {"", ""}, {"NOT-VALID", "bab"}, {"bab", "x"},
+ } {
+ if err := reg.Upsert(Route{Host: host, Kind: "instance", Account: tc.from, App: "km", UpdateSettings: settings, BuildState: state}); err != nil {
+ t.Fatal(err)
+ }
+ if err := reg.Upsert(Route{Host: host, Kind: "instance", Account: tc.to, App: "km"}); err != nil {
+ t.Fatal(err)
+ }
+ if rt, _ := reg.Get(host); rt.Account != tc.to || rt.UpdateSettings != settings || rt.BuildState != state {
+ t.Fatalf("%q -> %q (one effective account): account %q, settings %q, build state %q; want both columns kept",
+ tc.from, tc.to, rt.Account, rt.UpdateSettings, rt.BuildState)
+ }
+ }
+ for _, tc := range []struct{ from, to string }{{"", "bes"}, {"bes", ""}, {"bes", "cdf"}} {
+ if err := reg.Upsert(Route{Host: host, Kind: "instance", Account: tc.from, App: "km", UpdateSettings: settings, BuildState: state}); err != nil {
+ t.Fatal(err)
+ }
+ if err := reg.Upsert(Route{Host: host, Kind: "instance", Account: tc.to, App: "km"}); err != nil {
+ t.Fatal(err)
+ }
+ if rt, _ := reg.Get(host); rt.UpdateSettings != "" || rt.BuildState != "" {
+ t.Fatalf("%q -> %q (another account): settings %q, build state %q; want both cleared",
+ tc.from, tc.to, rt.UpdateSettings, rt.BuildState)
+ }
+ }
+}
+
+// Upsert's tenant compare runs in SQLite (effectiveAccountSQL); it must
+// say exactly what EffectiveAccount (controlname.ValidAccount) says.
+func TestEffectiveAccountSQLIsEffectiveAccount(t *testing.T) {
+ reg, err := Open(filepath.Join(t.TempDir(), "r.db"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ for _, a := range []string{"", "bab", "bes", "abc", "ab", "a", "abcdefghijklmnop", "abcdefghijklmnopq",
+ "Abc", "ABC", "ab1", "a-b", "a_b", "a b", " abc", "abc ", "abc\n", "\u00e1bc", "ab\u00e1", "ab]", "ab^", "ab*", "ab?", "ab["} {
+ var got string
+ if err := reg.sql.QueryRow(`SELECT `+effectiveAccountSQL("?1"), a).Scan(&got); err != nil {
+ t.Fatal(err)
+ }
+ if want := EffectiveAccount(a); got != want {
+ t.Fatalf("account %q: SQL says %q, EffectiveAccount says %q", a, got, want)
+ }
+ }
+}
+
func TestUpdateSettingsMovesSnapshotFingerprint(t *testing.T) {
reg, err := Open(filepath.Join(t.TempDir(), "r.db"))
if err != nil {
diff --git a/internal/release/buildstate.go b/internal/release/buildstate.go
index c280b9a56..b729a594d 100644
--- a/internal/release/buildstate.go
+++ b/internal/release/buildstate.go
@@ -128,10 +128,11 @@ func (c *Consumed) DecidedFor(rt registry.Route) bool {
// rt.App). A hostname can move between tenants on one box, and the file is
// keyed by hostname, so root and the agent treat a foreign file as absent
// wherever they decide from it (ruling R26): the adoption read, NextConsumed,
-// the pre-exec write and a move's carry. An untagged file (from before
-// ruling R22) is not foreign: those readers take it as the route's own, so a
-// completion or resolution only it holds still counts. The held wake check
-// and the update report are stricter and trust only DecidedFor.
+// the pre-exec write, a move's carry and the held wake check. An untagged
+// file (from before ruling R22) is not foreign: those readers take it as the
+// route's own, so a completion, resolution or failure only it holds still
+// counts (ruling R28). The update report is stricter and shows only a file
+// DecidedFor the route.
func (c *Consumed) ForeignTo(rt registry.Route) bool {
return c != nil && (c.Account != "" || c.App != "") && (c.Account != RouteAccount(rt) || c.App != rt.App)
}
diff --git a/internal/release/tenantchange_test.go b/internal/release/tenantchange_test.go
index f1bcdcf62..fc7446160 100644
--- a/internal/release/tenantchange_test.go
+++ b/internal/release/tenantchange_test.go
@@ -207,3 +207,15 @@ func TestConsumedForeignTo(t *testing.T) {
t.Error("the ops account's own file is foreign to its route")
}
}
+
+// The registry's Upsert compares tenants by registry.EffectiveAccount (Astra
+// r4 finding 2). It must be RouteAccount on the account, or the registry
+// keeps one tenant's settings columns for a route root reads as another's,
+// or drops them for one root reads as the same.
+func TestRegistryEffectiveAccountIsRouteAccount(t *testing.T) {
+ for _, a := range []string{"", OpsAccount, "bes", "cdf", "ab", "ABC", "ab1", "abcdefghijklmnopq", "abcdefghijklmnop"} {
+ if got, want := registry.EffectiveAccount(a), RouteAccount(registry.Route{Account: a}); got != want {
+ t.Fatalf("account %q: registry.EffectiveAccount %q, RouteAccount %q", a, got, want)
+ }
+ }
+}