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 `, `value="hosted" selected`, } { if !strings.Contains(body, want) { t.Fatalf("/fleet/economics/accounts missing %q in:\n%s", want, body) } } } // TestEconomicsAccountsPageTierFormOnlyForConsoleRows is the fix-round // regression for IMPORTANT 2 (Task 6 review): a usage-only account — real // traffic (usage.EdgeRow.Account is read directly by sweepUsage, // economicspass.go, independent of any registry route) under a sqid that // never got a console account row (accounts table) — is absent from // EconomicsView.Tiers entirely (Store.AccountTiers only ever selects live // accounts-table rows). Its row must still render a plain "unassigned" // chip, but WITHOUT the per-row tier-set form: POSTing that form's account // sqid to /fleet/economics/tier can only ever fail with "No such account." // (handleEconomicsTierSave's AccountBySqid lookup). A REAL console row that // simply has no tier assigned yet (the explicit "" default — fixtureAcct // here, left untouched) is a different case entirely and must still get // the form: index can't tell the two apart, which is exactly why tierKnown // (server.go) exists. func TestEconomicsAccountsPageTierFormOnlyForConsoleRows(t *testing.T) { fx := newConsoleFixture(t) seedEconomicsGolden(t, fx) // A usage-only account: edge traffic under a sqid with no accounts-table // row at all. A DIFFERENT box than seedEconomicsGolden's "box1", so this // writes its own EdgeDay object rather than colliding with the one // seedEconomicsGolden already created (putStoreJSON is create-only). 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) } 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) } // The usage-only ghost account's row: unassigned chip, no form. rowStart := strings.Index(body, fmt.Sprintf(`data-account="%s"`, ghostAcct)) if rowStart < 0 { t.Fatalf("/fleet/economics/accounts missing a row for the usage-only account %q in:\n%s", ghostAcct, body) } rowEnd := strings.Index(body[rowStart:], "") if rowEnd < 0 { t.Fatalf("unterminated row for %q in:\n%s", ghostAcct, body) } ghostRow := body[rowStart : rowStart+rowEnd] if !strings.Contains(ghostRow, `class="chip chip-tier-unassigned"`) { t.Fatalf("usage-only account %q row missing the unassigned chip:\n%s", ghostAcct, ghostRow) } if strings.Contains(ghostRow, "") if fixtureRowEnd < 0 { t.Fatalf("unterminated row for %q in:\n%s", fixtureAcct, body) } fixtureRow := body[fixtureRowStart : fixtureRowStart+fixtureRowEnd] if !strings.Contains(fixtureRow, `class="chip chip-tier-unassigned"`) { t.Fatalf("console-row account %q (untiered) row missing the unassigned chip:\n%s", fixtureAcct, fixtureRow) } if !strings.Contains(fixtureRow, "