356 lines
17 KiB
Go
356 lines
17 KiB
Go
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)
|
||
}
|
||
}
|
||
}
|