package console
import (
"bytes"
"fmt"
"net/http"
"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"
)
// TestEconomicsOperatorOnly is TestFleetOperatorOnly's shape (server_test.go)
// against /fleet/economics: signed-out and non-operator both get the exact
// 404 an unrouted path gets, an operator gets 200 with the page's content,
// and revoking sends it straight back to that same 404 — the gate this page
// promises is byte-identical to every other operator-only route, never a
// separate "you can't see this" page that would itself confirm the route
// exists.
func TestEconomicsOperatorOnly(t *testing.T) {
fx := newConsoleFixture(t)
out := newBrowser(t)
statusGhost, _, bodyGhost := get(t, out, fx.ts.URL+"/this-route-does-not-exist")
if statusGhost != http.StatusNotFound {
t.Fatalf("sanity: a missing route = %d, want 404", statusGhost)
}
if status, _, body := get(t, out, fx.ts.URL+"/fleet/economics"); status != http.StatusNotFound || body != bodyGhost {
t.Fatalf("signed-out /fleet/economics = %d %q, want the same 404 as a missing route (%d %q)", status, body, statusGhost, bodyGhost)
}
// Signed in and owns kass, but is not an operator: still the same 404.
owner := signInPaul(t, fx)
if status, _, body := get(t, owner, fx.ts.URL+"/fleet/economics"); status != http.StatusNotFound || body != bodyGhost {
t.Fatalf("non-operator /fleet/economics = %d %q, want the same 404 as a missing route", status, body)
}
if err := fx.st.SetOperator("paul@keymail.dev", true); err != nil {
t.Fatal(err)
}
status, _, body := get(t, owner, fx.ts.URL+"/fleet/economics")
if status != 200 || !strings.Contains(body, "id=\"econ-data\"") {
t.Errorf("operator economics view: %d %s", status, body)
}
// Revoked: back to the exact same 404.
if err := fx.st.SetOperator("paul@keymail.dev", false); err != nil {
t.Fatal(err)
}
if status, _, body := get(t, owner, fx.ts.URL+"/fleet/economics"); status != http.StatusNotFound || body != bodyGhost {
t.Fatalf("revoked operator /fleet/economics = %d %q, want the same 404", status, body)
}
}
// seedEconomicsGolden reuses TestLoadEconomicsGolden's seeding
// (economicsview_test.go): one box ("box1"), one route, an hour of cpu/mem
// samples, and matching edge-traffic for the same route's account — enough
// for LoadEconomics to price a real account and a real box under the
// default scenario.
func seedEconomicsGolden(t *testing.T, fx *consoleFixture) {
t.Helper()
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())
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: 0.5, N: 60},
"mem_bytes": {Avg: 1e9, 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},
}},
})
}
// TestEconomicsPageContent proves the page renders real numbers from
// LoadEconomics — not just chrome: the coverage label, the box's own label,
// an account name, and the embedded payload every id="econ-data" the
// content test above already looked for by string is the SAME script tag
// this test confirms carries actual JSON.
func TestEconomicsPageContent(t *testing.T) {
fx := newConsoleFixture(t)
seedEconomicsGolden(t, fx)
c := signInPaul(t, fx)
if err := fx.st.SetOperator("paul@keymail.dev", true); err != nil {
t.Fatal(err)
}
status, _, body := get(t, c, fx.ts.URL+"/fleet/economics")
if status != 200 {
t.Fatalf("/fleet/economics = %d, want 200:\n%s", status, body)
}
for _, want := range []string{"days metered", "box1", fixtureAcct, `id="econ-data"`} {
if !strings.Contains(body, want) {
t.Fatalf("/fleet/economics missing %q in:\n%s", want, body)
}
}
}
// TestEconomicsPageMTDShowsProjection is the spec's "month-to-date with
// projection" promise (F2, unit-economics final review; corrected post-F2:
// the projection's target denominator is the full current month's length,
// EconomicsView.DaysInMonth — NOT PeriodDays, which on "mtd" is only the
// number of days elapsed so far and would make "projected" collapse to
// plain MTD at full coverage): on "mtd" with at least one covered day, each
// of the three dollar tiles (revenue, cost, profit — never margin, a ratio
// doesn't project) gains a second line naming the word "projected" and
// carrying economics.Project's own figure. Rendered directly through
// templates.ExecuteTemplate against a hand-built EconomicsView (the
// ladder_test.go pattern) rather than a real LoadEconomics sweep, so
// CoveredDays/PeriodDays are exact, known numbers instead of whatever
// today's real calendar date happens to produce. DaysInMonth is still
// derived from the real current date, the same formula buildEconomicsView
// uses, so the expected figure never drifts from what the page itself would
// compute on any given day.
func TestEconomicsPageMTDShowsProjection(t *testing.T) {
now := time.Now().UTC()
daysInMonth := time.Date(now.Year(), now.Month()+1, 0, 0, 0, 0, 0, time.UTC).Day()
view := &EconomicsView{
Period: "mtd",
CoveredDays: 10,
PeriodDays: 31,
DaysInMonth: daysInMonth,
Accounts: []economics.AccountEconomics{{Account: fixtureAcct, CostUSD: 100, RevenueUSD: 200}},
}
// No overhead/box-idle line items on the scenario, so the tiles' totals
// are exactly the one account's own numbers: revenue 200, cost 100,
// profit 100 — easy to hand-check economics.Project's daysInMonth/10
// scale-up against.
p := page{Title: "Fleet economics", CSRF: "tok", Economics: view}
var buf bytes.Buffer
if err := templates.ExecuteTemplate(&buf, "economics", p); err != nil {
t.Fatalf("render economics: %v", err)
}
html := buf.String()
wantRevenue := usd(economics.Project(200, 10, daysInMonth))
wantCost := usd(economics.Project(100, 10, daysInMonth))
wantProfit := usd(economics.Project(100, 10, daysInMonth))
for _, want := range []string{
"projected " + wantRevenue + " for the month",
"projected " + wantCost + " for the month",
"projected " + wantProfit + " for the month",
} {
if !strings.Contains(html, want) {
t.Fatalf("mtd page missing %q in:\n%s", want, html)
}
}
if !strings.Contains(html, "projected") {
t.Fatal("mtd page must contain the word \"projected\" (honesty rule)")
}
// A non-mtd period must show none of it: the projected line only ever
// exists on "mtd" (economics.html's $showProjection guard).
view7d := &EconomicsView{
Period: "7d",
CoveredDays: 7,
PeriodDays: 7,
Accounts: []economics.AccountEconomics{{Account: fixtureAcct, CostUSD: 100, RevenueUSD: 200}},
}
p7d := page{Title: "Fleet economics", CSRF: "tok", Economics: view7d}
var buf7d bytes.Buffer
if err := templates.ExecuteTemplate(&buf7d, "economics", p7d); err != nil {
t.Fatalf("render economics (7d): %v", err)
}
if strings.Contains(buf7d.String(), "projected") {
t.Fatalf("?period=7d page must not contain \"projected\":\n%s", buf7d.String())
}
}
// usd mirrors server.go's "usd" template func (same rounding, same sign
// handling) so this test's expected strings are computed the exact way the
// page itself renders them, without reaching into the unexported FuncMap
// closure directly.
func usd(v float64) string {
if v < 0 {
return fmt.Sprintf("-$%.2f", -v)
}
return fmt.Sprintf("$%.2f", v)
}
// TestEconomicsPageHonestUnmetered: a box with a metrics day rollup but no
// usage/edge day ever written must render "unmetered" for that box's egress
// — never a blank or a fabricated zero. See BoxEconomics.EgressMetered and
// usageSweep.EgressMetered's doc comments (economicspass.go): a box simply
// absent from the edge-boxes listing is unmetered, not "confirmed no
// egress".
func TestEconomicsPageHonestUnmetered(t *testing.T) {
fx := newConsoleFixture(t)
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())
roll := metrics.DayRollup{V: 1, Box: "darkbox", Date: date, Hours: map[string]metrics.HourAgg{
"09": {Units: map[string]map[string]metrics.Agg{
"carlos-app@a.example.com.service": {
"cpu": {Avg: 0.5, N: 60},
"mem_bytes": {Avg: 1e9, N: 60},
},
}},
}}
putStoreJSON(t, fx.rel, metrics.DayKey("darkbox", date), roll)
// Deliberately no usage.EdgeDayKey("darkbox", date) object: darkbox has
// metrics but has never written an edge day.
c := signInPaul(t, fx)
if err := fx.st.SetOperator("paul@keymail.dev", true); err != nil {
t.Fatal(err)
}
status, _, body := get(t, c, fx.ts.URL+"/fleet/economics")
if status != 200 {
t.Fatalf("/fleet/economics = %d, want 200:\n%s", status, body)
}
if !strings.Contains(body, "darkbox") {
t.Fatalf("/fleet/economics missing box label %q in:\n%s", "darkbox", body)
}
if !strings.Contains(body, "unmetered") {
t.Fatalf("/fleet/economics missing %q for a box with no usage/edge day:\n%s", "unmetered", body)
}
}
// economics.DefaultScenario is used implicitly by every test above (no
// active.json seeded) — sanity-checked once here so a future change to it
// that breaks Validate fails loudly at the source, not as a mystery 500 on
// this page.
func TestEconomicsDefaultScenarioValid(t *testing.T) {
sc := economics.DefaultScenario()
if err := sc.Validate(); err != nil {
t.Fatalf("DefaultScenario: %v", err)
}
}
// TestEconomicsPageBillServicesGuardShape is a structural stand-in for a
// browser test (none exists in this suite — no headless DOM): it greps the
// SERVED page for the exact guard shapes the fix-round review demanded, so
// a future edit that reintroduces either bug fails a test instead of only
// a manual re-review.
//
// Finding 1 (economics.html, fix round 1): the original one-line ternary
// `(boxes.length > 0 && checked.length < boxes.length) ? checked : []`
// wrote [] whenever no bill checkboxes existed on screen — clobbering the
// base scenario's saved bill_services from an unrelated keystroke in the
// manual-bill-entry form sharing #econ-live. The fix wraps the whole
// bill_services branch in `if (boxes.length > 0)`, leaving sc.bill_services
// untouched when there are no checkboxes at all.
//
// Finding 2: an all-unchecked selection is not a representable
// reconciliation state (the empty list already means "count everything").
// The fix keeps a lastNonEmptyBillServices variable and an inline
// #bill-services-zero-note the script shows exactly when every checkbox is
// unchecked.
func TestEconomicsPageBillServicesGuardShape(t *testing.T) {
fx := newConsoleFixture(t)
seedEconomicsGolden(t, fx)
c := signInPaul(t, fx)
if err := fx.st.SetOperator("paul@keymail.dev", true); err != nil {
t.Fatal(err)
}
status, _, body := get(t, c, fx.ts.URL+"/fleet/economics")
if status != 200 {
t.Fatalf("/fleet/economics = %d, want 200:\n%s", status, body)
}
old := "(boxes.length > 0 && checked.length < boxes.length) ? checked : []"
if strings.Contains(body, old) {
t.Fatalf("/fleet/economics still contains the pre-fix ungated ternary %q — Finding 1 regressed", old)
}
for _, want := range []string{
// Finding 1: the bill_services branch is gated on boxes existing,
// so a page with no bill on screen never touches sc.bill_services.
`if (boxes.length > 0) {`,
// Finding 2: the last-non-empty-selection fallback and its visible
// note both exist, wired to the all-unchecked branch.
"lastNonEmptyBillServices",
`id="bill-services-zero-note"`,
"sc.bill_services = lastNonEmptyBillServices !== null ? lastNonEmptyBillServices : sc.bill_services;",
"setBillServicesZeroNote(true)",
"setBillServicesZeroNote(false)",
} {
if !strings.Contains(body, want) {
t.Fatalf("/fleet/economics missing guard-shape string %q", want)
}
}
// The header comment claiming a "Project" transliteration was dropped
// (the script renders no projections) — check the doc-comment line
// itself, not the word in isolation (which could legitimately appear
// elsewhere, e.g. an English sentence using "project" as a verb).
if strings.Contains(body, "AllocateBox/PriceAccount/Project") {
t.Fatalf("/fleet/economics still claims a Project transliteration in its header comment")
}
}
// TestEconomicsAccountsOperatorOnly is TestEconomicsOperatorOnly's shape,
// cloned against /fleet/economics/accounts (Task 14): signed-out and
// non-operator both get the exact 404 an unrouted path gets, an operator
// gets 200 with the page's content, and revoking sends it straight back to
// that same 404.
func TestEconomicsAccountsOperatorOnly(t *testing.T) {
fx := newConsoleFixture(t)
out := newBrowser(t)
statusGhost, _, bodyGhost := get(t, out, fx.ts.URL+"/this-route-does-not-exist")
if statusGhost != http.StatusNotFound {
t.Fatalf("sanity: a missing route = %d, want 404", statusGhost)
}
if status, _, body := get(t, out, fx.ts.URL+"/fleet/economics/accounts"); status != http.StatusNotFound || body != bodyGhost {
t.Fatalf("signed-out /fleet/economics/accounts = %d %q, want the same 404 as a missing route (%d %q)", status, body, statusGhost, bodyGhost)
}
// Signed in and owns kass, but is not an operator: still the same 404.
owner := signInPaul(t, fx)
if status, _, body := get(t, owner, fx.ts.URL+"/fleet/economics/accounts"); status != http.StatusNotFound || body != bodyGhost {
t.Fatalf("non-operator /fleet/economics/accounts = %d %q, want the same 404 as a missing route", status, body)
}
if err := fx.st.SetOperator("paul@keymail.dev", true); err != nil {
t.Fatal(err)
}
status, _, body := get(t, owner, fx.ts.URL+"/fleet/economics/accounts")
if status != 200 || !strings.Contains(body, "id=\"econ-data\"") {
t.Errorf("operator economics accounts view: %d %s", status, body)
}
// Revoked: back to the exact same 404.
if err := fx.st.SetOperator("paul@keymail.dev", false); err != nil {
t.Fatal(err)
}
if status, _, body := get(t, owner, fx.ts.URL+"/fleet/economics/accounts"); status != http.StatusNotFound || body != bodyGhost {
t.Fatalf("revoked operator /fleet/economics/accounts = %d %q, want the same 404", status, body)
}
}
// TestEconomicsAccountsPageContent proves the page renders one real
// account row from LoadEconomics, joined with its own meters: seeds the
// same golden fixture as TestEconomicsPageContent (one box, one route, 2GB
// egress over 4000 requests for fixtureAcct — DefaultScenario's free tier
// fully covers that egress, so PriceAccount marks the row "partial", per
// PriceAccount's own clipped rule: any one meter benefiting from the free
// tier is enough, independent of the others). Asserts the account name, its
// egress-GB meter cell (2.00, exactly the seeded 2e9 bytes), and the
// free-tier chip all render.
func TestEconomicsAccountsPageContent(t *testing.T) {
fx := newConsoleFixture(t)
seedEconomicsGolden(t, fx)
// Task 5 (tier-aware revenue): an untiered account is pool-tier by
// default and renders FreeTierPos "pooled", not a priced position —
// this test wants the fixture priced normally to assert a real
// "partial" chip, so it needs the fixture account tiered "hosted".
// Same sanctioned pattern as economicsview_test.go's
// TestLoadEconomicsGolden (economics-v2 plan, Task 5); no production
// behavior change.
if err := fx.st.SetAccountTier(OpsAccountID, "hosted"); err != nil {
t.Fatal(err)
}
c := signInPaul(t, fx)
if err := fx.st.SetOperator("paul@keymail.dev", true); err != nil {
t.Fatal(err)
}
status, _, body := get(t, c, fx.ts.URL+"/fleet/economics/accounts")
if status != 200 {
t.Fatalf("/fleet/economics/accounts = %d, want 200:\n%s", status, body)
}
for _, want := range []string{
fixtureAcct,
">2.00<", // the egress-GB meter cell (2e9 bytes seeded)
`class="chip chip-freetier-partial"`, // the free-tier chip, this fixture's position
`id="econ-data"`,
} {
if !strings.Contains(body, want) {
t.Fatalf("/fleet/economics/accounts missing %q in:\n%s", want, body)
}
}
}
// TestEconomicsAccountsPageRendersPooledChip is the fix-round regression for
// IMPORTANT 2: "pooled" (Task 5's fourth FreeTierPos word) must render as a
// first-class chip on the accounts page, both on the row itself and as a
// countable header filter chip — the fixture account here is deliberately
// left UNTIERED (the default "" tier, pool semantics), so its row lands in
// "pooled" rather than a priced position, same as a fresh console with no
// tiers placed yet.
func TestEconomicsAccountsPageRendersPooledChip(t *testing.T) {
fx := newConsoleFixture(t)
seedEconomicsGolden(t, fx)
c := signInPaul(t, fx)
if err := fx.st.SetOperator("paul@keymail.dev", true); err != nil {
t.Fatal(err)
}
status, _, body := get(t, c, fx.ts.URL+"/fleet/economics/accounts")
if status != 200 {
t.Fatalf("/fleet/economics/accounts = %d, want 200:\n%s", status, body)
}
for _, want := range []string{
`class="chip chip-freetier-pooled"`, // the row's own free-tier chip
">pooled 1<", // the header filter chip, count 1
} {
if !strings.Contains(body, want) {
t.Fatalf("/fleet/economics/accounts missing %q in:\n%s", want, body)
}
}
}
// TestEconomicsAccountsPageThreadsPeriod is the fix-round regression for
// the silent-wrong-data bug: every list control on this page (search form,
// pos-filter chips, period nav) must carry the SAME period the rows on
// screen were priced under. Seeds the golden fixture inside LAST calendar
// month's window specifically (economicsPeriodBounds("last", now)) —
// same shape as seedEconomicsGolden, just dated a month back — so
// ?period=last&pos=partial has a genuine matching row (this fixture's
// egress is fully free-tier-covered under DefaultScenario, same reasoning
// as TestEconomicsAccountsPageContent) and renders a real "partial" chip,
// rather than asserting only on chrome that happens to render with zero
// matches.
func TestEconomicsAccountsPageThreadsPeriod(t *testing.T) {
fx := newConsoleFixture(t)
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)
}
// A date safely inside last calendar month by the wall clock — the same
// "relative to real time" rule TestLoadEconomicsGolden documents.
lastMonth := time.Now().UTC().AddDate(0, -1, 0)
date := usage.DayOf(lastMonth)
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: 0.5, N: 60},
"mem_bytes": {Avg: 1e9, 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},
}},
})
c := signInPaul(t, fx)
if err := fx.st.SetOperator("paul@keymail.dev", true); err != nil {
t.Fatal(err)
}
status, _, body := get(t, c, fx.ts.URL+"/fleet/economics/accounts?period=last&pos=partial&cursor=abc123")
if status != 200 {
t.Fatalf("/fleet/economics/accounts?period=last = %d, want 200:\n%s", status, body)
}
// 1. The search form carries period forward as a hidden field.
if !strings.Contains(body, ``) {
t.Fatalf("missing hidden period field in the search form:\n%s", body)
}
// 2. A pos-filter chip's href carries period=last — this fixture's
// "partial" row makes that chip's presence deterministic rather than
// incidental. html/template escapes "&" to "&" inside an href
// attribute value.
if !strings.Contains(body, `href="?period=last&pos=partial"`) {
t.Fatalf("missing the partial-position chip href carrying period=last:\n%s", body)
}
// 3. The period nav preserves pos= (the operator's filter survives a
// period switch) but never a cursor. The "Month to date" link
// switches TO the default period, so period= itself is omitted (the
// same "omit when mtd" rule as everywhere else on this page),
// leaving exactly "?pos=partial" — no period=, no cursor=.
if !strings.Contains(body, `href="?pos=partial"`) {
t.Fatalf("missing the Month-to-date period link preserving pos=partial with no period/cursor:\n%s", body)
}
}
// TestEconomicsAccountsPageShowsAccountName (account-names batch): a row for
// an account with a known console name renders the NAME as primary text
// with the sqid muted+small beside it (class="small", the existing
// secondary-text treatment; there is no separate ".muted" CSS class, only
// the ".small" rule, which
// already carries var(--muted)). A usage-only ghost row — real traffic under
// a sqid with no accounts-table row at all, same shape as
// TestEconomicsAccountsPageTierFormOnlyForConsoleRows — renders the bare
// sqid alone, exactly as before this batch. Checked on BOTH pages: the
// dashboard's own accounts table (economics.html, which has no econAccountRow
// and must look Name up from EconomicsView.Names) and the searchable
// accounts page (economics_accounts.html, whose econAccountRow now carries
// its own Name field).
func TestEconomicsAccountsPageShowsAccountName(t *testing.T) {
fx := newConsoleFixture(t)
const ghostAcct = "ghostacct"
date := usage.DayOf(time.Now())
putStoreJSON(t, fx.rel, usage.EdgeDayKey("boxghost", date), usage.EdgeDay{
V: 1, Box: "boxghost", Date: date,
Hours: map[string]map[string]usage.EdgeRow{"09": {
"ghost.example.com": {Account: ghostAcct, App: "ghostapp", Requests: 100, BytesOut: 1e6},
}},
})
c := signInPaul(t, fx)
if err := fx.st.SetOperator("paul@keymail.dev", true); err != nil {
t.Fatal(err)
}
for _, path := range []string{"/fleet/economics", "/fleet/economics/accounts"} {
status, _, body := get(t, c, fx.ts.URL+path)
if status != 200 {
t.Fatalf("%s = %d, want 200:\n%s", path, status, body)
}
fixtureRowStart := strings.Index(body, fmt.Sprintf(`data-account="%s"`, fixtureAcct))
if fixtureRowStart < 0 {
t.Fatalf("%s missing a row for %q in:\n%s", path, fixtureAcct, body)
}
fixtureRowEnd := strings.Index(body[fixtureRowStart:], "")
if fixtureRowEnd < 0 {
t.Fatalf("%s unterminated row for %q in:\n%s", path, fixtureAcct, body)
}
fixtureRow := body[fixtureRowStart : fixtureRowStart+fixtureRowEnd]
if !strings.Contains(fixtureRow, "ops "+fixtureAcct+"") {
t.Fatalf("%s: known-name row missing NAME %q as primary text with the sqid muted+small beside it:\n%s", path, "ops", fixtureRow)
}
ghostRowStart := strings.Index(body, fmt.Sprintf(`data-account="%s"`, ghostAcct))
if ghostRowStart < 0 {
t.Fatalf("%s missing a row for %q in:\n%s", path, ghostAcct, body)
}
ghostRowEnd := strings.Index(body[ghostRowStart:], "")
if ghostRowEnd < 0 {
t.Fatalf("%s unterminated row for %q in:\n%s", path, ghostAcct, body)
}
ghostRow := body[ghostRowStart : ghostRowStart+ghostRowEnd]
if !strings.Contains(ghostRow, ">"+ghostAcct+"<") {
t.Fatalf("%s: ghost row missing the bare sqid %q:\n%s", path, ghostAcct, ghostRow)
}
if strings.Contains(ghostRow, ``) {
t.Fatalf("%s: ghost row (no console name) must not render a muted-sqid span:\n%s", path, ghostRow)
}
}
}
// ---- Task 6: pool tile, v2 knobs, tier chips -------------------------------
// TestEconomicsPagePoolTile proves the dashboard renders id="tile-pool" with
// the "POOL $X · draw $Y · headroom $Z" figures straight from
// EconomicsView.Pool — templates.ExecuteTemplate against a hand-built view
// (TestEconomicsPageMTDShowsProjection's pattern), so the expected dollar
// strings are exact, known numbers rather than whatever a real sweep
// produces.
func TestEconomicsPagePoolTile(t *testing.T) {
view := &EconomicsView{
Period: "mtd",
Pool: PoolLedger{PoolUSD: 123.45, DrawUSD: 50, HeadroomUSD: 73.45, PayingRevenueUSD: 1000},
}
p := page{Title: "Fleet economics", CSRF: "tok", Economics: view}
var buf bytes.Buffer
if err := templates.ExecuteTemplate(&buf, "economics", p); err != nil {
t.Fatalf("render economics: %v", err)
}
html := buf.String()
for _, want := range []string{
`id="tile-pool"`,
"POOL",
`id="tile-pool-usd">` + usd(123.45),
"draw",
`id="tile-pool-draw">` + usd(50),
"headroom",
`id="tile-pool-headroom">` + usd(73.45),
} {
if !strings.Contains(html, want) {
t.Fatalf("/fleet/economics missing pool tile string %q in:\n%s", want, html)
}
}
}
// TestEconomicsPagePoolTileNegativeHeadroomWarns: a negative HeadroomUSD (the
// pool is overdrawn this period) must render honestly — never clamped to
// zero (PoolLedger's own doc comment) — AND in the page's existing warning
// styling, per the brief. The headroom span carries class="error" (the same
// class every other on-page warning uses — layout.html's ".error { color:
// var(--bad); }") exactly when HeadroomUSD is negative, never otherwise.
func TestEconomicsPagePoolTileNegativeHeadroomWarns(t *testing.T) {
view := &EconomicsView{
Period: "mtd",
Pool: PoolLedger{PoolUSD: 10, DrawUSD: 50, HeadroomUSD: -40},
}
p := page{Title: "Fleet economics", CSRF: "tok", Economics: view}
var buf bytes.Buffer
if err := templates.ExecuteTemplate(&buf, "economics", p); err != nil {
t.Fatalf("render economics: %v", err)
}
html := buf.String()
if !strings.Contains(html, `id="tile-pool-headroom" class="error"`) {
t.Fatalf("/fleet/economics: negative headroom must carry the warning class:\n%s", html)
}
if !strings.Contains(html, `class="error">`+usd(-40)) {
t.Fatalf("/fleet/economics missing the honest negative headroom figure %q in:\n%s", usd(-40), html)
}
// Positive headroom: no warning class at all.
viewOK := &EconomicsView{Period: "mtd", Pool: PoolLedger{PoolUSD: 50, DrawUSD: 10, HeadroomUSD: 40}}
pOK := page{Title: "Fleet economics", CSRF: "tok", Economics: viewOK}
var bufOK bytes.Buffer
if err := templates.ExecuteTemplate(&bufOK, "economics", pOK); err != nil {
t.Fatalf("render economics (positive headroom): %v", err)
}
if strings.Contains(bufOK.String(), `id="tile-pool-headroom" class="error"`) {
t.Fatalf("/fleet/economics: positive headroom must not carry the warning class:\n%s", bufOK.String())
}
}
// TestEconomicsPagePooledMarginIsDash is the carried ruling (Task 6 review,
// point 3): a pooled row's margin cell renders an em-dash, never "0%" —
// PriceAccount's MarginFrac is genuinely 0 for the pool override
// (buildEconomicsView), but "0%" reads as a account LOSING money, not one
// that simply isn't individually priced. Checked on both /fleet/economics
// and /fleet/economics/accounts, server-rendered (no JS in this test suite).
func TestEconomicsPagePooledMarginIsDash(t *testing.T) {
pooledRow := economics.AccountEconomics{Account: fixtureAcct, CostUSD: 5, RevenueUSD: 0, MarginFrac: 0, FreeTierPos: "pooled"}
view := &EconomicsView{Period: "mtd", Accounts: []economics.AccountEconomics{pooledRow}}
p := page{Title: "Fleet economics", CSRF: "tok", Economics: view}
var buf bytes.Buffer
if err := templates.ExecuteTemplate(&buf, "economics", p); err != nil {
t.Fatalf("render economics: %v", err)
}
html := buf.String()
if !strings.Contains(html, `class="js-acct-margin">—<`) {
t.Fatalf("/fleet/economics: pooled row's margin cell must be an em-dash, not a percentage:\n%s", html)
}
if strings.Contains(html, `class="js-acct-margin">0%<`) {
t.Fatalf("/fleet/economics: pooled row's margin cell must not render \"0%%\":\n%s", html)
}
rows := pageEconAccounts([]econAccountRow{{Account: fixtureAcct, Meters: map[string]float64{}, Econ: pooledRow}}, econAccountQuery{}, econAccountPageSize)
pAcct := page{Title: "Fleet economics: accounts", CSRF: "tok", Economics: view, EconAccounts: rows, EconQuery: econAccountQuery{}}
var bufAcct bytes.Buffer
if err := templates.ExecuteTemplate(&bufAcct, "economics_accounts", pAcct); err != nil {
t.Fatalf("render economics_accounts: %v", err)
}
htmlAcct := bufAcct.String()
if !strings.Contains(htmlAcct, `class="js-acct-margin">—<`) {
t.Fatalf("/fleet/economics/accounts: pooled row's margin cell must be an em-dash, not a percentage:\n%s", htmlAcct)
}
}
// TestEconomicsPageFleetMarginLegend is carried ruling 4 PLUS the fix-round
// MINOR 3 (Task 6 review): fleet-only accounts read 100%% margin by design
// (customer hardware, zero cost to the platform — PriceAccount's cost
// formula has no fleet term at all), and a number that looks like a
// modelling error gets an explanation somewhere on the page (dashboard-truth
// rule, AGENTS.md). The title= tooltip on the Margin header is invisible on
// touch/keyboard, so both notes ALSO need a visible one-line legend under
// each table — this asserts the tooltip AND the legend paragraph, on both
// pages, not just "the text appears somewhere" (which the tooltip alone
// would already satisfy and silently let the legend regress).
func TestEconomicsPageFleetMarginLegend(t *testing.T) {
// A margin-column explanation only means anything once the accounts
// table itself renders (both pages hide the table entirely with zero
// rows), so this seeds one row on each — not a claim that the row
// itself is a fleet account, just enough for the header/legend markup
// carrying the explanation to be on screen at all.
row := economics.AccountEconomics{Account: fixtureAcct, CostUSD: 0, RevenueUSD: 10, MarginFrac: 1, FreeTierPos: "paying"}
view := &EconomicsView{Period: "mtd", Accounts: []economics.AccountEconomics{row}}
p := page{Title: "Fleet economics", CSRF: "tok", Economics: view}
var buf bytes.Buffer
if err := templates.ExecuteTemplate(&buf, "economics", p); err != nil {
t.Fatalf("render economics: %v", err)
}
html := buf.String()
if !strings.Contains(html, `title="Pooled rows`) {
t.Fatalf("/fleet/economics missing the Margin header's title= tooltip:\n%s", html)
}
if !strings.Contains(html, `
Margin: pooled rows show an em-dash`) {
t.Fatalf("/fleet/economics missing the VISIBLE margin legend line (MINOR 3 — tooltips alone are invisible on touch/keyboard):\n%s", html)
}
rows := pageEconAccounts([]econAccountRow{{Account: fixtureAcct, Meters: map[string]float64{}, Econ: row}}, econAccountQuery{}, econAccountPageSize)
pAcct := page{Title: "Fleet economics: accounts", CSRF: "tok", Economics: view, EconAccounts: rows, EconQuery: econAccountQuery{}}
var bufAcct bytes.Buffer
if err := templates.ExecuteTemplate(&bufAcct, "economics_accounts", pAcct); err != nil {
t.Fatalf("render economics_accounts: %v", err)
}
htmlAcct := bufAcct.String()
if !strings.Contains(htmlAcct, `title="Pooled rows`) {
t.Fatalf("/fleet/economics/accounts missing the Margin header's title= tooltip:\n%s", htmlAcct)
}
if !strings.Contains(htmlAcct, `
Margin: pooled rows show an em-dash`) {
t.Fatalf("/fleet/economics/accounts missing the VISIBLE margin legend line (MINOR 3):\n%s", htmlAcct)
}
}
// TestEconomicsKnobDrawerHasV2Knobs proves the shared knob drawer
// (econ_knob_drawer) exposes all seven new economics-v2 knobs the brief
// names, with values sourced from the active scenario — box_month_server_usd
// and box_month_device_usd and mem_gb_hour_usd (Sheet), fleet_break.min_boxes
// and fleet_break.discount, pool.revenue_frac and pool.topup_usd.
func TestEconomicsKnobDrawerHasV2Knobs(t *testing.T) {
sc := economics.DefaultScenario()
sc.Sheet.BoxMonthServerUSD = 7.5
sc.Sheet.BoxMonthDeviceUSD = 1.5
sc.Sheet.MemGBHourUSD = 0.02
sc.FleetBreak.MinBoxes = 12
sc.FleetBreak.Discount = 0.3
sc.Pool.RevenueFrac = 0.15
sc.Pool.TopupUSD = 250
view := &EconomicsView{Period: "mtd", Scenario: sc}
p := page{Title: "Fleet economics", CSRF: "tok", Economics: view}
var buf bytes.Buffer
if err := templates.ExecuteTemplate(&buf, "economics", p); err != nil {
t.Fatalf("render economics: %v", err)
}
html := buf.String()
for _, want := range []string{
`id="knob-sheet-box-month-server-usd" value="7.5"`,
`id="knob-sheet-box-month-device-usd" value="1.5"`,
`id="knob-sheet-mem-gb-hour-usd" value="0.02"`,
`id="knob-fleet-break-min-boxes" value="12"`,
`id="knob-fleet-break-discount" value="0.3"`,
`id="knob-pool-revenue-frac" value="0.15"`,
`id="knob-pool-topup-usd" value="250"`,
} {
if !strings.Contains(html, want) {
t.Fatalf("knob drawer missing %q in:\n%s", want, html)
}
}
}
// TestEconomicsJSTwinsShape is a structural stand-in for a browser test (this
// suite has no headless DOM — same rationale as
// TestEconomicsPageBillServicesGuardShape): greps the served page's inline
// script for the exact shapes the carried rulings (Task 6 review) demand, so
// a future edit that drops one of them fails a test instead of only a manual
// re-review.
//
// Ruling 1 (JS meter guards + fleet formulas): every new meter read is
// guarded against undefined, the fleet revenue term sits OUTSIDE the
// cost-plus/rate-sheet branch (mirroring compute.go's PriceAccount exactly),
// the volume-break discount applies only when min_boxes>0 AND avg boxes
// clears it, and the rate-sheet arm's mem overage term is present.
//
// Ruling 2 (tier-aware JS): a tierOf/isPoolTier pair mirrors
// economicsview.go's same-named functions, and poolLedger mirrors
// buildEconomicsView's PoolLedger construction — including the exact
// linear-scaling topup term (payload.poolTopupUnitAccrual × the live
// topup_usd knob) rather than any client-side re-derivation of the per-day
// sum.
func TestEconomicsJSTwinsShape(t *testing.T) {
fx := newConsoleFixture(t)
seedEconomicsGolden(t, fx)
c := signInPaul(t, fx)
if err := fx.st.SetOperator("paul@keymail.dev", true); err != nil {
t.Fatal(err)
}
status, _, body := get(t, c, fx.ts.URL+"/fleet/economics")
if status != 200 {
t.Fatalf("/fleet/economics = %d, want 200:\n%s", status, body)
}
for _, want := range []string{
// Ruling 1: guarded fleet meter reads, fleet term outside the mode
// branch, the SAME bMem binding used in the rate-sheet arm's mem
// overage term.
`(meters.fleet_box_hours_server || 0)`,
`(meters.fleet_box_hours_device || 0)`,
"bMem * sc.sheet.mem_gb_hour_usd",
"revenue += fleetRevenue",
"sc.fleet_break.min_boxes > 0 && avgBoxes >= sc.fleet_break.min_boxes",
// Ruling 2: tier helpers + pool ledger twin, exact linear topup scaling.
"function tierOf(",
"function isPoolTier(",
"function poolLedger(",
"sc.pool.topup_usd * payload.poolTopupUnitAccrual",
// Ruling 3: marginCell renders the em-dash, never "0%", for a pooled row.
"function marginCell(",
`pos === "pooled"`,
// readScenarioFromDOM reads all seven new knobs back into the scenario.
"sc.sheet.box_month_server_usd = numVal(",
"sc.sheet.box_month_device_usd = numVal(",
"sc.sheet.mem_gb_hour_usd = numVal(",
"sc.fleet_break.min_boxes = numVal(",
"sc.fleet_break.discount = numVal(",
"sc.pool.revenue_frac = numVal(",
"sc.pool.topup_usd = numVal(",
// recompute() applies the tier override AFTER priceAccount, so a knob
// tweak can never flip a pooled row back to priced.
"var tiers = payload.tiers || {};",
"isPoolTier(tierOf(tiers, acct))",
`a.freeTierPos = "pooled";`,
} {
if !strings.Contains(body, want) {
t.Fatalf("/fleet/economics inline script missing %q", want)
}
}
}
// TestEconomicsAccountsPageTierChipAndForm proves the accounts page renders
// a chip-tier- chip for a tiered account and a per-row operator form
// (progressive: a plain