125 lines
5.7 KiB
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)
|
|
}
|
|
}
|