carlos / platform Public

console: economics pass — refuse a missing/unconfigured registry, pin census golden values

Fixes from Task 8 review:
- censusPass now refuses (rather than silently zero-censusing) when
  cfg.Registry is unset or the path does not exist, mirroring
  instanceHealth's guard for the same registry.Open hazard
  (apiinstances.go:207): sql.Open creates+migrates an empty sqlite DB at
  any path handed to it, so an unmounted registry volume would otherwise
  write a permanent, uncorrectable all-zero census day.
- TestStoreAccountsListsAll now asserts non-emptiness, pins the fixture's
  ops account (sqid == release.OpsAccount, name == "ops"), and covers a
  soft-deleted account (tombstoned via Store.DeleteAccount) disappearing
  from Accounts() — pinning the deleted_at=0 deviation's semantics.
- TestEconomicsPassWritesCensusOnce pins the golden census value the
  fixture actually produces (day.Accounts[release.OpsAccount] ==
  {Apps: 2, Routes: 3}) instead of only checking non-emptiness.
- New TestCensusPassRefusesMissingRegistry covers finding 1 directly:
  a missing registry path errors, writes no census object, and leaves no
  stray file at that path.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Paul Campbell 95e0f7df33dda66a017ae53ab32b5fcc7b19683b parent 2486cb3
2 files changed, +86 −4
  • internal/console/economicspass.go +17 −0
  • internal/console/economicspass_test.go +69 −4
diff --git a/internal/console/economicspass.go b/internal/console/economicspass.go
index b99896192..edb34c080 100644
--- a/internal/console/economicspass.go
+++ b/internal/console/economicspass.go
@@ -2,7 +2,9 @@ package console
import (
"context"
+ "fmt"
"log"
+ "os"
"time"
"github.com/carlosframework/platform/internal/release"
@@ -58,6 +60,21 @@ func (s *Server) censusPass(ctx context.Context, date string) error {
if _, _, err := s.cfg.Releases.Get(ctx, key); err == nil {
return nil // today already written (by us or a sibling console)
}
+ // Guard both the unconfigured and the unmounted-volume case before
+ // opening: registry.Open goes through sql.Open, which creates and
+ // migrates an EMPTY sqlite file at whatever path it's given, missing
+ // file or not. Left unguarded, a console booted with no registry path
+ // or an unmounted registry volume would read that empty DB as zero
+ // routes and CreateDayObject an all-zero census day — create-only, so
+ // that day could never be corrected, plus a stray sqlite file left at
+ // the registry path. Mirrors instanceHealth's same-shaped guard for the
+ // empty-string case (apiinstances.go:207).
+ if s.cfg.Registry == "" {
+ return fmt.Errorf("console: census pass: no registry configured")
+ }
+ if _, err := os.Stat(s.cfg.Registry); err != nil {
+ return fmt.Errorf("console: census pass: registry %s: %w", s.cfg.Registry, err)
+ }
reg, err := openRegistry(s.cfg.Registry)
if err != nil {
return err
diff --git a/internal/console/economicspass_test.go b/internal/console/economicspass_test.go
index 94732bb8d..8b60110f5 100644
--- a/internal/console/economicspass_test.go
+++ b/internal/console/economicspass_test.go
@@ -3,9 +3,12 @@ package console
import (
"context"
"encoding/json"
+ "os"
+ "path/filepath"
"testing"
"time"
+ "github.com/carlosframework/platform/internal/release"
"github.com/carlosframework/platform/internal/usage"
)
@@ -25,8 +28,13 @@ func TestEconomicsPassWritesCensusOnce(t *testing.T) {
if err := json.Unmarshal(raw, &day); err != nil {
t.Fatal(err)
}
- if len(day.Accounts) == 0 {
- t.Fatal("census counted nothing — fixture routes should appear")
+ // buildFixtures (view_test.go) seeds three routes with no Account set —
+ // release.RouteAccount normalizes all of them to release.OpsAccount — two
+ // distinct apps (kass, otherapp) and three routes total (kass instance,
+ // kass static docs, otherapp instance).
+ want := usage.CensusAccount{Apps: 2, Routes: 3}
+ if got := day.Accounts[release.OpsAccount]; got != want {
+ t.Fatalf("day.Accounts[%q] = %+v, want %+v", release.OpsAccount, got, want)
}
// Idempotence: a second run the same day must not error or rewrite.
before := string(raw)
@@ -39,16 +47,73 @@ func TestEconomicsPassWritesCensusOnce(t *testing.T) {
}
}
+// TestCensusPassRefusesMissingRegistry: a console booted with an unmounted
+// or misconfigured registry volume must refuse the census rather than read
+// registry.Open's freshly-created, empty sqlite DB as "zero routes" and
+// write a permanent, uncorrectable all-zero day (create-only). It must also
+// leave no stray file behind at the path it didn't open.
+func TestCensusPassRefusesMissingRegistry(t *testing.T) {
+ fx := newConsoleFixture(t)
+ ctx := context.Background()
+ missing := filepath.Join(t.TempDir(), "nonexistent-registry.db")
+ fx.srv.cfg.Registry = missing
+
+ if err := fx.srv.censusPass(ctx, "2026-08-11"); err == nil {
+ t.Fatal("censusPass with a missing registry succeeded, want an error")
+ }
+ if _, err := os.Stat(missing); err == nil {
+ t.Fatal("censusPass created a file at the missing registry path")
+ }
+ if _, _, err := fx.rel.Get(ctx, usage.CensusDayKey("2026-08-11")); err == nil {
+ t.Fatal("censusPass with a missing registry wrote a census object")
+ }
+}
+
func TestStoreAccountsListsAll(t *testing.T) {
fx := newConsoleFixture(t)
accts, err := fx.st.Accounts()
if err != nil {
t.Fatal(err)
}
- // The fixture mints at least one account; every row must carry id+sqid+name.
- for _, a := range accts {
+ if len(accts) == 0 {
+ t.Fatal("Accounts() returned nothing — the fixture's ops account should appear")
+ }
+ var ops *Account
+ for i := range accts {
+ a := accts[i]
if a.ID == 0 || a.Sqid == "" || a.Name == "" {
t.Fatalf("incomplete account row: %+v", a)
}
+ if a.Sqid == release.OpsAccount {
+ ops = &accts[i]
+ }
+ }
+ if ops == nil {
+ t.Fatal("Accounts() is missing the fixture's built-in ops account")
+ }
+ if ops.Name != "ops" {
+ t.Fatalf("ops account name = %q, want %q", ops.Name, "ops")
+ }
+
+ // A soft-deleted account, tombstoned the way the store's own member-facing
+ // delete path does it (DeleteAccount sets deleted_at), must disappear
+ // from Accounts() — pinning the deviation from the brief's literal query.
+ id := testAccountID()
+ sqid := "test-deleted-1"
+ now := time.Now()
+ if err := fx.st.CreateAccount(id, sqid, "Deadco", "paul@keymail.dev", now); err != nil {
+ t.Fatal(err)
+ }
+ if err := fx.st.DeleteAccount(id, now); err != nil {
+ t.Fatal(err)
+ }
+ accts, err = fx.st.Accounts()
+ if err != nil {
+ t.Fatal(err)
+ }
+ for _, a := range accts {
+ if a.Sqid == sqid {
+ t.Fatalf("Accounts() returned a soft-deleted account: %+v", a)
+ }
}
}