carlos / platform Public

console: the seeds operator doors answer without a deployment bucket (final review carry item)

handleAPISystemSeedsFloor and handleAPISystemLegacyRoutes read and wrote
s.cfg.Releases without a nil check, so a console with no deployment
bucket panicked in the store call (net/http recovered it per request).
Each now answers its own failure sentence with a 503 when Releases is
nil.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 405b85ffb9b4099f1edae0778f27d1dfde5ec758 parent d567cac
3 files changed, +39 −0
  • internal/console/apilegacyroutes.go +6 −0
  • internal/console/operatornobucket_test.go +27 −0
  • internal/console/seedfloor.go +6 −0
diff --git a/internal/console/apilegacyroutes.go b/internal/console/apilegacyroutes.go
index ae6cf8175..68c8d6452 100644
--- a/internal/console/apilegacyroutes.go
+++ b/internal/console/apilegacyroutes.go
@@ -43,6 +43,12 @@ func (s *Server) handleAPISystemLegacyRoutes(w http.ResponseWriter, r *http.Requ
jsonError(w, ref.Status, ref.Msg)
return
}
+ // A console with no deployment bucket has no assignments to read: the
+ // handler's own sentence, not a panic (final review carry item).
+ if s.cfg.Releases == nil {
+ jsonError(w, http.StatusServiceUnavailable, "could not read the legacy route assignments. Try again.")
+ return
+ }
var req struct {
Name string `json:"name"`
To string `json:"to"`
diff --git a/internal/console/operatornobucket_test.go b/internal/console/operatornobucket_test.go
new file mode 100644
index 000000000..08049c540
--- /dev/null
+++ b/internal/console/operatornobucket_test.go
@@ -0,0 +1,27 @@
+package console
+
+import (
+ "net/http"
+ "testing"
+)
+
+// Final review carry item: a console with no deployment bucket answers its
+// operator doors with the door's own failure sentence and a 503, instead of
+// panicking in the store read.
+func TestOperatorDoorsWithNoDeploymentBucket(t *testing.T) {
+ fx := legacyFixture(t, "code-no-bucket")
+ fx.srv.cfg.Releases = nil
+ for _, c := range []struct {
+ path string
+ body map[string]any
+ want string
+ }{
+ {"/api/cli/system/seeds-floor", map[string]any{"min_release": "v0.27.0"}, "could not publish the seeds floor. Try again."},
+ {"/api/cli/system/legacy-routes", map[string]any{"name": "shop", "to": "cdef/shop"}, "could not read the legacy route assignments. Try again."},
+ } {
+ st, out := postJSON(t, fx.ts.URL+c.path, c.body, fx.owner)
+ if st != http.StatusServiceUnavailable || str(t, out, "error") != c.want {
+ t.Errorf("%s = %d %v, want 503 %q", c.path, st, out, c.want)
+ }
+ }
+}
diff --git a/internal/console/seedfloor.go b/internal/console/seedfloor.go
index a04e78a23..e41a694b3 100644
--- a/internal/console/seedfloor.go
+++ b/internal/console/seedfloor.go
@@ -169,6 +169,12 @@ func (s *Server) handleAPISystemSeedsFloor(w http.ResponseWriter, r *http.Reques
if !ok {
return
}
+ // A console with no deployment bucket has nowhere to publish: the
+ // handler's own failure sentence, not a panic (final review carry item).
+ if s.cfg.Releases == nil {
+ jsonError(w, http.StatusServiceUnavailable, "could not publish the seeds floor. Try again.")
+ return
+ }
var req struct {
MinRelease string `json:"min_release"`
}