textmachine/platform/internal/ingest/price_test.go

173 lines
7.3 KiB
Go

package ingest
import (
"encoding/json"
"testing"
"textmachine/platform/internal/money"
)
// pricedDoc is a two-chapter manifest carrying a whole projection, as the engine emits one. Written
// as JSON and decoded rather than built as a struct, because half of what these tests are about is
// the DECODING — a key the engine renames leaves a zero behind, and a struct literal cannot express
// that.
func pricedDoc(t *testing.T, edit func(map[string]any)) Manifest {
t.Helper()
const doc = `{
"manifest_version": "tm-manifest-v2",
"key": "k1", "chunker_version": "cv1", "structure": "detected",
"chapters_total": 2, "units_total": 2,
"price": {"expected_usd": 2.0062, "book_once_usd": 2, "step_max_usd": 0.129051, "source_chars": 48},
"chapters": [
{"id": "c1", "number": 1, "units_total": 1,
"price": {"source_chars": 24, "expected_usd": 0.0031},
"units": [{"id": "u1", "first_chunk_idx": 0, "price": {"source_chars": 24, "expected_usd": 0.0031}}]},
{"id": "c2", "number": 2, "units_total": 1,
"price": {"source_chars": 24, "expected_usd": 0.0031},
"units": [{"id": "u2", "first_chunk_idx": 0, "price": {"source_chars": 24, "expected_usd": 0.0031}}]}
]
}`
var raw map[string]any
if err := json.Unmarshal([]byte(doc), &raw); err != nil {
t.Fatal(err)
}
if edit != nil {
edit(raw)
}
b, err := json.Marshal(raw)
if err != nil {
t.Fatal(err)
}
m, err := DecodeManifest(b)
if err != nil {
t.Fatal(err)
}
return m
}
// The projection arrives whole, and it arrives as MONEY: whole micro-USD converted at the seam,
// rounding UP, never a float64 carried inward. A JSON decimal bound to a float puts drift one step
// before the integer column that exists to prevent drift (PD-15).
func TestThePriceProjectionIsReadAsWholeMicroDollars(t *testing.T) {
m := pricedDoc(t, nil)
p, ok := m.Priced()
if !ok {
t.Fatal("a whole projection was not read")
}
// 0.129051 → 129051 exactly; 2.0062 → 2006200; the per-chapter 0.0031 → 3100.
if p.StepMaxUSD != 129_051 || p.ExpectedUSD != 2_006_200 || p.BookOnceUSD != 2_000_000 {
t.Errorf("the book price came out as %+v", p)
}
if p.SourceChars != 48 {
t.Errorf("source chars %d", p.SourceChars)
}
if len(m.Chapters) != 2 || m.Chapters[0].Price.ExpectedUSD != 3100 || m.Chapters[1].Price.ExpectedUSD != 3100 {
t.Errorf("the per-chapter bills came out as %+v", m.Chapters)
}
// Rounding is UP, and the direction is the engine's own rule for money on this seam: a price
// rounded down under-quotes the account by construction, every time, the same way.
up := pricedDoc(t, func(raw map[string]any) {
raw["price"].(map[string]any)["step_max_usd"] = 0.0000001
})
if p, _ := up.Priced(); p.StepMaxUSD != 1 {
t.Errorf("a tenth of a micro-dollar rounded to %d, want 1", p.StepMaxUSD)
}
}
// ⛔ HALF-READ IS NOT ABSENT, AND ABSENT IS NOT ZERO. `json.Unmarshal` leaves a key the engine
// renamed at its zero value, so a projection whose `step_max_usd` decoded as zero is the exact shape
// of the PD-440 wall coming back in silence: the hold loses its floor, the run is admitted with a
// ceiling no single call can clear, and it dies at once having spent nothing and moved nothing.
//
// Every one of these documents is internally VALID — the tree passes Whole() — which is what makes
// the check worth having: nothing else in this package would notice.
//
// Mutation caught: any of the three guards in Priced turned into a truth.
func TestAHalfReadProjectionIsNotAFreeBook(t *testing.T) {
for name, edit := range map[string]func(map[string]any){
"the engine renamed step_max_usd": func(raw map[string]any) {
p := raw["price"].(map[string]any)
delete(p, "step_max_usd")
},
"the engine renamed expected_usd": func(raw map[string]any) {
p := raw["price"].(map[string]any)
delete(p, "expected_usd")
},
"the engine renamed the per-chapter price": func(raw map[string]any) {
for _, c := range raw["chapters"].([]any) {
delete(c.(map[string]any), "price")
}
},
"one chapter lost its price": func(raw map[string]any) {
delete(raw["chapters"].([]any)[1].(map[string]any), "price")
},
"the chapters and the book disagree": func(raw map[string]any) {
raw["chapters"].([]any)[0].(map[string]any)["price"].(map[string]any)["expected_usd"] = 9.0
},
} {
t.Run(name, func(t *testing.T) {
if p, ok := pricedDoc(t, edit).Priced(); ok {
t.Errorf("a half-read projection was believed: %+v", p)
}
})
}
// A book the engine could not price AT ALL is legal and says so — `price` is `omitempty` on the
// engine's side. It is the same answer as a half-read one, which is the point: the caller refuses
// the sale either way and never invents a number.
none := pricedDoc(t, func(raw map[string]any) { delete(raw, "price") })
if _, ok := none.Priced(); ok {
t.Error("a manifest with no projection reported one")
}
// …and the TREE is still readable, which is what keeps an unpriceable book a book: the intake and
// the materializer must not refuse it, they must refuse to SELL it.
if err := none.Whole(); err != nil {
t.Errorf("an unpriced manifest failed the tree's own check: %v", err)
}
}
// ⛔ `detected` IS TRUSTED AND `declared` IS NOT, which is the opposite of what the words suggest and
// therefore the thing most likely to be "corrected" back by a later reader.
//
// For an EPUB the engine cuts by SPINE DOCUMENTS — one document, one «chapter» — and labels that
// `declared`, honestly, because the format did draw those boundaries. But a spine is the READING
// ORDER, not a table of contents: the real chapter boundaries live in `nav`/NCX, which the engine
// does not read yet. So `declared` today means «this many documents»: a book shipped as one large
// document has one, a book split by scene has dozens. Selling «through chapter 12» against it hands
// a buyer twelve DOCUMENTS under the name of twelve chapters. Ratified 05.09.
//
// Mutation caught: adding StructureDeclared to ChapterOrdersOffered.
func TestOnlyADetectedCutMayBeSoldAgainstInChapters(t *testing.T) {
for structure, want := range map[string]bool{
StructureDetected: true,
StructureDeclared: false,
StructureNone: false,
"": false,
"toc": false, // a word a later engine grows: unknown is untrusted, never refused
} {
if got := (Manifest{Structure: structure}).ChapterOrdersOffered(); got != want {
t.Errorf("structure %q offers chapter orders: %v, want %v", structure, got, want)
}
}
}
// The projection's arithmetic is the ENGINE's and the seam only carries it: the chapters' bills plus
// the book-level bound ARE the book's expected bill. Asserted because the platform prices an ORDER
// from the chapters and shows the BOOK figure beside it, and two numbers that must agree are exactly
// where a silent drift lives.
func TestTheChaptersAndTheBookFigureDescribeTheSameBook(t *testing.T) {
m := pricedDoc(t, nil)
p, ok := m.Priced()
if !ok {
t.Fatal("not priced")
}
sum := money.MicroUSD(0)
for _, c := range m.Chapters {
sum += c.Price.ExpectedUSD
}
// The slack is the conversion's own: every amount is rounded UP independently, so the chapters
// can sum a micro-dollar per chapter above their share.
if diff := p.ExpectedUSD - p.BookOnceUSD - sum; diff < -3 || diff > 3 {
t.Errorf("the chapters sum to %s and the book says %s above its book-level bound",
sum.USD(), (p.ExpectedUSD - p.BookOnceUSD).USD())
}
}