package console import ( "context" "math" "path/filepath" "strings" "testing" "time" "github.com/carlosframework/platform/internal/economics" "github.com/carlosframework/platform/internal/metrics" "github.com/carlosframework/platform/internal/registry" "github.com/carlosframework/platform/internal/usage" ) // TestLoadEconomicsGolden reuses TestStatementsPassGolden's golden objects // (economicspass_test.go) — one box, one route, cpu 0.5/mem 1e9 avg over 60 // samples in one hour, 2GB/4000req edge traffic for the same route's // account — plus a named scenario ("test") set active, and asserts // LoadEconomics prices it: the active scenario loads by name, the account // shows up with real cost, box1's allocation reconstructs its own cost via // the very same economics.AllocateBox call used for the golden fixture, and // the embedded payload names its "meters" field for Task 13's inline JS. // // Dates are relative to the real wall clock (today), not a fixed 2026-08 // date, because LoadEconomics has no `now` parameter — it reads real time — // mirroring how metricsview_test.go seeds LoadMetrics fixtures. func TestLoadEconomicsGolden(t *testing.T) { fx := newConsoleFixture(t) ctx := context.Background() reg, err := registry.Open(fx.regPath) if err != nil { t.Fatal(err) } if err := reg.Upsert(registry.Route{Host: "a.example.com", Kind: "instance", Status: "ready", App: fixtureApp, Version: "v1"}); err != nil { t.Fatal(err) } date := usage.DayOf(time.Now()) cpu, mem := 0.5, 1e9 roll := metrics.DayRollup{V: 1, Box: "box1", Date: date, Hours: map[string]metrics.HourAgg{ "09": {Units: map[string]map[string]metrics.Agg{ "carlos-app@a.example.com.service": { "cpu": {Avg: cpu, N: 60}, "mem_bytes": {Avg: mem, N: 60}, }, }}, }} putStoreJSON(t, fx.rel, metrics.DayKey("box1", date), roll) putStoreJSON(t, fx.rel, usage.EdgeDayKey("box1", date), usage.EdgeDay{ V: 1, Box: "box1", Date: date, Hours: map[string]map[string]usage.EdgeRow{"09": { "a.example.com": {Account: fixtureAcct, App: fixtureApp, Requests: 4000, BytesOut: 2e9}, }}, }) // One day of storage (31 GB) and census (2 apps) for today, to prove // buildEconomicsView folds MeterStorageGBMonth/MeterAppMonths in — the // MAJOR fix-round finding: the view path used to call sweepUsage alone // and never see these meters at all. Expected contribution uses the // controller's denominator ruling: divide by days in THIS day's own // calendar month, computed the same way production code does, so the // assertion holds regardless of which real month the test runs in. now := time.Now().UTC() daysInMonth := time.Date(now.Year(), now.Month()+1, 0, 0, 0, 0, 0, time.UTC).Day() putStoreJSON(t, fx.rel, usage.StorageDayKey(date), usage.StorageDay{ V: 1, Date: date, Apps: map[string]usage.AppStorage{ fixtureAcct + "/" + fixtureApp: {BucketBytes: 31 * 1e9}, }, }) putStoreJSON(t, fx.rel, usage.CensusDayKey(date), usage.CensusDay{ V: 1, Date: date, Accounts: map[string]usage.CensusAccount{ fixtureAcct: {Apps: 2, Routes: 3}, }, }) sc := economics.DefaultScenario() sc.Name = "test" putStoreJSON(t, fx.rel, economics.ScenarioKey("test"), sc) putStoreJSON(t, fx.rel, economics.ActiveKey, economics.Active{Schema: 1, Name: "test"}) // Task 5 (tier-aware revenue): an untiered account is pool-tier by // default and prices at RevenueUSD=0 — this test's revenue assertions // need the fixture account tiered "hosted" to keep meaning under that // rule. Sanctioned test-only change (economics-v2 plan, Task 5); no // production behavior here. if err := fx.st.SetAccountTier(OpsAccountID, "hosted"); err != nil { t.Fatal(err) } view, err := fx.srv.LoadEconomics(ctx, "mtd") if err != nil { t.Fatal(err) } if view.Scenario.Name != "test" { t.Fatalf("Scenario.Name = %q, want %q", view.Scenario.Name, "test") } if view.ScenarioWarning != "" { t.Fatalf("ScenarioWarning = %q, want empty (a named, valid active scenario)", view.ScenarioWarning) } var acctRow *economics.AccountEconomics for i := range view.Accounts { if view.Accounts[i].Account == fixtureAcct { acctRow = &view.Accounts[i] } } if acctRow == nil { t.Fatalf("no AccountEconomics row for %q in %+v", fixtureAcct, view.Accounts) } if acctRow.CostUSD <= 0 { t.Fatalf("acctRow.CostUSD = %v, want > 0", acctRow.CostUSD) } var boxRow *BoxEconomics for i := range view.Boxes { if view.Boxes[i].Box == "box1" { boxRow = &view.Boxes[i] } } if boxRow == nil { t.Fatalf("no BoxEconomics row for box1 in %+v", view.Boxes) } if boxRow.Hours != 1 { t.Fatalf("boxRow.Hours = %v, want 1 (one hour bucket, \"09\", in the golden day)", boxRow.Hours) } // Independently recompute the same box's allocation via // economics.AllocateBox with hand-built load matching the golden // fixture's cpu/mem averages, and confirm LoadEconomics's box row agrees // — idle + allocated reconstructs the box's own total cost. load := economics.BoxLoad{ Box: "box1", Hours: 1, ByAccount: map[string]economics.BoxAccountLoad{ fixtureAcct: {CPUCoreHours: 0.5, MemGBHours: 1.0}, }, } perAcct, idleUSD, totalUSD := economics.AllocateBox(sc.RateCard, sc.Allocation, load) allocated := perAcct[fixtureAcct] if math.Abs((idleUSD+allocated)-totalUSD) > 1e-9 { t.Fatalf("idle+allocated = %v, want totalUSD %v", idleUSD+allocated, totalUSD) } if math.Abs(boxRow.CostUSD-totalUSD) > 1e-9 { t.Fatalf("boxRow.CostUSD = %v, want %v", boxRow.CostUSD, totalUSD) } if math.Abs(boxRow.IdleUSD-idleUSD) > 1e-9 { t.Fatalf("boxRow.IdleUSD = %v, want %v", boxRow.IdleUSD, idleUSD) } if !strings.Contains(string(view.DataJSON), "meters") { t.Fatalf("DataJSON missing %q: %s", "meters", view.DataJSON) } wantStorage := 31.0 / float64(daysInMonth) if got := view.Meters[fixtureAcct][usage.MeterStorageGBMonth]; math.Abs(got-wantStorage) > 1e-9 { t.Fatalf("Meters[%q][storage_gb_month] = %v, want %v (31 GB / %d days in month)", fixtureAcct, got, wantStorage, daysInMonth) } wantAppMonths := 2.0 / float64(daysInMonth) if got := view.Meters[fixtureAcct][usage.MeterAppMonths]; math.Abs(got-wantAppMonths) > 1e-9 { t.Fatalf("Meters[%q][app_months] = %v, want %v (2 apps / %d days in month)", fixtureAcct, got, wantAppMonths, daysInMonth) } // AccountEconomics.CostUSD is derived from Meters, so the storage line // must be priced in too — the MAJOR finding's other symptom (cost // dropping the S3-storage line entirely). if got, want := acctRow.CostUSD, wantStorage*sc.RateCard.StorageGBMonthUSD; got < want { t.Fatalf("acctRow.CostUSD = %v, want >= %v (must include the storage line)", got, want) } } // TestLoadEconomicsNamesFromAccounts (account-names batch): EconomicsView.Names // is populated sqid->display-name from the SAME s.store.Accounts() read // buildEconomicsView already performs for the universe/Tiers — the fixture's // built-in account is named "ops" (newConsoleFixture / store.go's backfill), // keyed by fixtureAcct (its sqid, "bab"), exactly like Tiers is keyed. The // embedded payload carries the same map under "names" (economicsPayload), // the JS twin of view.Names, for parity with how "tiers" already travels. func TestLoadEconomicsNamesFromAccounts(t *testing.T) { fx := newConsoleFixture(t) ctx := context.Background() view, err := fx.srv.LoadEconomics(ctx, "mtd") if err != nil { t.Fatal(err) } if got, want := view.Names[fixtureAcct], "ops"; got != want { t.Fatalf("Names[%q] = %q, want %q (the fixture account's own name)", fixtureAcct, got, want) } if !strings.Contains(string(view.DataJSON), `"names"`) { t.Fatalf("DataJSON missing %q: %s", `"names"`, view.DataJSON) } } // TestLoadEconomicsTierRevenueAndPool: tier-aware pricing (Task 5). A // hosted-tier account prices normally — economics.PriceAccount's own // RevenueUSD stands. A pool-tier account, even one with real cost AND fleet // meters merged into its meters map (interface (a)'s merge, proven via // view.Meters below), gets RevenueUSD=0/MarginFrac=0 and FreeTierPos // overridden to "pooled" in the VIEW layer — never economics.PriceAccount's // own "inside"/"partial"/"paying", which is exactly the trap the carried // ruling calls out: a fleet-only pool account has real (fleet) revenue by // PriceAccount's tier-unaware maths, so naively zeroing revenue and then // recomputing FreeTierPos off that zero would misreport it "inside" the // free tier rather than "pooled". PoolLedger sums the paying side's revenue // and the pool side's cost per economicsview.go's documented formula, // top-up prorated to the period via view's own PeriodDays/DaysInMonth. func TestLoadEconomicsTierRevenueAndPool(t *testing.T) { fx := newConsoleFixture(t) ctx := context.Background() reg, err := registry.Open(fx.regPath) if err != nil { t.Fatal(err) } if err := reg.Upsert(registry.Route{Host: "a.example.com", Kind: "instance", Status: "ready", App: fixtureApp, Version: "v1"}); err != nil { t.Fatal(err) } date := usage.DayOf(time.Now()) cpu, mem := 0.5, 1e9 roll := metrics.DayRollup{V: 1, Box: "box1", Date: date, Hours: map[string]metrics.HourAgg{ "09": {Units: map[string]map[string]metrics.Agg{ "carlos-app@a.example.com.service": { "cpu": {Avg: cpu, N: 60}, "mem_bytes": {Avg: mem, N: 60}, }, }}, }} putStoreJSON(t, fx.rel, metrics.DayKey("box1", date), roll) // A second, pool-tier account with no console app and no registered // route of its own — CreateAccount alone is enough for it to land in // s.store.Accounts() and therefore the pricing universe. poolID := testAccountID() poolAcct := "poolsqid" if err := fx.st.CreateAccount(poolID, poolAcct, "Poolco", "paul@keymail.dev", time.Now()); err != nil { t.Fatal(err) } // Edge usage for both accounts on the same box's edge day: fixtureAcct // gets the golden test's revenue-bearing traffic, poolAcct gets its own // smaller row — EdgeRow.Account is read directly by sweepUsage with no // registry route required (economicspass.go's edge loop). putStoreJSON(t, fx.rel, usage.EdgeDayKey("box1", date), usage.EdgeDay{ V: 1, Box: "box1", Date: date, Hours: map[string]map[string]usage.EdgeRow{"09": { "a.example.com": {Account: fixtureAcct, App: fixtureApp, Requests: 4000, BytesOut: 2e9}, "b.example.com": {Account: poolAcct, App: "poolapp", Requests: 1000, BytesOut: 1e9}, }}, }) // Fleet meters (Task 3) attributed to poolAcct — proves interface (a)'s // merge reaches an account that never touched a platform route at all. if _, err := reg.CreateFleet(registry.Fleet{Name: "fx", Account: poolAcct, Bucket: "fx-data"}); err != nil { t.Fatal(err) } if err := reg.AddFleetBox(registry.FleetBox{Fleet: "fx", Label: "bx1", TokenHash: strings.Repeat("a", 64)}); err != nil { t.Fatal(err) } putStoreJSON(t, fx.rel, "fleets/fx/"+metrics.DayKey("bx1", date), metrics.DayRollup{ V: 1, Box: "bx1", Date: date, Hours: map[string]metrics.HourAgg{ "09": {Host: map[string]metrics.Agg{"cpu": {N: 60}}}, }, }) sc := economics.DefaultScenario() sc.Name = "test" putStoreJSON(t, fx.rel, economics.ScenarioKey("test"), sc) putStoreJSON(t, fx.rel, economics.ActiveKey, economics.Active{Schema: 1, Name: "test"}) if err := fx.st.SetAccountTier(OpsAccountID, "hosted"); err != nil { t.Fatal(err) } if err := fx.st.SetAccountTier(poolID, "pool"); err != nil { t.Fatal(err) } view, err := fx.srv.LoadEconomics(ctx, "mtd") if err != nil { t.Fatal(err) } var hostedRow, poolRow *economics.AccountEconomics for i := range view.Accounts { switch view.Accounts[i].Account { case fixtureAcct: hostedRow = &view.Accounts[i] case poolAcct: poolRow = &view.Accounts[i] } } if hostedRow == nil { t.Fatalf("no AccountEconomics row for hosted %q in %+v", fixtureAcct, view.Accounts) } if poolRow == nil { t.Fatalf("no AccountEconomics row for pool %q in %+v", poolAcct, view.Accounts) } if hostedRow.RevenueUSD <= 0 { t.Fatalf("hostedRow.RevenueUSD = %v, want > 0", hostedRow.RevenueUSD) } if poolRow.RevenueUSD != 0 { t.Fatalf("poolRow.RevenueUSD = %v, want 0 (pool tier)", poolRow.RevenueUSD) } if poolRow.MarginFrac != 0 { t.Fatalf("poolRow.MarginFrac = %v, want 0 (pool tier)", poolRow.MarginFrac) } if poolRow.CostUSD <= 0 { t.Fatalf("poolRow.CostUSD = %v, want > 0 (its cost stands)", poolRow.CostUSD) } if poolRow.FreeTierPos != "pooled" { t.Fatalf("poolRow.FreeTierPos = %q, want %q", poolRow.FreeTierPos, "pooled") } if got := view.Meters[poolAcct][usage.MeterFleetBoxHoursServer]; got <= 0 { t.Fatalf("Meters[%q][fleet_box_hours_server] = %v, want > 0 (fleet meters merged)", poolAcct, got) } if got, want := view.Pool.PayingRevenueUSD, hostedRow.RevenueUSD; math.Abs(got-want) > 1e-9 { t.Fatalf("Pool.PayingRevenueUSD = %v, want %v (only hosted/fleet/dedicated revenue)", got, want) } // Independent expected value for the topup term: today's day-of-month // and today's calendar month length, derived from the wall clock // directly rather than read back off view.PeriodDays/view.DaysInMonth // (the SUT's own reported fields) — an "mtd" period is wholly inside // the current month, so per-day accrual (poolTopupAccrual) reduces to // exactly this fraction, same as it always did for "mtd". nowUTC := time.Now().UTC() wantDaysElapsed := float64(nowUTC.Day()) wantDaysInMonth := float64(time.Date(nowUTC.Year(), nowUTC.Month()+1, 0, 0, 0, 0, 0, time.UTC).Day()) wantPoolUSD := sc.Pool.RevenueFrac*view.Pool.PayingRevenueUSD + sc.Pool.TopupUSD*(wantDaysElapsed/wantDaysInMonth) if math.Abs(view.Pool.PoolUSD-wantPoolUSD) > 1e-9 { t.Fatalf("Pool.PoolUSD = %v, want %v", view.Pool.PoolUSD, wantPoolUSD) } if math.Abs(view.Pool.DrawUSD-poolRow.CostUSD) > 1e-9 { t.Fatalf("Pool.DrawUSD = %v, want %v (sum of pool-tier CostUSD)", view.Pool.DrawUSD, poolRow.CostUSD) } wantHeadroom := view.Pool.PoolUSD - view.Pool.DrawUSD if math.Abs(view.Pool.HeadroomUSD-wantHeadroom) > 1e-9 { t.Fatalf("Pool.HeadroomUSD = %v, want %v (Pool - Draw)", view.Pool.HeadroomUSD, wantHeadroom) } if !strings.Contains(string(view.DataJSON), `"tiers"`) { t.Fatalf("DataJSON missing %q: %s", "tiers", view.DataJSON) } } // TestLoadEconomicsPoolTopupAccrualLast is the fix-round regression for // IMPORTANT 1: the pool's top-up term must accrue day-by-day // (poolTopupAccrual), not the brief's literal PeriodDays/DaysInMonth single // fraction — dishonest on period=last, since PeriodDays there counts LAST // month's days while DaysInMonth is pinned to THIS month's (a Jan-from-Feb // "last" view would credit 31/28 of a month, over 100%). No usage is seeded // at all — every live account (just the built-in ops account) sits at its // default "" pool tier with zero cost, so PayingRevenueUSD is exactly 0 and // PoolUSD reduces to the topup term alone. That lets the assertion use a // truly INDEPENDENT expected value — sc.Pool.TopupUSD itself, no formula, // no day arithmetic re-derived from the view at all — which per-day accrual // over the WHOLE previous calendar month must equal exactly: one full // month's top-up, never 110% of one. func TestLoadEconomicsPoolTopupAccrualLast(t *testing.T) { fx := newConsoleFixture(t) ctx := context.Background() sc := economics.DefaultScenario() sc.Name = "test" putStoreJSON(t, fx.rel, economics.ScenarioKey("test"), sc) putStoreJSON(t, fx.rel, economics.ActiveKey, economics.Active{Schema: 1, Name: "test"}) view, err := fx.srv.LoadEconomics(ctx, "last") if err != nil { t.Fatal(err) } if view.Pool.PayingRevenueUSD != 0 { t.Fatalf("Pool.PayingRevenueUSD = %v, want 0 (no usage seeded, only the pool-tier ops account exists)", view.Pool.PayingRevenueUSD) } if math.Abs(view.Pool.PoolUSD-sc.Pool.TopupUSD) > 1e-9 { t.Fatalf("Pool.PoolUSD = %v, want exactly %v (one full previous month's top-up; PayingRevenueUSD is 0)", view.Pool.PoolUSD, sc.Pool.TopupUSD) } } // TestLoadEconomicsWarnsOnMissingNamedScenario: active.json naming a // scenario that doesn't exist under economics/scenarios/ is a WARNING, not // silence — the spec distinguishes "no active.json at all" (fresh // deployment, silent DefaultScenario) from "active.json points at // something broken" (visible warning, still DefaultScenario so the page // still renders). func TestLoadEconomicsWarnsOnMissingNamedScenario(t *testing.T) { fx := newConsoleFixture(t) ctx := context.Background() putStoreJSON(t, fx.rel, economics.ActiveKey, economics.Active{Schema: 1, Name: "missing"}) view, err := fx.srv.LoadEconomics(ctx, "mtd") if err != nil { t.Fatal(err) } if view.Scenario.Name != economics.DefaultScenario().Name { t.Fatalf("Scenario.Name = %q, want default %q", view.Scenario.Name, economics.DefaultScenario().Name) } if view.ScenarioWarning == "" { t.Fatal("ScenarioWarning is empty, want a warning naming the unloadable scenario") } } // TestLoadEconomicsDefaultsSilentlyWithNoActiveFile: a fresh deployment with // no economics/active.json at all must fall back to DefaultScenario with NO // warning — that absence is the expected initial state, not a fault. func TestLoadEconomicsDefaultsSilentlyWithNoActiveFile(t *testing.T) { fx := newConsoleFixture(t) ctx := context.Background() view, err := fx.srv.LoadEconomics(ctx, "mtd") if err != nil { t.Fatal(err) } if view.Scenario.Name != economics.DefaultScenario().Name { t.Fatalf("Scenario.Name = %q, want default %q", view.Scenario.Name, economics.DefaultScenario().Name) } if view.ScenarioWarning != "" { t.Fatalf("ScenarioWarning = %q, want empty (missing active.json is a fresh deployment, not a fault)", view.ScenarioWarning) } } // TestLoadEconomicsDegradesWithoutRegistry: a console with no registry // configured/mounted (MINOR 2, fix round) must still render the dashboard — // sweepUsage simply attributes every sampled unit to release.OpsAccount, // same as any other unmatched unit — with a visible Warning, never a hard // error. This is the opposite of censusPass/statementsPass's own registry // guard (TestCensusPassRefusesMissingRegistry): those are WRITE passes // protecting a create-only day object from a bogus all-zero write; this is // a READ path with nothing to protect by refusing. func TestLoadEconomicsDegradesWithoutRegistry(t *testing.T) { fx := newConsoleFixture(t) ctx := context.Background() fx.srv.cfg.Registry = filepath.Join(t.TempDir(), "nonexistent-registry.db") view, err := fx.srv.LoadEconomics(ctx, "mtd") if err != nil { t.Fatalf("LoadEconomics with no registry returned an error, want a degraded view: %v", err) } if view.Warning == "" { t.Fatal("Warning is empty, want a notice that the sweep degraded (no registry)") } } // TestLoadEconomicsAccountsSortStableOnTies: CostUSD-desc sorting must // tie-break on Account ascending (MINOR 4, fix round) — otherwise the // zero-cost tail (every account with no usage this period, all tied at 0) // has no defined order and can visibly reshuffle on every background sweep. func TestLoadEconomicsAccountsSortStableOnTies(t *testing.T) { fx := newConsoleFixture(t) ctx := context.Background() now := time.Now() if err := fx.st.CreateAccount(testAccountID(), "zzzsqid", "Zedco", "paul@keymail.dev", now); err != nil { t.Fatal(err) } if err := fx.st.CreateAccount(testAccountID(), "aaasqid", "Aardco", "paul@keymail.dev", now); err != nil { t.Fatal(err) } view, err := fx.srv.LoadEconomics(ctx, "mtd") if err != nil { t.Fatal(err) } var order []string for _, a := range view.Accounts { if a.Account == "zzzsqid" || a.Account == "aaasqid" || a.Account == fixtureAcct { order = append(order, a.Account) } } want := []string{"aaasqid", fixtureAcct, "zzzsqid"} if len(order) != len(want) { t.Fatalf("order = %v, want %v", order, want) } for i := range want { if order[i] != want[i] { t.Fatalf("order = %v, want %v (Account ascending among CostUSD==0 ties)", order, want) } } }