173 lines
7.3 KiB
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())
|
|
}
|
|
}
|