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()) } }