carlos / platform Public

host: retention pass resolves route accounts via RouteAccount

The raw Account column is empty or a legacy name until the domains pass
stamps it; RouteAccount is how every other box-side release path resolves
it (review finding, round 1).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Paul Campbell 3f1293f171caca45369433ce7a6180509e4a6bf0 parent 4e7233a
2 files changed, +59 −2
  • internal/host/retention.go +2 −2
  • internal/host/retention_test.go +57 −0
diff --git a/internal/host/retention.go b/internal/host/retention.go
index 85e669fe2..6b31240ba 100644
--- a/internal/host/retention.go
+++ b/internal/host/retention.go
@@ -31,10 +31,10 @@ func retentionPass(ctx context.Context, store release.Store, pub ed25519.PublicK
type appKey struct{ acct, app string }
pins := map[appKey]map[string]bool{}
for _, rt := range routes {
- if rt.Account == "" || rt.App == "" {
+ if rt.App == "" {
continue
}
- k := appKey{rt.Account, rt.App}
+ k := appKey{release.RouteAccount(rt), rt.App}
if pins[k] == nil {
pins[k] = map[string]bool{}
}
diff --git a/internal/host/retention_test.go b/internal/host/retention_test.go
index 4cbb96f35..174049ca8 100644
--- a/internal/host/retention_test.go
+++ b/internal/host/retention_test.go
@@ -87,6 +87,63 @@ func TestRetentionPassPrunesEligibleAndProtectsBoxPins(t *testing.T) {
}
}
+// TestRetentionPassResolvesEmptyAccountRoutesViaRouteAccount covers the
+// review finding: a route row with an EMPTY Account column (unstamped by
+// the domains pass) must still resolve to release.OpsAccount ("bab"), the
+// same resolution every other box-side release path uses
+// (release.RouteAccount, domains/reconcile.go's resolveRouteAccount) —
+// never be silently skipped, and never key its pins under "".
+func TestRetentionPassResolvesEmptyAccountRoutesViaRouteAccount(t *testing.T) {
+ ctx := context.Background()
+ dir := t.TempDir()
+ s := release.NewDirStore(dir)
+ now := time.Now()
+ base := now.Add(-30 * 24 * time.Hour)
+ vname := func(i int) string { return "v" + string(rune('0'+i/10)) + string(rune('0'+i%10)) }
+ for i := 1; i <= 25; i++ {
+ stampKey(t, s, dir, release.ManifestKey(release.OpsAccount, "kass", vname(i)), base.Add(time.Duration(i)*time.Hour))
+ }
+ _, pub := retentionKeys(t)
+ if err := release.SetRetention(ctx, s, release.OpsAccount, "kass", 20, "test", now); err != nil {
+ t.Fatal(err)
+ }
+ reg, err := registry.Open(filepath.Join(t.TempDir(), "registry.db"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer reg.Close()
+ // Account column left empty — the state every route is in until the
+ // domains pass stamps it (reconcile.go: "A route with an empty account
+ // column resolves to ops until 1b writes the account the BUCKET
+ // asserts"). v03/v02 are this box's pins, both outside keep-20.
+ if err := reg.Upsert(registry.Route{
+ Host: "kass.example.com", Kind: "instance", App: "kass",
+ Version: "v03", PrevVersion: "v02", Status: "ready",
+ }); err != nil {
+ t.Fatal(err)
+ }
+
+ if err := retentionPass(ctx, s, pub, reg); err != nil {
+ t.Fatal(err)
+ }
+
+ // The bab/kass policy must have been enforced — an empty Account
+ // column must not cause the app to be silently skipped.
+ if _, _, err := s.Get(ctx, release.ManifestKey(release.OpsAccount, "kass", "v01")); err == nil {
+ t.Fatalf("v01 still present, want pruned (empty-account route was not resolved to %s)", release.OpsAccount)
+ }
+ if _, _, err := s.Get(ctx, release.ManifestKey(release.OpsAccount, "kass", "v04")); err == nil {
+ t.Fatalf("v04 still present, want pruned")
+ }
+ // v02/v03 are the empty-account route's Version/PrevVersion pins —
+ // must survive despite ranking outside keep-20.
+ for _, kept := range []string{"v02", "v03"} {
+ if _, _, err := s.Get(ctx, release.ManifestKey(release.OpsAccount, "kass", kept)); err != nil {
+ t.Fatalf("%s pruned, want kept as an empty-account route's pin: %v", kept, err)
+ }
+ }
+}
+
func TestRetentionPassSkipsAppsWithoutAPolicy(t *testing.T) {
ctx := context.Background()
dir := t.TempDir()