package lua import ( "encoding/json" "os/exec" "strings" "testing" ) // runBillingPage executes the billing page's mount script under node against a // DOM stub, drives the period selector, and returns what ended up on screen. // // It asserts against RENDERED output rather than the plugin's internal // helpers. Reaching into the IIFE for daysInWindow/rescale needs a test-only // hook in the plugin, and the first version of this file took a long detour // through such a hook: the harness's fake `Date` broke toISOString(), the // plugin script aborted before assigning the hook, and every assertion // compared "" against a real value. The rendered tables are the contract that // actually matters, they need no hook, and they cannot pass if the rescale is // wrong — which is exactly the bug class under test. func runBillingPage(t *testing.T, todayISO, period, captureID string) string { t.Helper() node, err := exec.LookPath("node") if err != nil { t.Skipf("node not available (%v)", err) } page, _ := billingUI(t) js := strings.Join(extractScripts(page), "\n") if !strings.Contains(js, "__billing_setPeriod") { t.Fatal("billing page exposes no period selector") } harness := ` global.window = global; global.__written = []; global.__errors = []; var __realErr = console.error; console.error = function () { global.__errors.push("console.error: " + Array.prototype.map.call(arguments, function (a) { return (a && a.message) ? a.message : String(a); }).join(" ")); __realErr.apply(console, arguments); }; process.on("uncaughtException", function (e) { global.__errors.push("uncaught: " + String(e && e.message || e)); }); process.on("unhandledRejection", function (e) { global.__errors.push("unhandled: " + String(e && e.message || e)); }); // Pin "now" for the window under test. // // Two earlier attempts were wrong here, and both looked like product bugs: // replacing the Date constructor wholesale breaks toISOString/UTC; and // subclassing (setting __FakeDate.prototype = __RealDate.prototype while // returning __RealDate instances) leaves instances WITHOUT a working // getUTCDay, so the week window came out NaN and every rendered day became // "Invalid time value". A Proxy over the real constructor intercepts only the // zero-argument call and leaves instances — and therefore every prototype // method — completely untouched. var __NOW_MS = new Date("` + todayISO + `T12:34:56Z").getTime(); var __RealDate = global.Date; var __PatchedDate = new Proxy(__RealDate, { construct: function (target, args) { // Reflect.construct, not a hand-spelled argument list: writing // new target(a, b, c, d, e, f, g) passes seven explicit undefineds for a // one-argument call, which Date reads as year..millisecond fields and turns // into an Invalid Date. The symptom was a week window rendering // "Invalid time value" for every day — a harness fault indistinguishable // from a product bug. if (args.length === 0) return Reflect.construct(target, [__NOW_MS]); return Reflect.construct(target, args); }, apply: function () { return new __RealDate(__NOW_MS); } }); __PatchedDate.now = function () { return __NOW_MS; }; __PatchedDate.UTC = __RealDate.UTC; __PatchedDate.parse = __RealDate.parse; __PatchedDate.prototype = __RealDate.prototype; global.Date = __PatchedDate; global.document = { getElementById: function (id) { if (!global.__els) global.__els = {}; if (!global.__els[id]) global.__els[id] = { style: {}, dataset: {}, classList: { add: function(){}, remove: function(){} }, options: [{value:"day"},{value:"week"},{value:"month"},{value:"all"}], set innerHTML(v) { if (v && String(v).trim()) global.__written.push(id); this.__h = v; }, get innerHTML() { return this.__h || ""; }, set textContent(v) { if (v !== undefined && String(v).trim()) global.__written.push(id); this.__t = v; }, get textContent() { return this.__t || ""; }, set innerText(v) { if (v !== undefined && String(v).trim()) global.__written.push(id); this.__i = v; }, get innerText() { return this.__i || ""; }, appendChild: function(){}, querySelector: function(){ return null; }, querySelectorAll: function(){ return []; }, addEventListener: function(){} }; return global.__els[id]; }, createElement: function () { return { style: {}, dataset: {}, appendChild: function(){}, setAttribute: function(){} }; }, addEventListener: function () {}, }; // Fixture days carry deliberately distinctive costs so a fold over the wrong // subset still produces a number, and only a mismatch shows it. 2026-01-05/06/07 // are Monday/Tuesday/Wednesday; 2025-12-01 is far outside any window here. global.__st = { currency: "USD", total: { cost: 999, requests: 999, prompt_tokens: 999999 }, by_source: { LIFETIMESRC: { cost: 999, requests: 999 } }, by_model: { LIFETIMEMODEL: { cost: 999, requests: 999 } }, by_key: { LIFETIMEKEY: { cost: 999, requests: 999 } }, by_day: { "2026-01-05": { cost: 10, requests: 1, prompt_tokens: 100, completion_tokens: 10, failures: 0 }, "2026-01-06": { cost: 20, requests: 2, prompt_tokens: 200, completion_tokens: 20, failures: 1 }, "2026-01-07": { cost: 30, requests: 3, prompt_tokens: 300, completion_tokens: 30, failures: 0 }, "2025-12-01": { cost: 777, requests: 77, prompt_tokens: 7700 } }, by_day_src: { "2026-01-05": { srcA: { cost: 10, requests: 1 } }, "2026-01-06": { srcA: { cost: 20, requests: 2 }, srcB: { cost: 5, requests: 1 } }, "2026-01-07": { srcA: { cost: 30, requests: 3 } }, "2025-12-01": { srcOLD: { cost: 777, requests: 77 } } }, by_day_model: { "2026-01-05": { mA: { cost: 10, requests: 1 } }, "2026-01-06": { mA: { cost: 20, requests: 2 } }, "2026-01-07": { mA: { cost: 30, requests: 3 } } }, by_day_key: { "2026-01-05": { kA: { cost: 10, requests: 1 } }, "2026-01-06": { kA: { cost: 20, requests: 2 } }, "2026-01-07": { kA: { cost: 30, requests: 3 } } }, degraded_reqs: 4242, unpriced_reqs: 77 }; global.fetch = function () { return Promise.resolve({ ok: true, json: function () { return Promise.resolve({ plugin: "billing", state: global.__st }); }}); }; global.pluginAPI = { onTabShown: function () {}, lang: "en" }; ` script := harness + "\n" + js + "\n" + "__billing_setPeriod(" + jsonStr(period) + ");\n" + // Let the period-triggered refresh() settle before reading the DOM. "setTimeout(function () {\n" + " var el = document.getElementById(" + jsonStr(captureID) + ");\n" + " console.log('__CAPTURE__' + JSON.stringify({\n" + " html: el ? String(el.innerHTML) : '(missing)',\n" + " kpis: String(document.getElementById('billing-kpis').innerHTML),\n" + " sel: String(document.getElementById('billing-period').value),\n" + " errors: global.__errors\n" + " }));\n" + "}, 60);\n" cmd := exec.Command(node, "-e", script) out, err := cmd.CombinedOutput() if err != nil { t.Fatalf("node execution failed: %v\n%s", err, out) } o := string(out) idx := strings.Index(o, "__CAPTURE__") if idx < 0 { t.Fatalf("no capture in node output (the plugin script aborted?):\n%s", o) } raw := strings.TrimSpace(o[idx+len("__CAPTURE__"):]) if nl := strings.IndexAny(raw, "\r\n"); nl >= 0 { raw = raw[:nl] } var got struct { HTML string `json:"html"` KPIs string `json:"kpis"` Sel string `json:"sel"` Errors []string `json:"errors"` } if err := json.Unmarshal([]byte(raw), &got); err != nil { t.Fatalf("capture is not JSON: %v\n%s", err, raw) } if len(got.Errors) > 0 { t.Fatalf("billing script reported errors: %v", got.Errors) } if got.Sel != period { t.Errorf("selector shows %q after setting %q — the change handler did not take", got.Sel, period) } return got.HTML + "\n@@KPI@@\n" + got.KPIs } func jsonStr(s string) string { b, _ := json.Marshal(s) return string(b) } // TestBillingPeriodDayShowsOnlyThatDay checks the one-day window end to end: // the tables must carry that day's rows and drop every other day, and the // lifetime sentinel (999 / LIFETIMESRC) must appear nowhere. func TestBillingPeriodDayShowsOnlyThatDay(t *testing.T) { got := runBillingPage(t, "2026-01-07", "day", "billing-by-source") if strings.Contains(got, "LIFETIME") || strings.Contains(got, "999") { t.Errorf("a one-day view still shows lifetime figures:\n%s", got) } if strings.Contains(got, "srcB") { t.Errorf("a one-day view shows srcB, which only had traffic on 01-06:\n%s", got) } if strings.Contains(got, "srcOLD") { t.Errorf("a one-day view shows a day from 2025-12:\n%s", got) } if !strings.Contains(got, "srcA") { t.Errorf("a one-day view lost the only source that had traffic that day:\n%s", got) } // The KPI total must be that day's cost (30), not the week (60) and not // the lifetime sentinel (999). if !strings.Contains(got, "30") { t.Errorf("KPI does not show the day's cost 30:\n%s", got) } } // TestBillingPeriodWeekSumsThreeDays is the arithmetic: Mon+Wed rows and a // total of 60. A window that started on Sunday or ended early still renders // tables, so only the number distinguishes it. func TestBillingPeriodWeekSumsThreeDays(t *testing.T) { // 2026-01-07 is a Wednesday: the ISO week holds 01-05, 01-06, 01-07. got := runBillingPage(t, "2026-01-07", "week", "billing-by-source") if strings.Contains(got, "srcOLD") || strings.Contains(got, "LIFETIME") { t.Errorf("weekly view leaks out-of-window or lifetime rows:\n%s", got) } // srcA = 10+20+30 = 60, srcB = 5 (only 01-06, but inside the week). if !strings.Contains(got, "60") { t.Errorf("weekly total should be 60, got:\n%s", got) } if !strings.Contains(got, "srcB") { t.Errorf("weekly view dropped srcB, whose only day (01-06) is in the window:\n%s", got) } } // TestBillingPeriodSundayReachesBackToMonday pins the ISO-week Monday rule // from the other side. On Sunday 2026-01-11 the week that already ended is // Mon 05..Sun 11, so all three fixture days are in it; a start-on-Sunday bug // would still include 01-07 and pass, so this case is paired with the // weekday case above rather than relied on alone. func TestBillingPeriodSundayReachesBackToMonday(t *testing.T) { got := runBillingPage(t, "2026-01-11", "week", "billing-by-source") if !strings.Contains(got, "60") { t.Errorf("Sunday's week view should still total 60:\n%s", got) } // And the Monday view must NOT include Sunday's (nonexistent) traffic. gotMon := runBillingPage(t, "2026-01-05", "day", "billing-by-source") if strings.Contains(gotMon, "30") { t.Errorf("Monday's single day view shows Wednesday's cost 30:\n%s", gotMon) } if !strings.Contains(gotMon, "10") { t.Errorf("Monday's day view lost Monday's own cost 10:\n%s", gotMon) } } // TestBillingPeriodKpiTotalMatchesWindowDays pins the KPI total itself. // // The dimension-table tests above cannot catch a broken window or a broken // fold: by_source is computed from the per-day DIMENSION map, independently of // the total. Mutation verification proved it — disabling the total fold, and // shifting the ISO-week start to Sunday, both left every earlier test green. // The total is derived from by_day, so it is the one value that moves when // either the window membership or the accumulation is wrong. func TestBillingPeriodKpiTotalMatchesWindowDays(t *testing.T) { kpis := func(t *testing.T, today, period string) string { t.Helper() return runBillingPage(t, today, period, "billing-by-source") } // 2026-01-07 is a Wednesday; fixture days are 05(10) 06(20) 07(30). // KPI renders through fmtInt, so 30 -> "30", 60 -> "60". if got := kpis(t, "2026-01-07", "day"); !strings.Contains(got, "30") { t.Errorf("day KPI should show that day's cost 30:\n%s", got) } if got := kpis(t, "2026-01-07", "week"); !strings.Contains(got, "60") { t.Errorf("week KPI should total 10+20+30 = 60:\n%s", got) } // Sunday 2026-01-11: the ISO week Mon 05..Sun 11 still holds all three days. if got := kpis(t, "2026-01-11", "week"); !strings.Contains(got, "60") { t.Errorf("Sunday's week KPI should still total 60:\n%s", got) } // The Monday rule from the negative side. A window that started on SUNDAY // would cover Sun 11..Sat 17 and contain none of the fixture days, giving // a total of 0 — so this case is what makes the +6 shift observable. The // positive case alone cannot distinguish it: shifting the start one day // later still happens to contain 01-07. if got := kpis(t, "2026-01-11", "day"); !strings.Contains(got, "0") { t.Errorf("Sunday's single-day window has no fixture traffic and must total 0:\n%s", got) } // Monday 2026-01-12 begins a new week; it contains no fixture days. if got := kpis(t, "2026-01-12", "week"); !strings.Contains(got, "0") { t.Errorf("an empty week should report 0, not stale data:\n%s", got) } // ...whereas the Monday-to-Sunday week that just ended does. if got := kpis(t, "2026-01-12", "day"); !strings.Contains(got, "0") { t.Errorf("Monday the 12th has no fixture traffic and must total 0:\n%s", got) } } // TestBillingPeriodDimensionFoldSumsAcrossDays covers the per-dimension // accumulation. Asserting "60 appears somewhere" was NOT enough: mutation // verification showed a fold that keeps only the last day still passed, // because 60 also appears in the KPI total and in a token column. The // assertion must therefore pin srcA's own cost cell, not the page's text. func TestBillingPeriodDimensionFoldSumsAcrossDays(t *testing.T) { got := runBillingPage(t, "2026-01-07", "week", "billing-by-source") // srcA exists on all three days at 10/20/30. Summed = 60; last-only = 30. if !strings.Contains(got, "USD 60.0000") { t.Errorf("weekly by-source did not sum srcA across days (want USD 60.0000, "+ "last-day-only would be USD 30.0000):\n%s", got) } // srcB exists only on 01-06 at 5, inside the window, so it must survive at // exactly its own value. if !strings.Contains(got, "USD 5.0000") { t.Errorf("weekly by-source lost or altered srcB (want USD 5.0000):\n%s", got) } // srcA's summed cost must also differ from its last-day value in the // rendered table, so an overwrite cannot masquerade as a sum. if strings.Count(got, "USD 60.0000") < 2 { t.Errorf("expected the summed 60 in both the table and the KPI:\n%s", got) } } // state untouched, including the lifetime sentinel and the lifetime-only // degraded counter. Without this control, a rescale that silently degraded // into "always empty" would pass the narrow-window tests. // TestBillingPeriodAllStaysLifetime is the control: "all" must keep the stored // state untouched. Without it, a rescale that silently degraded into "always // empty" would pass every narrow-window test above. func TestBillingPeriodAllStaysLifetime(t *testing.T) { got := runBillingPage(t, "2026-01-07", "all", "billing-by-source") if !strings.Contains(got, "LIFETIMESRC") && !strings.Contains(got, "999") { t.Errorf("the all-time view dropped the lifetime figures:\n%s", got) } // 4242 is the lifetime degraded_reqs; it has no day-keyed tally, so it // must remain visible when the window is "all". The KPI goes through // fmtInt, so it renders grouped ("4,242") — assert the rendered form. if !strings.Contains(got, "4,242") { t.Errorf("the all-time view should still show the lifetime degraded count:\n%s", got) } } // TestBillingPeriodHidesLifetimeOnlyCounters is the mismatch the period view // exists to remove: degraded/unpriced have no day-keyed tally, so printing // the lifetime number next to a per-window cost is misleading. They must be // hidden, not merely relabelled. func TestBillingPeriodHidesLifetimeOnlyCounters(t *testing.T) { got := runBillingPage(t, "2026-01-07", "day", "billing-by-source") if strings.Contains(got, "4,242") || strings.Contains(got, "Degraded") { t.Errorf("a one-day view still shows the lifetime degraded counter:\n%s", got) } // ...while "all" does show it (covered above), so this is not just a // dictionary that lost the words. if !strings.Contains(runBillingPage(t, "2026-01-07", "all", "billing-by-source"), "4,242") { t.Error("the all-time view lost the degraded counter — the hide is not period-scoped") } }