textmachine/backend/cmd/tmctl/pricebasis_report_test.go

125 lines
5.7 KiB
Go

package main
import (
"strings"
"testing"
"textmachine/backend/internal/ledger"
"textmachine/backend/internal/store"
)
// TestTheReportNamesRowsBilledByAModelThatDidNotAnswer is the retroactive half of making the price
// substitution visible. The live path says it once, in a WARN, at the moment it happens; a run already
// on disk has no such line, and the rows are the only thing left. Because the legend is DERIVED from
// the two model columns and today's catalogue, those runs answer too — which is the point: the
// measured case is 28 rows of a finished book, not a run somebody is watching.
func TestTheReportNamesRowsBilledByAModelThatDidNotAnswer(t *testing.T) {
zero, eightK := 0, 8496
rows := []store.RequestLogView{
{TS: "t1", Stage: "draft", Role: "translator", ModelRequested: "deepseek-v4-flash", ModelActual: "deepseek-flash", CostUSD: 0.012044, OK: 1, ReasoningInCompletion: &eightK},
{TS: "t2", Stage: "draft", Role: "translator", ModelRequested: "deepseek-v4-flash", ModelActual: "deepseek-flash", CostUSD: 0.011637, OK: 1, ReasoningInCompletion: &zero},
{TS: "t3", Stage: "edit", Role: "editor", ModelRequested: "deepseek-v4-pro", ModelActual: "deepseek-v4-pro", CostUSD: 0.071009, OK: 1},
// A replayed row bought nothing this run and must not be counted as a bill.
{TS: "t4", Stage: "draft", Role: "translator", ModelRequested: "deepseek-v4-flash", ModelActual: "deepseek-flash", CostUSD: 0, TMHit: 1, OK: 1},
}
basis := func(requested, actual string) ledger.PriceBasis {
if actual == "deepseek-flash" {
return ledger.PriceByRequested
}
return ledger.PriceByAnswerer
}
var b strings.Builder
if err := renderReport(&b,
func() ([]store.RequestLogView, error) { return rows, nil },
func() ([]store.ChunkStatus, error) { return nil, nil },
func() ([]store.RetrievalState, error) { return nil, nil },
basis, okLedger); err != nil {
t.Fatal(err)
}
out := b.String()
if !strings.Contains(out, "PRICED BY A MODEL THAT DID NOT ANSWER") {
t.Fatalf("a run billed at the rate of a model that did not answer must say so:\n%s", out)
}
if !strings.Contains(out, "deepseek-v4-flash -> deepseek-flash") {
t.Fatalf("the legend must name BOTH slugs — the fix is to add the answering one to the catalogue:\n%s", out)
}
// Two paying rows, and the replayed one excluded: $0.012044 + $0.011637.
if !strings.Contains(out, "2 row(s)") || !strings.Contains(out, "$0.023681") {
t.Fatalf("the legend must count only the rows that were BILLED, and total them:\n%s", out)
}
// The row priced by the model that answered must not be swept in.
if strings.Contains(out, "deepseek-v4-pro -> deepseek-v4-pro") {
t.Fatalf("a call priced by its own answerer is not a substitution:\n%s", out)
}
// The thinking column has to REACH the reader, and it has to reach them saying three different
// things. Row t1 reports 8496 thinking tokens, t2 a MEASURED zero, t3 nothing at all — and the
// third must print `?`, because a run whose provider says nothing and a call that did not think
// are the two answers this whole column exists to keep apart.
//
// ⚠ READ BY COLUMN NAME, NEVER BY SUBSTRING. Every other numeric cell in this fixture is zero, so
// a search for a padded "0" anywhere in the output is satisfied with the think column DELETED —
// the assertion would then be green on a report that lost the very thing it is asserting.
for _, c := range []struct{ ts, want string }{
{"t1", "8496"}, // a reported count
{"t2", "0"}, // a MEASURED zero: the provider answered, and the answer was none
{"t3", "?"}, // no answer at all, which must not be spelled like the line above
} {
if got := cellOf(t, out, c.ts, "think"); got != c.want {
t.Fatalf("row %s: think column = %q, want %q — measured, measured-zero and unanswered are three different cells:\n%s", c.ts, got, c.want, out)
}
}
}
// cellOf reads one cell of the report table by its COLUMN NAME, from the row whose first field is ts.
// The header is the index, so an assertion cannot be satisfied by an identical-looking value in some
// other column — and a column that disappears takes its assertions down with it instead of silently
// passing them to a neighbour.
func cellOf(t *testing.T, out, ts, column string) string {
t.Helper()
var header []string
for _, line := range strings.Split(out, "\n") {
f := strings.Fields(line)
if len(f) == 0 {
continue
}
if f[0] == "ts" {
header = f
continue
}
if f[0] != ts {
continue
}
if header == nil {
t.Fatalf("row %q appears before any header row:\n%s", ts, out)
}
for i, name := range header {
if name == column && i < len(f) {
return f[i]
}
}
t.Fatalf("the report has no %q column (header: %v):\n%s", column, header, out)
}
t.Fatalf("the report has no row %q:\n%s", ts, out)
return ""
}
// TestTheLegendIsSilentWhenEveryBillNamesItsAnswerer is the control without which the test above proves
// nothing: a section that printed on every run would be noise, and the day the missing slug is added to
// models.yaml these lines must disappear on their own.
func TestTheLegendIsSilentWhenEveryBillNamesItsAnswerer(t *testing.T) {
rows := []store.RequestLogView{
{TS: "t1", Stage: "draft", Role: "translator", ModelRequested: "deepseek-v4-flash", ModelActual: "deepseek-v4-flash", CostUSD: 0.012044, OK: 1},
}
var b strings.Builder
if err := renderReport(&b,
func() ([]store.RequestLogView, error) { return rows, nil },
func() ([]store.ChunkStatus, error) { return nil, nil },
func() ([]store.RetrievalState, error) { return nil, nil },
pricedByAnswerer, okLedger); err != nil {
t.Fatal(err)
}
if out := b.String(); strings.Contains(out, "PRICED BY A MODEL THAT DID NOT ANSWER") {
t.Fatalf("with every bill named by its answerer the section must not print at all:\n%s", out)
}
}