; per-column widths on " +
"| or | are advisory and get recomputed from content")
}
// The colgroup is built from a JS array of pixel widths, so assert on the
// array, not on literal ""`) {
t.Error("colgroup entries are no longer emitted as ")
}
// Every price-row INPUT must be allowed to shrink, or the row's fixed widths
// sum past the cell and the × button lands on the next column. The × button
// is the opposite: it must NOT shrink (flex:0 0 auto), because a squashed
// delete button is unclickable.
for _, needle := range []string{"m-name", "m-p", "m-c"} {
i := strings.Index(js, "class='"+needle+"'")
if i < 0 {
i = strings.Index(js, "class='ghost small "+needle+"'")
}
if i < 0 {
t.Errorf("price row lost the %s input/button", needle)
continue
}
window := js[i : i+220]
if !strings.Contains(window, "flex:") {
t.Errorf("%s has no flex sizing; the row is 80+96+96+38 ≈ 330px wide "+
"against a 262px cell and overflows onto the next column", needle)
}
if !strings.Contains(window, "min-width:0") {
t.Errorf("%s lacks min-width:0; a flex item will not shrink below its "+
"content width, which is how the × button ended up 78px past its cell", needle)
}
}
i := strings.Index(js, "class='ghost small m-del'")
if i < 0 {
t.Fatal("the per-model delete button is gone")
}
if !strings.Contains(js[i:i+120], "flex:0 0 auto") {
t.Error("the per-model × button must be flex:0 0 auto — it is a fixed-size " +
"control and must never be squeezed by the flexible inputs")
}
}
// TestBillingUIShowsRecentUnpriced: the page must be able to distinguish a
// settled pricing debt from a live leak. The lifetime unpriced counter answers
// neither — it keeps every request from before a rule was added, so it reads the
// same number in both situations. The day buckets exist to make the difference
// visible, and a bucket nothing renders is a bucket nobody benefits from.
func TestBillingUIShowsRecentUnpriced(t *testing.T) {
raw, err := os.ReadFile("plugins/billing.lua")
if err != nil {
t.Fatal(err)
}
src := string(raw)
for _, needle := range []string{
"unpriced_by_day", // the tally is written
"recentUnpriced", // it is reduced to a window
"unpriced_recent", // it reaches the view
"unpriced7d", // the card labels it
"unpricedClean", // and says so when the window is clear
} {
if !strings.Contains(src, needle) {
t.Errorf("billing UI lacks %q", needle)
}
}
}
// TestBillingRulesUIExposesReasoningPrice: the thinking-token rate must be
// editable in the rules editor, not only settable by hand-editing config.yaml.
// A pricing capability that lives only in the config file is one the operator
// will assume does not exist.
func TestBillingRulesUIExposesReasoningPrice(t *testing.T) {
raw, err := os.ReadFile("plugins/billing.lua")
if err != nil {
t.Fatal(err)
}
src := string(raw)
for _, needle := range []string{
`class='m-r'`, // the input, in both the existing rows and the "+ model" row
`models[n].reasoning`, // collected back out of the DOM on save
`reasoningHint`, // and it says what leaving it blank means
} {
if !strings.Contains(src, needle) {
t.Errorf("rules editor lacks %q", needle)
}
}
// Two occurrences: an existing model's row and the template for a new one.
// One would mean the "+ model" button produces a row that silently drops
// the rate on the next save.
if n := strings.Count(src, `class='m-r'`); n < 2 {
t.Errorf("m-r appears %d time(s); both the row template and the editor need it", n)
}
}
|