textmachine/platform/internal/pricing/pricing_test.go

356 lines
17 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package pricing
import (
"testing"
"textmachine/platform/internal/money"
)
func model(t *testing.T) Model {
t.Helper()
m, err := New(DefaultHoldFactorPercent)
if err != nil {
t.Fatal(err)
}
return m
}
// book is a priced book of `n` equal chapters, one unit each.
func book(n int, perChapter, stepMax, bookOnce money.MicroUSD) Book {
b := Book{StepMax: stepMax, BookOnce: bookOnce}
for i := 1; i <= n; i++ {
b.Remaining = append(b.Remaining, Chapter{Number: i, Units: 1, Expected: perChapter, SourceChars: 1000})
}
return b
}
// A cushion below the engine's own expected bill is a discount, not a cushion: every purchase under
// it would latch before it delivered what it quoted. Refused at construction so an operator learns
// it at boot rather than from a user's paused run.
func TestAHoldFactorBelowTheExpectedBillIsRefused(t *testing.T) {
for _, v := range []int{99, 0, -1} {
if _, err := New(v); err == nil {
t.Errorf("a hold factor of %d%% was accepted", v)
}
}
if _, err := New(100); err != nil {
t.Errorf("exactly the expected bill is a legal (if tight) cushion: %v", err)
}
}
// ⛔ THE REGRESSION OF PD-440, AND IT IS THE REASON THIS PACKAGE WAS REWRITTEN.
//
// Under the per-chapter constant a run's new headroom was `chaptersLeft × $0.03`, so the last two
// chapters of ANY book were bought with $0.06 against a single editor reservation of about $0.07:
// the run was admitted, died on its first reservation, moved nothing, and did so at ANY balance.
// The measured floor was the same by two independent readings (9.25 and 9.31 chapters), so a book
// was translatable only from ten undelivered chapters up.
//
// The cure is not a bigger constant — the old headroom SHRINKS with the book while the cost of one
// indivisible call does not — it is that `step_max` is ADDED rather than compared against.
func TestTheLastTwoChaptersOfABookAreBuyable(t *testing.T) {
m := model(t)
// The measured shapes of 04.09: a chapter's expected bill around five cents, one editor
// reservation $0.069828 (`denied estimate` in the engine's own refusal), the book-level bound $2.
const chapter, stepMax, bookOnce = money.MicroUSD(50_000), money.MicroUSD(69_828), money.MicroUSD(2_000_000)
tail := book(2, chapter, stepMax, bookOnce)
q := m.Quote(tail, money.MicroUSD(500_000), 0)
if q.Hold <= stepMax {
t.Fatalf("the last two chapters reserve %s, which does not clear ONE reservation of %s — "+
"the run would be admitted and die having moved nothing (PD-440)", q.Hold.USD(), stepMax.USD())
}
// And the old arithmetic, stated here so the regression is legible rather than implied.
if old := money.MicroUSD(2 * 30_000); old >= stepMax {
t.Fatalf("the fixture no longer reproduces the defect: 2 chapters at the old $0.03 rate is %s, "+
"which already clears a reservation of %s", old.USD(), stepMax.USD())
}
// One chapter left is the hardest case of all and it must still be buyable.
if one := m.Quote(book(1, chapter, stepMax, bookOnce), money.MicroUSD(500_000), 0); one.Hold <= stepMax {
t.Errorf("the last chapter reserves %s against a reservation of %s", one.Hold.USD(), stepMax.USD())
}
}
// ⛔ THE OTHER HALF OF THE SAME WALL, and it is the one that would have been REINTRODUCED by the
// obvious reading of the engine's projection.
//
// The book-level figure `expected_usd` INCLUDES `book_once_usd`, and that term is a BOUND on two
// gates — $2.00 flat on the shipped arm, for a book of three chapters and one of three hundred
// alike. Priced from it, a five-chapter book expected to cost a quarter of a dollar would demand
// two dollars and change: unbuyable to anyone holding less, at any length of book. So the base of
// the hold is the ORDER's own chapters, and the bound is added only when the balance carries it.
func TestAFlatBookLevelBoundDoesNotPutATwoDollarThresholdUnderEveryPurchase(t *testing.T) {
m := model(t)
const chapter, stepMax, bookOnce = money.MicroUSD(50_000), money.MicroUSD(69_828), money.MicroUSD(2_000_000)
b := book(5, chapter, stepMax, bookOnce)
// A buyer holding one dollar — less than the book-level bound, more than the book costs.
const balance = money.MicroUSD(1_000_000)
opts := m.Order(b, balance)
if opts.Verdict != VerdictCoversAll {
t.Fatalf("a $1 balance over a book expected to cost %s answered %q; the flat $%s bound is "+
"back under the purchase", (chapter * 5).USD(), opts.Verdict, bookOnce.USD())
}
if opts.AffordableChapters != 5 {
t.Errorf("affordable %d of 5 chapters at a balance of %s", opts.AffordableChapters, balance.USD())
}
// …and the buyer is TOLD what they did not get. A silent degradation of the book-wide
// consistency pass is the failure this member exists to make visible (D39.198, D39.202 §3).
if opts.Whole.BondFunded {
t.Error("a balance that cannot carry the book-level pass reported it as funded")
}
}
// The bond is funded whenever the balance carries it on top of the order, because the pass it pays
// for is the mechanism behind consistency of terms across a book — the owner's first priority. What
// is refused is only the version of it that makes short books unbuyable.
func TestTheBookLevelBoundIsFundedWheneverTheBalanceCarriesIt(t *testing.T) {
m := model(t)
const chapter, stepMax, bookOnce = money.MicroUSD(50_000), money.MicroUSD(69_828), money.MicroUSD(2_000_000)
b := book(5, chapter, stepMax, bookOnce)
rich := m.Quote(b, money.MicroUSD(25_000_000), 0)
if !rich.BondFunded {
t.Fatal("a balance of $25 did not fund a $2 book-level pass")
}
poor := m.Quote(b, money.MicroUSD(1_000_000), 0)
if rich.Hold-poor.Hold != bookOnce {
t.Errorf("the funded hold exceeds the unfunded one by %s, want exactly the bound %s",
(rich.Hold - poor.Hold).USD(), bookOnce.USD())
}
// A deployment that runs no book-level pass has nothing to warn about, and «true» is the honest
// answer there: no consistency work is going unpaid.
if q := m.Quote(book(5, chapter, stepMax, 0), money.MicroUSD(1000), 0); !q.BondFunded {
t.Error("a deployment with no book-level pass reported an unfunded one")
}
}
// The hold is `k × expected + step_max`, and the `+` is load-bearing rather than cosmetic: the
// engine compares its ceiling against the book's CUMULATIVE spend on every reservation, so the LAST
// call of an order needs a whole reservation of room ON TOP of everything the order has already
// been billed. Under `max(k × expected, step_max)` any order whose bill exceeds one reservation
// ends one call short of what was bought.
func TestTheHoldIsTheCushionedBillPlusOneWholeReservation(t *testing.T) {
m := model(t)
const expected, stepMax = money.MicroUSD(1_000_000), money.MicroUSD(70_000)
hold, _ := m.Hold(expected, stepMax, 0, money.MicroUSD(0))
want := expected*money.MicroUSD(DefaultHoldFactorPercent)/100 + stepMax
if hold != want {
t.Fatalf("hold %s, want %s", hold.USD(), want.USD())
}
if hold <= expected+stepMax {
t.Errorf("the hold %s leaves no cushion over the expected bill %s plus a reservation %s",
hold.USD(), expected.USD(), stepMax.USD())
}
}
// Affordability WALKS the chapters. There is no average to divide by — that assumption is what the
// per-chapter constant was — and a book whose chapters differ must be judged chapter by chapter.
func TestAffordabilityWalksTheChaptersRatherThanDividingByAnAverage(t *testing.T) {
m := model(t)
b := Book{StepMax: 70_000, Remaining: []Chapter{
{Number: 1, Units: 1, Expected: 10_000},
{Number: 2, Units: 1, Expected: 10_000},
{Number: 3, Units: 1, Expected: 5_000_000}, // one enormous chapter
}}
// Enough for the two small ones and their reservation, nowhere near the third.
got := m.Affordable(b, money.MicroUSD(200_000))
if got != 2 {
t.Fatalf("affordable %d of 3, want 2 — the third chapter alone is %s", got, b.Remaining[2].Expected.USD())
}
// An average would have said otherwise, and stating it here is what keeps the test about the
// method rather than about the numbers.
avg := (10_000 + 10_000 + 5_000_000) / 3
if byAverage := int(200_000 / avg); byAverage == got {
t.Errorf("the fixture does not separate the two methods: an average of %d gives the same answer", avg)
}
}
// The three verdicts, and the one that has to be told apart from an empty book: a book with nothing
// left carries `covers_none` AND `chapters_left: 0`, which is what makes the two distinguishable on
// the wire without a fourth word.
func TestTheVerdictAnswersTheBuyersQuestionAndNamesTheEmptyBookByItsCount(t *testing.T) {
m := model(t)
const chapter, stepMax = money.MicroUSD(50_000), money.MicroUSD(69_828)
b := book(10, chapter, stepMax, 0)
for _, tc := range []struct {
name string
balance money.MicroUSD
want string
}{
{"the whole book fits", 25_000_000, VerdictCoversAll},
{"part of it fits", 400_000, VerdictCoversPart},
{"not even one chapter", 1_000, VerdictCoversNone},
} {
if got := m.Order(b, tc.balance).Verdict; got != tc.want {
t.Errorf("%s: verdict %q, want %q", tc.name, got, tc.want)
}
}
empty := m.Order(Book{StepMax: stepMax}, money.MicroUSD(25_000_000))
if empty.Verdict != VerdictCoversNone || empty.ChaptersLeft != 0 {
t.Errorf("a fully translated book: %q with %d chapters left", empty.Verdict, empty.ChaptersLeft)
}
}
// A partial order names the chapter it stops at; the whole remainder deliberately does not. The
// asymmetry is what keeps «the whole book» meaning the whole book after a re-cut moves the numbers.
func TestOnlyAPartialOrderFreezesAChapterNumber(t *testing.T) {
m := model(t)
b := book(10, 50_000, 69_828, 0)
if q := m.Quote(b, money.MicroUSD(25_000_000), 4); q.ThroughChapter != 4 || q.Chapters != 4 {
t.Errorf("a four-chapter order: through %d, chapters %d", q.ThroughChapter, q.Chapters)
}
for _, n := range []int{0, 10, 99} {
if q := m.Quote(b, money.MicroUSD(25_000_000), n); q.ThroughChapter != 0 || q.Chapters != 10 {
t.Errorf("an order of %d over ten chapters: through %d, chapters %d — the whole remainder "+
"must not freeze a chapter number", n, q.ThroughChapter, q.Chapters)
}
}
}
// The character order resolves to a prefix of UNITS and rounds UP: a buyer who asks for ten thousand
// characters gets the unit that CONTAINS the ten-thousandth, never one short of it. Characters are
// what a person expresses; units are what the engine ships and stops at.
func TestACharacterOrderRoundsUpToAWholeUnitAndCannotOverrunTheBook(t *testing.T) {
units := []Unit{{ID: "u1", SourceChars: 100}, {ID: "u2", SourceChars: 100}, {ID: "u3", SourceChars: 100}}
for _, tc := range []struct {
want int64
n int
ok bool
}{{1, 1, true}, {100, 1, true}, {101, 2, true}, {200, 2, true}, {201, 3, true},
{10_000, 3, true}, {0, 0, false}, {-5, 0, false}} {
got, ok := UnitsFor(units, tc.want)
if got != tc.n || ok != tc.ok {
t.Errorf("%d characters of a 300-character book buys %d units (ok %v), want %d (ok %v)",
tc.want, got, ok, tc.n, tc.ok)
}
}
if got, ok := UnitsFor(nil, 500); got != 0 || ok {
t.Errorf("a book with nothing left buys %d units (ok %v)", got, ok)
}
// ⚠ A UNIT OF UNKNOWN SIZE STOPS THE ANSWER rather than being skipped. Zero sizes make every
// prefix fall short, so a loop that ran to the end would answer «the whole book» — a partial
// order silently becoming a total one, which is the shape this whole pack exists to prevent.
blind := []Unit{{ID: "u1", SourceChars: 100}, {ID: "u2"}, {ID: "u3", SourceChars: 100}}
if got, ok := UnitsFor(blind, 5000); ok {
t.Errorf("a book whose unit sizes did not arrive sold %d units as if they had", got)
}
}
// ⛔ A CHARACTER ORDER IS PRICED FROM ITS UNITS, not from the chapters they fall in — and on a book
// with no chapter structure those are the same chapter, so the difference is the whole order.
//
// Measured on the shape that made it visible: a `none` book is ONE chapter of five units, so quoting
// a one-unit order at the chapter's price quotes the WHOLE BOOK. The buyer holding enough for one
// unit was refused, and the buyer holding more had five times too much frozen.
func TestACharacterOrderIsPricedFromItsUnitsAndNotFromTheChapterTheyFallIn(t *testing.T) {
m := model(t)
const chapter, stepMax = money.MicroUSD(150_000), money.MicroUSD(69_828) // one chapter, five units
b := Book{StepMax: stepMax, Remaining: []Chapter{
{ID: "c1", Number: 1, Units: 5, Expected: chapter, SourceChars: 5000},
}}
units := make([]Unit, 5)
for i := range units {
units[i] = Unit{ID: "u" + string(rune('1'+i)), Expected: 30_000, SourceChars: 1000}
}
one := m.QuoteUnits(b, money.MicroUSD(10_000_000), units, 1, 1)
if one.Expected != 30_000 {
t.Fatalf("one unit is quoted at %s, want the unit's own %s — the chapter's price is %s",
one.Expected.USD(), money.MicroUSD(30_000).USD(), chapter.USD())
}
whole := m.Quote(b, money.MicroUSD(10_000_000), 0)
if one.Hold >= whole.Hold {
t.Errorf("a one-unit order reserves %s and the whole book %s: the order does not exist as a "+
"purchase", one.Hold.USD(), whole.Hold.USD())
}
// And the buyer who can afford exactly one unit is not refused it.
if afford := one.Hold; afford > money.MicroUSD(120_000) {
t.Errorf("one unit of a $0.03 book reserves %s, which no buyer of one unit holds", afford.USD())
}
}
// A book with nothing left to buy quotes ZERO, not «one reservation plus a flat book-level bound».
// The pair «expected $0.00, reserve $2.07» is arithmetic reaching a screen as an offer for a book
// that is finished.
func TestAFinishedBookQuotesNothingRatherThanTheBookLevelBound(t *testing.T) {
m := model(t)
got := m.Order(Book{StepMax: 69_828, BookOnce: 2_000_000}, money.MicroUSD(25_000_000))
if got.Whole.Expected != 0 || got.Whole.Hold != 0 {
t.Errorf("a finished book is quoted %s expected / %s held",
got.Whole.Expected.USD(), got.Whole.Hold.USD())
}
if !got.Whole.BondFunded {
t.Error("a finished book reports an unfunded consistency pass it will never run")
}
}
// An absurd projection — one no book produces, but one a document that is not a manifest could — must
// not wrap the int64 into a hold SMALLER than the bill it covers. Not a wrap into the negative, which
// the ledger would refuse: a plausible, small, wrong number.
func TestAnAbsurdProjectionCannotWrapTheHoldBelowTheBill(t *testing.T) {
m := model(t)
for _, expected := range []money.MicroUSD{maxExpected, maxExpected * 4, 1 << 62} {
hold, _ := m.Hold(expected, 69_828, 0, 0)
if hold <= 0 {
t.Errorf("expected %d gave a hold of %d", expected, hold)
}
if capped := maxExpected*money.MicroUSD(DefaultHoldFactorPercent)/100 + 69_828; hold != capped {
t.Errorf("expected %d gave a hold of %d, want the bounded %d", expected, hold, capped)
}
}
}
// The minimum of the money slider is ONE indivisible reservation, so the slider cannot express an
// order under which no run could move at all (unified backlog row 276).
func TestTheMoneySlidersMinimumIsOneIndivisibleReservation(t *testing.T) {
m := model(t)
const stepMax = money.MicroUSD(69_828)
got := m.Order(book(10, 50_000, stepMax, 0), money.MicroUSD(25_000_000))
if got.MinHold != stepMax {
t.Errorf("the slider's minimum is %s, want the engine's own step %s", got.MinHold.USD(), stepMax.USD())
}
}
// ⚠ THE CUSHION IS DERIVED, NOT MEASURED, and this test pins the band its derivation gives rather
// than the value — so that the measurement of unified backlog row 281, when it lands, REPLACES the
// number instead of being argued against it.
//
// Above 100%: `expected_usd` excludes retries, escalation, repair and additive reasoning by
// construction, so a factor of exactly one latches on the first regeneration of every purchase.
// Below 162%: the only live figures there are — two chapters of one paid run, $0.093 and $0.151 —
// bound the guess at a ratio of 1.62, and that is TWO POINTS rather than a distribution. The
// direction of the error is ratified (D39.206): a short hold costs one top-up, a long one freezes
// credit, so err low.
func TestTheHoldFactorStaysInTheBandItsDerivationGives(t *testing.T) {
if DefaultHoldFactorPercent <= 100 || DefaultHoldFactorPercent >= 162 {
t.Errorf("the hold factor is %d%%, outside the band its derivation justifies (100..162, exclusive)",
DefaultHoldFactorPercent)
}
}
// The running sum in Affordable and the prefix sum in Quote must answer the SAME thing, because the
// first is the second made linear and nothing else. Two arithmetics that must agree are exactly
// where a silent drift lives, so they are compared over a book whose chapters differ.
func TestAffordabilityAgreesWithQuotingEveryPrefix(t *testing.T) {
m := model(t)
b := Book{StepMax: 69_828, BookOnce: 2_000_000}
for i := 1; i <= 40; i++ {
// Deliberately uneven, and one chapter far larger than its neighbours: an average would sail
// past it and a running sum must not.
expected := money.MicroUSD(10_000 + (i*7919)%90_000)
if i == 17 {
expected = 3_000_000
}
b.Remaining = append(b.Remaining, Chapter{ID: "c", Number: i, Units: 1, Expected: expected})
}
for _, balance := range []money.MicroUSD{0, 1, 69_828, 200_000, 1_000_000, 3_000_000, 25_000_000, 1 << 40} {
want := 0
for i := range b.Remaining {
if m.Quote(b, balance, i+1).Hold > balance {
break
}
want = i + 1
}
if got := m.Affordable(b, balance); got != want {
t.Errorf("balance %s: the running sum says %d chapters and quoting every prefix says %d",
balance.USD(), got, want)
}
}
}